diff --git a/apps/web-graphpolaris/project.json b/apps/web-graphpolaris/project.json
index b3632e1739bf0207d33a72ecab62d724a1523c95..dc9c691acc30a77b498ee4ee9c7abf79928cff15 100644
--- a/apps/web-graphpolaris/project.json
+++ b/apps/web-graphpolaris/project.json
@@ -19,9 +19,7 @@
     },
     "build": {
       "executor": "@nrwl/web:webpack",
-      "outputs": [
-        "{options.outputPath}"
-      ],
+      "outputs": ["{options.outputPath}"],
       "defaultConfiguration": "production",
       "options": {
         "compiler": "babel",
@@ -35,18 +33,18 @@
           "apps/web-graphpolaris/src/favicon.ico",
           "apps/web-graphpolaris/src/assets"
         ],
-        "styles": [
-          "apps/web-graphpolaris/src/styles.scss"
-        ],
+        "styles": ["apps/web-graphpolaris/src/styles.scss"],
         "scripts": [],
         "webpackConfig": "@nrwl/react/plugins/webpack"
       },
       "configurations": {
         "production": {
-          "fileReplacements": [{
-            "replace": "apps/graphpolaris/src/environments/environment.ts",
-            "with": "apps/graphpolaris/src/environments/environment.prod.ts"
-          }],
+          "fileReplacements": [
+            {
+              "replace": "apps/graphpolaris/src/environments/environment.ts",
+              "with": "apps/graphpolaris/src/environments/environment.prod.ts"
+            }
+          ],
           "optimization": true,
           "outputHashing": "all",
           "sourceMap": false,
@@ -71,20 +69,14 @@
     },
     "lint": {
       "executor": "@nrwl/linter:eslint",
-      "outputs": [
-        "{options.outputFile}"
-      ],
+      "outputs": ["{options.outputFile}"],
       "options": {
-        "lintFilePatterns": [
-          "apps/graphpolaris/**/*.{ts,tsx,js,jsx}"
-        ]
+        "lintFilePatterns": ["apps/graphpolaris/**/*.{ts,tsx,js,jsx}"]
       }
     },
     "test": {
       "executor": "@nrwl/jest:jest",
-      "outputs": [
-        "coverage/apps/graphpolaris"
-      ],
+      "outputs": ["coverage/apps/graphpolaris"],
       "options": {
         "jestConfig": "apps/web-graphpolaris/jest.config.js",
         "passWithNoTests": true
@@ -113,9 +105,7 @@
     },
     "build-storybook": {
       "executor": "@nrwl/storybook:build",
-      "outputs": [
-        "{options.outputPath}"
-      ],
+      "outputs": ["{options.outputPath}"],
       "options": {
         "uiFramework": "@storybook/react",
         "outputPath": "dist/storybook/graphpolaris",
@@ -131,4 +121,4 @@
     }
   },
   "tags": []
-}
\ No newline at end of file
+}
diff --git a/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts
index a466f310151f9e8a39af521c66d4487032795662..c3c71d7a5ba4910f9f19e45ae5f659d87f690ac6 100644
--- a/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts
+++ b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts
@@ -1,25 +1,26 @@
-import { SchemaFromBackend } from '@graphpolaris/shared/data-access/store';
+import { SchemaFromBackend } from '@graphpolaris/models';
+import { SchemaUtils } from '@graphpolaris/schema-utils';
 import { MultiGraph } from 'graphology';
-import { parse } from 'path/posix';
-import { parseSchemaFromBackend } from '..';
 import { Attributes } from 'graphology-types';
 import {
-  movieSchema,
-  northWindSchema,
-  simpleSchema,
-  twitterSchema,
+  movieSchemaRaw,
+  northwindSchemaRaw,
+  simpleSchemaRaw,
+  twitterSchemaRaw,
 } from 'libs/shared/mock-data/src';
 
 describe('SchemaUsecases', () => {
   test.each([
-    { data: simpleSchema },
-    { data: movieSchema },
-    { data: northWindSchema },
-    { data: twitterSchema },
+    { data: simpleSchemaRaw },
+    { data: movieSchemaRaw },
+    { data: northwindSchemaRaw },
+    { data: twitterSchemaRaw },
   ])('parseSchemaFromBackend parsing should work', ({ data }) => {
     // console.log('testinput', input);
 
-    const parsed = parseSchemaFromBackend(data as SchemaFromBackend);
+    const parsed = SchemaUtils.ParseSchemaFromBackend(
+      data as SchemaFromBackend
+    );
     expect(parsed).toBeDefined();
 
     let parsedNodeAttributes: Attributes[] = [];
@@ -52,17 +53,19 @@ describe('SchemaUsecases', () => {
   });
 
   it('should export and reimport', () => {
-    const parsed = parseSchemaFromBackend(simpleSchema as SchemaFromBackend);
+    const parsed = SchemaUtils.ParseSchemaFromBackend(
+      simpleSchemaRaw as SchemaFromBackend
+    );
     const reload = MultiGraph.from(parsed.export());
 
     expect(parsed).toStrictEqual(reload);
   });
 
   test.each([
-    { data: simpleSchema },
-    { data: movieSchema },
-    { data: northWindSchema },
-    { data: twitterSchema },
+    { data: simpleSchemaRaw },
+    { data: movieSchemaRaw },
+    { data: northwindSchemaRaw },
+    { data: twitterSchemaRaw },
   ])('should load my test json $data', ({ data }) => {
     expect(data).toBeDefined();
     expect(data.nodes).toBeDefined();
diff --git a/libs/schema/schema-usecases/src/lib/schema-usecases.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.ts
index ea3fdc0fadfcfa9edf4b318ee1bf2ab0de50ccdc..323e1bb7f6679e35d4d6799ca4d5ad1faaaeeb25 100644
--- a/libs/schema/schema-usecases/src/lib/schema-usecases.ts
+++ b/libs/schema/schema-usecases/src/lib/schema-usecases.ts
@@ -181,32 +181,32 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> {
   return initialElements;
 }
 
-export function parseSchemaFromBackend(
-  schemaFromBackend: SchemaFromBackend
-): Graph {
-  const { nodes, edges } = schemaFromBackend;
-  // Instantiate a directed graph that allows self loops and parallel edges
-  const schemaGraph = new MultiGraph({ allowSelfLoops: true });
-  // console.log('parsing schema');
-  // The graph schema needs a node for each node AND edge. These need then be connected
-
-  nodes.forEach((node) => {
-    schemaGraph.addNode(node.name, {
-      name: node.name,
-      attributes: node.attributes,
-      x: 0,
-      y: 0,
-    });
-  });
+// export function parseSchemaFromBackend(
+//   schemaFromBackend: SchemaFromBackend
+// ): Graph {
+//   const { nodes, edges } = schemaFromBackend;
+//   // Instantiate a directed graph that allows self loops and parallel edges
+//   const schemaGraph = new MultiGraph({ allowSelfLoops: true });
+//   // console.log('parsing schema');
+//   // The graph schema needs a node for each node AND edge. These need then be connected
+
+//   nodes.forEach((node) => {
+//     schemaGraph.addNode(node.name, {
+//       name: node.name,
+//       attributes: node.attributes,
+//       x: 0,
+//       y: 0,
+//     });
+//   });
 
-  // The name of the edge will be name + from + to, since edge names are not unique
-  edges.forEach((edge) => {
-    const edgeID = [edge.name, '_', edge.from, edge.to].join(''); //ensure that all interpreted as string
+//   // The name of the edge will be name + from + to, since edge names are not unique
+//   edges.forEach((edge) => {
+//     const edgeID = [edge.name, '_', edge.from, edge.to].join(''); //ensure that all interpreted as string
 
-    // This node is the actual edge
-    schemaGraph.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, {
-      attribute: edge.attributes,
-    });
-  });
-  return schemaGraph;
-}
+//     // This node is the actual edge
+//     schemaGraph.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, {
+//       attribute: edge.attributes,
+//     });
+//   });
+//   return schemaGraph;
+// }
diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.ts b/libs/shared/data-access/store/src/lib/schemaSlice.ts
index eb8fdb5627d94c45a5445e45e2e18f6386efefb9..92a7270fddcc5779a83913708a473c62dd62be32 100644
--- a/libs/shared/data-access/store/src/lib/schemaSlice.ts
+++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts
@@ -1,35 +1,9 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { RootState } from './store';
 import Graph, { MultiGraph } from 'graphology';
+import { SchemaFromBackend } from 'libs/shared/models/src';
 
-/*************** schema format from the backend *************** */
-// TODO: should probably not live here
-/** Schema type, consist of nodes and edges */
-export type SchemaFromBackend = {
-  edges: Edge[];
-  nodes: Node[];
-};
-
-/** Attribute type, consist of a name */
-export type Attribute = {
-  name: string;
-  type: 'string' | 'int' | 'bool' | 'float';
-};
 
-/** Node type, consist of a name and a list of attributes */
-export type Node = {
-  name: string;
-  attributes: Attribute[];
-};
-
-/** Edge type, consist of a name, start point, end point and a list of attributes */
-export type Edge = {
-  name: string;
-  to: string;
-  from: string;
-  collection: string;
-  attributes: Attribute[];
-};
 /**************************************************************** */
 
 // Define the initial state using that type
diff --git a/libs/shared/graph-layout/.babelrc b/libs/shared/graph-layout/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe
--- /dev/null
+++ b/libs/shared/graph-layout/.babelrc
@@ -0,0 +1,12 @@
+{
+  "presets": [
+    [
+      "@nrwl/react/babel",
+      {
+        "runtime": "automatic",
+        "useBuiltIns": "usage"
+      }
+    ]
+  ],
+  "plugins": []
+}
diff --git a/libs/shared/graph-layout/.eslintrc.json b/libs/shared/graph-layout/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2
--- /dev/null
+++ b/libs/shared/graph-layout/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/libs/shared/graph-layout/README.md b/libs/shared/graph-layout/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9925976b0df4b219612f8e6c9c29f0517911ed7c
--- /dev/null
+++ b/libs/shared/graph-layout/README.md
@@ -0,0 +1,7 @@
+# shared-graph-layout
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-graph-layout` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/graph-layout/jest.config.js b/libs/shared/graph-layout/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae3d05af50147b0e99ce13f1af409e0ca0752c64
--- /dev/null
+++ b/libs/shared/graph-layout/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  displayName: 'shared-graph-layout',
+  preset: '../../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]sx?$': 'babel-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../../coverage/libs/shared/graph-layout',
+};
diff --git a/libs/shared/graph-layout/node_modules/.yarn-integrity b/libs/shared/graph-layout/node_modules/.yarn-integrity
new file mode 100644
index 0000000000000000000000000000000000000000..8f6a4c8369cd7aff9555de934fb2b12041c4439b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/.yarn-integrity
@@ -0,0 +1,38 @@
+{
+  "systemParams": "win32-x64-83",
+  "modulesFolders": [
+    "node_modules"
+  ],
+  "flags": [],
+  "linkedModules": [],
+  "topLevelPatterns": [
+    "graphology-generators@^0.11.2",
+    "graphology-layout-forceatlas2@^0.8.2",
+    "graphology-layout-noverlap@^0.4.2",
+    "graphology-layout@^0.5.0",
+    "graphology@^0.24.1"
+  ],
+  "lockfileEntries": {
+    "@yomguithereal/helpers@^1.1.1": "https://registry.yarnpkg.com/@yomguithereal/helpers/-/helpers-1.1.1.tgz#185dfb0f88ca2beec53d0adf6eed15c33b1c549d",
+    "events@^3.3.0": "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400",
+    "graphology-generators@^0.11.2": "https://registry.yarnpkg.com/graphology-generators/-/graphology-generators-0.11.2.tgz#eff2c97e4f5bf401e86ab045470dded95f2ebe24",
+    "graphology-indices@^0.16.3": "https://registry.yarnpkg.com/graphology-indices/-/graphology-indices-0.16.6.tgz#0de112ef0367e44041490933e34ad2075cb24e80",
+    "graphology-layout-forceatlas2@^0.8.2": "https://registry.yarnpkg.com/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.8.2.tgz#7cb5b2fa00fd5445cb2b73c333e36ef22c8a82a8",
+    "graphology-layout-noverlap@^0.4.2": "https://registry.yarnpkg.com/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz#2ffa054ceeebaa31fcffe695d271fc55707cd29c",
+    "graphology-layout@^0.5.0": "https://registry.yarnpkg.com/graphology-layout/-/graphology-layout-0.5.0.tgz#a0a54861cebae5f486c778dbdafc6294859f23b5",
+    "graphology-metrics@^2.0.0": "https://registry.yarnpkg.com/graphology-metrics/-/graphology-metrics-2.1.0.tgz#7d00bae92d8970583afd020e6d40d8a16c378002",
+    "graphology-shortest-path@^2.0.0": "https://registry.yarnpkg.com/graphology-shortest-path/-/graphology-shortest-path-2.0.0.tgz#27a01b3a9980872bd44a197fc77114623dd2b302",
+    "graphology-utils@^2.1.0": "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c",
+    "graphology-utils@^2.3.0": "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c",
+    "graphology-utils@^2.4.2": "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c",
+    "graphology-utils@^2.4.3": "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c",
+    "graphology-utils@^2.4.4": "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c",
+    "graphology@^0.24.1": "https://registry.yarnpkg.com/graphology/-/graphology-0.24.1.tgz#035e452e294b01168cf5c85d5dd0a4b7e4837d87",
+    "mnemonist@^0.39.0": "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.0.tgz#4c83dd22e8d9d05dfb721ff66a905fec4c460041",
+    "obliterator@^2.0.1": "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21",
+    "obliterator@^2.0.2": "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21",
+    "pandemonium@^1.5.0": "https://registry.yarnpkg.com/pandemonium/-/pandemonium-1.5.0.tgz#93f35af555de1420022b341e730215c51c725be3"
+  },
+  "files": [],
+  "artifacts": {}
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/LICENSE.txt b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2cc931644b6e2ba0b755d2f27b569c40cc043dfe
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/README.md b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a619035fb22300f332332eefd921ff543846028f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/README.md
@@ -0,0 +1,33 @@
+[![Build Status](https://travis-ci.org/Yomguithereal/helpers.svg)](https://travis-ci.org/Yomguithereal/helpers)
+
+# Yomguithereal's helpers
+
+Miscellaneous helper functions.
+
+## Installation
+
+```
+npm install @yomguithereal/helpers
+```
+
+## Usage
+
+* [#.extend](#extend)
+
+## #.extend
+
+Pushes multiple values to the given array.
+
+It is faster than doing `array.push.apply(array, values)` and works by first mutating the array's length and then setting the new indices (benchmark proved it is faster than a loop of pushes).
+
+```js
+import extend from '@yomguithereal/helpers/extend';
+
+const a = [1, 2, 3];
+extend(a, [4, 5, 6]);
+
+a
+>>> [1, 2, 3, 4, 5, 6]
+```
+
+
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.d.ts b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc4182b12279aef485ed9715433b22e9864f9bc5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.d.ts
@@ -0,0 +1 @@
+export default function extend<T>(array: Array<T>, values: Array<T>): void;
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.js b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8fe843051320c2114261196c311e1577effe962
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/extend.js
@@ -0,0 +1,41 @@
+/**
+ * Extend function
+ * ================
+ *
+ * Function used to push a bunch of values into an array at once.
+ *
+ * Its strategy is to mutate target array's length then setting the new indices
+ * to be the values to add.
+ *
+ * A benchmark proved that it is faster than the following strategies:
+ *   1) `array.push.apply(array, values)`.
+ *   2) A loop of pushes.
+ *   3) `array = array.concat(values)`, obviously.
+ *
+ * Intuitively, this is correct because when adding a lot of elements, the
+ * chosen strategies does not need to handle the `arguments` object to
+ * execute #.apply's variadicity and because the array know its final length
+ * at the beginning, avoiding potential multiple reallocations of the underlying
+ * contiguous array. Some engines may be able to optimize the loop of push
+ * operations but empirically they don't seem to do so.
+ */
+
+/**
+ * Extends the target array with the given values.
+ *
+ * @param  {array} array  - Target array.
+ * @param  {array} values - Values to add.
+ */
+module.exports = function extend(array, values) {
+  var l2 = values.length;
+
+  if (l2 === 0)
+    return;
+
+  var l1 = array.length;
+
+  array.length += l2;
+
+  for (var i = 0; i < l2; i++)
+    array[l1 + i] = values[i];
+};
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.d.ts b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9feff994822f43dca10f1dc5c9cea936679ed0b3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.d.ts
@@ -0,0 +1 @@
+export {default as extend} from './extend';
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.js b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d45ba94a125d9c78ce4eb88b664998eef122641a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/index.js
@@ -0,0 +1,7 @@
+/**
+ * Miscellaneous helper functions.
+ * ================================
+ *
+ * Library endpoint.
+ */
+exports.extend = require('./extend.js');
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/package.json b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..bdbcf8c8c181292b51612bccdaedd8dbe3a141c1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "@yomguithereal/helpers",
+  "version": "1.1.1",
+  "description": "Miscellaneous helper functions.",
+  "main": "index.js",
+  "files": [
+    "*.d.ts",
+    "*.js"
+  ],
+  "types": "./index.d.ts",
+  "scripts": {
+    "lint": "eslint **/*.js",
+    "prepublish": "npm run lint && npm test",
+    "test": "mocha test.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/Yomguithereal/helpers.git"
+  },
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/Yomguithereal/helpers/issues"
+  },
+  "homepage": "https://github.com/Yomguithereal/helpers#readme",
+  "devDependencies": {
+    "@yomguithereal/eslint-config": "^4.0.0",
+    "eslint": "^7.10.0",
+    "mocha": "^8.1.3"
+  },
+  "eslintConfig": {
+    "extends": "@yomguithereal/eslint-config"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/test.js b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/test.js
new file mode 100644
index 0000000000000000000000000000000000000000..30db6431161c19b7a592369fec7c722bf2714aae
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/@yomguithereal/helpers/test.js
@@ -0,0 +1,19 @@
+/**
+ * Helpers Unit Tests
+ * ===================
+ */
+var assert = require('assert'),
+    lib = require('./');
+
+describe('#.extend', function() {
+  it('should correctly extend the given array.', function() {
+    var A = [1, 2, 3],
+        B = [4, 5, 6];
+
+    lib.extend(A, B);
+
+    assert.strictEqual(A.length, 6);
+    assert.strictEqual(B.length, 3);
+    assert.deepEqual(A, [1, 2, 3, 4, 5, 6]);
+  });
+});
diff --git a/libs/shared/graph-layout/node_modules/events/.airtap.yml b/libs/shared/graph-layout/node_modules/events/.airtap.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7a8a87d5e99d1b538b50bb40bd3e8a3d44b6f3b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/.airtap.yml
@@ -0,0 +1,15 @@
+sauce_connect: true
+loopback: airtap.local
+browsers:
+  - name: chrome
+    version: latest
+  - name: firefox
+    version: latest
+  - name: safari
+    version: 9..latest
+  - name: iphone
+    version: latest
+  - name: ie
+    version: 9..latest
+  - name: microsoftedge
+    version: 13..latest
diff --git a/libs/shared/graph-layout/node_modules/events/.github/FUNDING.yml b/libs/shared/graph-layout/node_modules/events/.github/FUNDING.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b8cb78ba90207e382cd5384d4a4bcc78e90113a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: npm/events
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/libs/shared/graph-layout/node_modules/events/.travis.yml b/libs/shared/graph-layout/node_modules/events/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..486dc3c4c1df7fe432eb19fa9174b2e33b263e37
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/.travis.yml
@@ -0,0 +1,18 @@
+dist: xenial
+os: linux
+language: node_js
+node_js:
+  - 'stable'
+  - 'lts/*'
+  - '0.12'
+script:
+  - npm test
+  - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ]; then npm run test:browsers; fi
+addons:
+  sauce_connect: true
+  hosts:
+    - airtap.local
+env:
+  global:
+  - secure: XcBiD8yReflut9q7leKsigDZ0mI3qTKH+QrNVY8DaqlomJOZw8aOrVuX9Jz12l86ZJ41nbxmKnRNkFzcVr9mbP9YaeTb3DpeOBWmvaoSfud9Wnc16VfXtc1FCcwDhSVcSiM3UtnrmFU5cH+Dw1LPh5PbfylYOS/nJxUvG0FFLqI=
+  - secure: jNWtEbqhUdQ0xXDHvCYfUbKYeJCi6a7B4LsrcxYCyWWn4NIgncE5x2YbB+FSUUFVYfz0dsn5RKP1oHB99f0laUEo18HBNkrAS/rtyOdVzcpJjbQ6kgSILGjnJD/Ty1B57Rcz3iyev5Y7bLZ6Y1FbDnk/i9/l0faOGz8vTC3Vdkc=
diff --git a/libs/shared/graph-layout/node_modules/events/History.md b/libs/shared/graph-layout/node_modules/events/History.md
new file mode 100644
index 0000000000000000000000000000000000000000..f48bf210da3ea23213ce37ebd13cd5788c9563c9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/History.md
@@ -0,0 +1,118 @@
+# 3.3.0
+
+ - Support EventTarget emitters in `events.once` from Node.js 12.11.0.
+
+   Now you can use the `events.once` function with objects that implement the EventTarget interface. This interface is used widely in
+   the DOM and other web APIs.
+
+   ```js
+   var events = require('events');
+   var assert = require('assert');
+
+   async function connect() {
+     var ws = new WebSocket('wss://example.com');
+     await events.once(ws, 'open');
+     assert(ws.readyState === WebSocket.OPEN);
+   }
+
+   async function onClick() {
+     await events.once(document.body, 'click');
+     alert('you clicked the page!');
+   }
+   ```
+
+# 3.2.0
+
+ - Add `events.once` from Node.js 11.13.0.
+
+   To use this function, Promises must be supported in the environment. Use a polyfill like `es6-promise` if you support older browsers.
+
+# 3.1.0 (2020-01-08)
+
+`events` now matches the Node.js 11.12.0 API.
+
+  - pass through return value in wrapped `emitter.once()` listeners
+
+    Now, this works:
+    ```js
+    emitter.once('myevent', function () { return 1; });
+    var listener = emitter.rawListeners('myevent')[0]
+    assert(listener() === 1);
+    ```
+    Previously, `listener()` would return undefined regardless of the implementation.
+
+    Ported from https://github.com/nodejs/node/commit/acc506c2d2771dab8d7bba6d3452bc5180dff7cf
+
+  - Reduce code duplication in listener type check ([#67](https://github.com/Gozala/events/pull/67) by [@friederbluemle](https://github.com/friederbluemle)).
+  - Improve `emitter.once()` performance in some engines
+
+# 3.0.0 (2018-05-25)
+
+**This version drops support for IE8.** `events` no longer includes polyfills
+for ES5 features. If you need to support older environments, use an ES5 shim
+like [es5-shim](https://npmjs.com/package/es5-shim). Both the shim and sham
+versions of es5-shim are necessary.
+
+  - Update to events code from Node.js 10.x
+    - (semver major) Adds `off()` method
+  - Port more tests from Node.js
+  - Switch browser tests to airtap, making things more reliable
+
+# 2.1.0 (2018-05-25)
+
+  - add Emitter#rawListeners from Node.js v9.4
+
+# 2.0.0 (2018-02-02)
+
+  - Update to events code from node.js 8.x
+    - Adds `prependListener()` and `prependOnceListener()`
+    - Adds `eventNames()` method
+    - (semver major) Unwrap `once()` listeners in `listeners()`
+  - copy tests from node.js
+
+Note that this version doubles the gzipped size, jumping from 1.1KB to 2.1KB,
+due to new methods and runtime performance improvements. Be aware of that when
+upgrading.
+
+# 1.1.1 (2016-06-22)
+
+  - add more context to errors if they are not instanceof Error
+
+# 1.1.0 (2015-09-29)
+
+  - add Emitter#listerCount (to match node v4 api)
+
+# 1.0.2 (2014-08-28)
+
+  - remove un-reachable code
+  - update devDeps
+
+## 1.0.1 / 2014-05-11
+
+  - check for console.trace before using it
+
+## 1.0.0 / 2013-12-10
+
+  - Update to latest events code from node.js 0.10
+  - copy tests from node.js
+
+## 0.4.0 / 2011-07-03 ##
+
+  - Switching to graphquire@0.8.0
+
+## 0.3.0 / 2011-07-03 ##
+
+  - Switching to URL based module require.
+
+## 0.2.0 / 2011-06-10 ##
+
+  - Simplified package structure.
+  - Graphquire for dependency management.
+
+## 0.1.1 / 2011-05-16 ##
+
+  - Unhandled errors are logged via console.error
+
+## 0.1.0 / 2011-04-22 ##
+
+  - Initial release
diff --git a/libs/shared/graph-layout/node_modules/events/LICENSE b/libs/shared/graph-layout/node_modules/events/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..52ed3b0a63274d653474abc893d6f7221fc301fd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/LICENSE
@@ -0,0 +1,22 @@
+MIT
+
+Copyright Joyent, Inc. and other Node contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/events/Readme.md b/libs/shared/graph-layout/node_modules/events/Readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..80849c0b2d7c39727a540f369d764ee36ef5ee07
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/Readme.md
@@ -0,0 +1,50 @@
+# events [![Build Status](https://travis-ci.org/Gozala/events.png?branch=master)](https://travis-ci.org/Gozala/events)
+
+> Node's event emitter for all engines.
+
+This implements the Node.js [`events`][node.js docs] module for environments that do not have it, like browsers.
+
+> `events` currently matches the **Node.js 11.13.0** API.
+
+Note that the `events` module uses ES5 features. If you need to support very old browsers like IE8, use a shim like [`es5-shim`](https://www.npmjs.com/package/es5-shim). You need both the shim and the sham versions of `es5-shim`.
+
+This module is maintained, but only by very few people. If you'd like to help, let us know in the [Maintainer Needed](https://github.com/Gozala/events/issues/43) issue!
+
+## Install
+
+You usually do not have to install `events` yourself! If your code runs in Node.js, `events` is built in. If your code runs in the browser, bundlers like [browserify](https://github.com/browserify/browserify) or [webpack](https://github.com/webpack/webpack) also include the `events` module.
+
+But if none of those apply, with npm do:
+
+```
+npm install events
+```
+
+## Usage
+
+```javascript
+var EventEmitter = require('events')
+
+var ee = new EventEmitter()
+ee.on('message', function (text) {
+  console.log(text)
+})
+ee.emit('message', 'hello world')
+```
+
+## API
+
+See the [Node.js EventEmitter docs][node.js docs]. `events` currently matches the Node.js 11.13.0 API.
+
+## Contributing
+
+PRs are very welcome! The main way to contribute to `events` is by porting features, bugfixes and tests from Node.js. Ideally, code contributions to this module are copy-pasted from Node.js and transpiled to ES5, rather than reimplemented from scratch. Matching the Node.js code as closely as possible makes maintenance simpler when new changes land in Node.js.
+This module intends to provide exactly the same API as Node.js, so features that are not available in the core `events` module will not be accepted. Feature requests should instead be directed at [nodejs/node](https://github.com/nodejs/node) and will be added to this module once they are implemented in Node.js.
+
+If there is a difference in behaviour between Node.js's `events` module and this module, please open an issue!
+
+## License
+
+[MIT](./LICENSE)
+
+[node.js docs]: https://nodejs.org/dist/v11.13.0/docs/api/events.html
diff --git a/libs/shared/graph-layout/node_modules/events/events.js b/libs/shared/graph-layout/node_modules/events/events.js
new file mode 100644
index 0000000000000000000000000000000000000000..34b69a0b4a6e124e8de119540980bbb4d6cdf4d6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/events.js
@@ -0,0 +1,497 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+
+var R = typeof Reflect === 'object' ? Reflect : null
+var ReflectApply = R && typeof R.apply === 'function'
+  ? R.apply
+  : function ReflectApply(target, receiver, args) {
+    return Function.prototype.apply.call(target, receiver, args);
+  }
+
+var ReflectOwnKeys
+if (R && typeof R.ownKeys === 'function') {
+  ReflectOwnKeys = R.ownKeys
+} else if (Object.getOwnPropertySymbols) {
+  ReflectOwnKeys = function ReflectOwnKeys(target) {
+    return Object.getOwnPropertyNames(target)
+      .concat(Object.getOwnPropertySymbols(target));
+  };
+} else {
+  ReflectOwnKeys = function ReflectOwnKeys(target) {
+    return Object.getOwnPropertyNames(target);
+  };
+}
+
+function ProcessEmitWarning(warning) {
+  if (console && console.warn) console.warn(warning);
+}
+
+var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
+  return value !== value;
+}
+
+function EventEmitter() {
+  EventEmitter.init.call(this);
+}
+module.exports = EventEmitter;
+module.exports.once = once;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._eventsCount = 0;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+var defaultMaxListeners = 10;
+
+function checkListener(listener) {
+  if (typeof listener !== 'function') {
+    throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
+  }
+}
+
+Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
+  enumerable: true,
+  get: function() {
+    return defaultMaxListeners;
+  },
+  set: function(arg) {
+    if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
+      throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
+    }
+    defaultMaxListeners = arg;
+  }
+});
+
+EventEmitter.init = function() {
+
+  if (this._events === undefined ||
+      this._events === Object.getPrototypeOf(this)._events) {
+    this._events = Object.create(null);
+    this._eventsCount = 0;
+  }
+
+  this._maxListeners = this._maxListeners || undefined;
+};
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+  if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+    throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
+  }
+  this._maxListeners = n;
+  return this;
+};
+
+function _getMaxListeners(that) {
+  if (that._maxListeners === undefined)
+    return EventEmitter.defaultMaxListeners;
+  return that._maxListeners;
+}
+
+EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
+  return _getMaxListeners(this);
+};
+
+EventEmitter.prototype.emit = function emit(type) {
+  var args = [];
+  for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
+  var doError = (type === 'error');
+
+  var events = this._events;
+  if (events !== undefined)
+    doError = (doError && events.error === undefined);
+  else if (!doError)
+    return false;
+
+  // If there is no 'error' event listener then throw.
+  if (doError) {
+    var er;
+    if (args.length > 0)
+      er = args[0];
+    if (er instanceof Error) {
+      // Note: The comments on the `throw` lines are intentional, they show
+      // up in Node's output if this results in an unhandled exception.
+      throw er; // Unhandled 'error' event
+    }
+    // At least give some kind of context to the user
+    var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
+    err.context = er;
+    throw err; // Unhandled 'error' event
+  }
+
+  var handler = events[type];
+
+  if (handler === undefined)
+    return false;
+
+  if (typeof handler === 'function') {
+    ReflectApply(handler, this, args);
+  } else {
+    var len = handler.length;
+    var listeners = arrayClone(handler, len);
+    for (var i = 0; i < len; ++i)
+      ReflectApply(listeners[i], this, args);
+  }
+
+  return true;
+};
+
+function _addListener(target, type, listener, prepend) {
+  var m;
+  var events;
+  var existing;
+
+  checkListener(listener);
+
+  events = target._events;
+  if (events === undefined) {
+    events = target._events = Object.create(null);
+    target._eventsCount = 0;
+  } else {
+    // To avoid recursion in the case that type === "newListener"! Before
+    // adding it to the listeners, first emit "newListener".
+    if (events.newListener !== undefined) {
+      target.emit('newListener', type,
+                  listener.listener ? listener.listener : listener);
+
+      // Re-assign `events` because a newListener handler could have caused the
+      // this._events to be assigned to a new object
+      events = target._events;
+    }
+    existing = events[type];
+  }
+
+  if (existing === undefined) {
+    // Optimize the case of one listener. Don't need the extra array object.
+    existing = events[type] = listener;
+    ++target._eventsCount;
+  } else {
+    if (typeof existing === 'function') {
+      // Adding the second element, need to change to array.
+      existing = events[type] =
+        prepend ? [listener, existing] : [existing, listener];
+      // If we've already got an array, just append.
+    } else if (prepend) {
+      existing.unshift(listener);
+    } else {
+      existing.push(listener);
+    }
+
+    // Check for listener leak
+    m = _getMaxListeners(target);
+    if (m > 0 && existing.length > m && !existing.warned) {
+      existing.warned = true;
+      // No error code for this since it is a Warning
+      // eslint-disable-next-line no-restricted-syntax
+      var w = new Error('Possible EventEmitter memory leak detected. ' +
+                          existing.length + ' ' + String(type) + ' listeners ' +
+                          'added. Use emitter.setMaxListeners() to ' +
+                          'increase limit');
+      w.name = 'MaxListenersExceededWarning';
+      w.emitter = target;
+      w.type = type;
+      w.count = existing.length;
+      ProcessEmitWarning(w);
+    }
+  }
+
+  return target;
+}
+
+EventEmitter.prototype.addListener = function addListener(type, listener) {
+  return _addListener(this, type, listener, false);
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.prependListener =
+    function prependListener(type, listener) {
+      return _addListener(this, type, listener, true);
+    };
+
+function onceWrapper() {
+  if (!this.fired) {
+    this.target.removeListener(this.type, this.wrapFn);
+    this.fired = true;
+    if (arguments.length === 0)
+      return this.listener.call(this.target);
+    return this.listener.apply(this.target, arguments);
+  }
+}
+
+function _onceWrap(target, type, listener) {
+  var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
+  var wrapped = onceWrapper.bind(state);
+  wrapped.listener = listener;
+  state.wrapFn = wrapped;
+  return wrapped;
+}
+
+EventEmitter.prototype.once = function once(type, listener) {
+  checkListener(listener);
+  this.on(type, _onceWrap(this, type, listener));
+  return this;
+};
+
+EventEmitter.prototype.prependOnceListener =
+    function prependOnceListener(type, listener) {
+      checkListener(listener);
+      this.prependListener(type, _onceWrap(this, type, listener));
+      return this;
+    };
+
+// Emits a 'removeListener' event if and only if the listener was removed.
+EventEmitter.prototype.removeListener =
+    function removeListener(type, listener) {
+      var list, events, position, i, originalListener;
+
+      checkListener(listener);
+
+      events = this._events;
+      if (events === undefined)
+        return this;
+
+      list = events[type];
+      if (list === undefined)
+        return this;
+
+      if (list === listener || list.listener === listener) {
+        if (--this._eventsCount === 0)
+          this._events = Object.create(null);
+        else {
+          delete events[type];
+          if (events.removeListener)
+            this.emit('removeListener', type, list.listener || listener);
+        }
+      } else if (typeof list !== 'function') {
+        position = -1;
+
+        for (i = list.length - 1; i >= 0; i--) {
+          if (list[i] === listener || list[i].listener === listener) {
+            originalListener = list[i].listener;
+            position = i;
+            break;
+          }
+        }
+
+        if (position < 0)
+          return this;
+
+        if (position === 0)
+          list.shift();
+        else {
+          spliceOne(list, position);
+        }
+
+        if (list.length === 1)
+          events[type] = list[0];
+
+        if (events.removeListener !== undefined)
+          this.emit('removeListener', type, originalListener || listener);
+      }
+
+      return this;
+    };
+
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+EventEmitter.prototype.removeAllListeners =
+    function removeAllListeners(type) {
+      var listeners, events, i;
+
+      events = this._events;
+      if (events === undefined)
+        return this;
+
+      // not listening for removeListener, no need to emit
+      if (events.removeListener === undefined) {
+        if (arguments.length === 0) {
+          this._events = Object.create(null);
+          this._eventsCount = 0;
+        } else if (events[type] !== undefined) {
+          if (--this._eventsCount === 0)
+            this._events = Object.create(null);
+          else
+            delete events[type];
+        }
+        return this;
+      }
+
+      // emit removeListener for all listeners on all events
+      if (arguments.length === 0) {
+        var keys = Object.keys(events);
+        var key;
+        for (i = 0; i < keys.length; ++i) {
+          key = keys[i];
+          if (key === 'removeListener') continue;
+          this.removeAllListeners(key);
+        }
+        this.removeAllListeners('removeListener');
+        this._events = Object.create(null);
+        this._eventsCount = 0;
+        return this;
+      }
+
+      listeners = events[type];
+
+      if (typeof listeners === 'function') {
+        this.removeListener(type, listeners);
+      } else if (listeners !== undefined) {
+        // LIFO order
+        for (i = listeners.length - 1; i >= 0; i--) {
+          this.removeListener(type, listeners[i]);
+        }
+      }
+
+      return this;
+    };
+
+function _listeners(target, type, unwrap) {
+  var events = target._events;
+
+  if (events === undefined)
+    return [];
+
+  var evlistener = events[type];
+  if (evlistener === undefined)
+    return [];
+
+  if (typeof evlistener === 'function')
+    return unwrap ? [evlistener.listener || evlistener] : [evlistener];
+
+  return unwrap ?
+    unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
+}
+
+EventEmitter.prototype.listeners = function listeners(type) {
+  return _listeners(this, type, true);
+};
+
+EventEmitter.prototype.rawListeners = function rawListeners(type) {
+  return _listeners(this, type, false);
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  if (typeof emitter.listenerCount === 'function') {
+    return emitter.listenerCount(type);
+  } else {
+    return listenerCount.call(emitter, type);
+  }
+};
+
+EventEmitter.prototype.listenerCount = listenerCount;
+function listenerCount(type) {
+  var events = this._events;
+
+  if (events !== undefined) {
+    var evlistener = events[type];
+
+    if (typeof evlistener === 'function') {
+      return 1;
+    } else if (evlistener !== undefined) {
+      return evlistener.length;
+    }
+  }
+
+  return 0;
+}
+
+EventEmitter.prototype.eventNames = function eventNames() {
+  return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
+};
+
+function arrayClone(arr, n) {
+  var copy = new Array(n);
+  for (var i = 0; i < n; ++i)
+    copy[i] = arr[i];
+  return copy;
+}
+
+function spliceOne(list, index) {
+  for (; index + 1 < list.length; index++)
+    list[index] = list[index + 1];
+  list.pop();
+}
+
+function unwrapListeners(arr) {
+  var ret = new Array(arr.length);
+  for (var i = 0; i < ret.length; ++i) {
+    ret[i] = arr[i].listener || arr[i];
+  }
+  return ret;
+}
+
+function once(emitter, name) {
+  return new Promise(function (resolve, reject) {
+    function errorListener(err) {
+      emitter.removeListener(name, resolver);
+      reject(err);
+    }
+
+    function resolver() {
+      if (typeof emitter.removeListener === 'function') {
+        emitter.removeListener('error', errorListener);
+      }
+      resolve([].slice.call(arguments));
+    };
+
+    eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
+    if (name !== 'error') {
+      addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
+    }
+  });
+}
+
+function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
+  if (typeof emitter.on === 'function') {
+    eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
+  }
+}
+
+function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
+  if (typeof emitter.on === 'function') {
+    if (flags.once) {
+      emitter.once(name, listener);
+    } else {
+      emitter.on(name, listener);
+    }
+  } else if (typeof emitter.addEventListener === 'function') {
+    // EventTarget does not have `error` event semantics like Node
+    // EventEmitters, we do not listen for `error` events here.
+    emitter.addEventListener(name, function wrapListener(arg) {
+      // IE does not have builtin `{ once: true }` support so we
+      // have to do it manually.
+      if (flags.once) {
+        emitter.removeEventListener(name, wrapListener);
+      }
+      listener(arg);
+    });
+  } else {
+    throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/events/package.json b/libs/shared/graph-layout/node_modules/events/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..b9580d88142d2921273599e2afaaec640906df6d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "events",
+  "version": "3.3.0",
+  "description": "Node's event emitter for all engines.",
+  "keywords": [
+    "events",
+    "eventEmitter",
+    "eventDispatcher",
+    "listeners"
+  ],
+  "author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/Gozala/events.git",
+    "web": "https://github.com/Gozala/events"
+  },
+  "bugs": {
+    "url": "http://github.com/Gozala/events/issues/"
+  },
+  "main": "./events.js",
+  "engines": {
+    "node": ">=0.8.x"
+  },
+  "devDependencies": {
+    "airtap": "^1.0.0",
+    "functions-have-names": "^1.2.1",
+    "has": "^1.0.3",
+    "has-symbols": "^1.0.1",
+    "isarray": "^2.0.5",
+    "tape": "^5.0.0"
+  },
+  "scripts": {
+    "test": "node tests/index.js",
+    "test:browsers": "airtap -- tests/index.js"
+  },
+  "license": "MIT"
+}
diff --git a/libs/shared/graph-layout/node_modules/events/security.md b/libs/shared/graph-layout/node_modules/events/security.md
new file mode 100644
index 0000000000000000000000000000000000000000..a14ace6a57db70992630a568df6a7bf57763d26b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/security.md
@@ -0,0 +1,10 @@
+# Security Policy
+
+## Supported Versions
+Only the latest major version is supported at any given time.
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/libs/shared/graph-layout/node_modules/events/tests/add-listeners.js b/libs/shared/graph-layout/node_modules/events/tests/add-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b578272ba889e9732543badfc9077a59f974640
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/add-listeners.js
@@ -0,0 +1,111 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+{
+  var ee = new EventEmitter();
+  var events_new_listener_emitted = [];
+  var listeners_new_listener_emitted = [];
+
+  // Sanity check
+  assert.strictEqual(ee.addListener, ee.on);
+
+  ee.on('newListener', function(event, listener) {
+    // Don't track newListener listeners.
+    if (event === 'newListener')
+      return;
+
+    events_new_listener_emitted.push(event);
+    listeners_new_listener_emitted.push(listener);
+  });
+
+  var hello = common.mustCall(function(a, b) {
+    assert.strictEqual('a', a);
+    assert.strictEqual('b', b);
+  });
+
+  ee.once('newListener', function(name, listener) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(listener, hello);
+
+    var listeners = this.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  });
+
+  ee.on('hello', hello);
+  ee.once('foo', assert.fail);
+
+  assert.ok(Array.isArray(events_new_listener_emitted));
+  assert.strictEqual(events_new_listener_emitted.length, 2);
+  assert.strictEqual(events_new_listener_emitted[0], 'hello');
+  assert.strictEqual(events_new_listener_emitted[1], 'foo');
+
+  assert.ok(Array.isArray(listeners_new_listener_emitted));
+  assert.strictEqual(listeners_new_listener_emitted.length, 2);
+  assert.strictEqual(listeners_new_listener_emitted[0], hello);
+  assert.strictEqual(listeners_new_listener_emitted[1], assert.fail);
+
+  ee.emit('hello', 'a', 'b');
+}
+
+// just make sure that this doesn't throw:
+{
+  var f = new EventEmitter();
+
+  f.setMaxListeners(0);
+}
+
+{
+  var listen1 = function() {};
+  var listen2 = function() {};
+  var ee = new EventEmitter();
+
+  ee.once('newListener', function() {
+    var listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+    ee.once('newListener', function() {
+      var listeners = ee.listeners('hello');
+      assert.ok(Array.isArray(listeners));
+      assert.strictEqual(listeners.length, 0);
+    });
+    ee.on('hello', listen2);
+  });
+  ee.on('hello', listen1);
+  // The order of listeners on an event is not always the order in which the
+  // listeners were added.
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listen2);
+  assert.strictEqual(listeners[1], listen1);
+}
+
+// Verify that the listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.on('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function. Received type object$/);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/check-listener-leaks.js b/libs/shared/graph-layout/node_modules/events/tests/check-listener-leaks.js
new file mode 100644
index 0000000000000000000000000000000000000000..7fce48f37bf24cc63a3df3e88a558a03fdbde2fa
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/check-listener-leaks.js
@@ -0,0 +1,101 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+
+// Redirect warning output to tape.
+var consoleWarn = console.warn;
+console.warn = common.test.comment;
+
+common.test.on('end', function () {
+  console.warn = consoleWarn;
+});
+
+// default
+{
+  var e = new events.EventEmitter();
+
+  for (var i = 0; i < 10; i++) {
+    e.on('default', common.mustNotCall());
+  }
+  assert.ok(!e._events['default'].hasOwnProperty('warned'));
+  e.on('default', common.mustNotCall());
+  assert.ok(e._events['default'].warned);
+
+  // specific
+  e.setMaxListeners(5);
+  for (var i = 0; i < 5; i++) {
+    e.on('specific', common.mustNotCall());
+  }
+  assert.ok(!e._events['specific'].hasOwnProperty('warned'));
+  e.on('specific', common.mustNotCall());
+  assert.ok(e._events['specific'].warned);
+
+  // only one
+  e.setMaxListeners(1);
+  e.on('only one', common.mustNotCall());
+  assert.ok(!e._events['only one'].hasOwnProperty('warned'));
+  e.on('only one', common.mustNotCall());
+  assert.ok(e._events['only one'].hasOwnProperty('warned'));
+
+  // unlimited
+  e.setMaxListeners(0);
+  for (var i = 0; i < 1000; i++) {
+    e.on('unlimited', common.mustNotCall());
+  }
+  assert.ok(!e._events['unlimited'].hasOwnProperty('warned'));
+}
+
+// process-wide
+{
+  events.EventEmitter.defaultMaxListeners = 42;
+  var e = new events.EventEmitter();
+
+  for (var i = 0; i < 42; ++i) {
+    e.on('fortytwo', common.mustNotCall());
+  }
+  assert.ok(!e._events['fortytwo'].hasOwnProperty('warned'));
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(e._events['fortytwo'].hasOwnProperty('warned'));
+  delete e._events['fortytwo'].warned;
+
+  events.EventEmitter.defaultMaxListeners = 44;
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(!e._events['fortytwo'].hasOwnProperty('warned'));
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(e._events['fortytwo'].hasOwnProperty('warned'));
+}
+
+// but _maxListeners still has precedence over defaultMaxListeners
+{
+  events.EventEmitter.defaultMaxListeners = 42;
+  var e = new events.EventEmitter();
+  e.setMaxListeners(1);
+  e.on('uno', common.mustNotCall());
+  assert.ok(!e._events['uno'].hasOwnProperty('warned'));
+  e.on('uno', common.mustNotCall());
+  assert.ok(e._events['uno'].hasOwnProperty('warned'));
+
+  // chainable
+  assert.strictEqual(e, e.setMaxListeners(1));
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/common.js b/libs/shared/graph-layout/node_modules/events/tests/common.js
new file mode 100644
index 0000000000000000000000000000000000000000..49569b05f59d5a6c4956cc3bcea2dfd3d426d632
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/common.js
@@ -0,0 +1,104 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var test = require('tape');
+var assert = require('assert');
+
+var noop = function() {};
+
+var mustCallChecks = [];
+
+function runCallChecks(exitCode) {
+  if (exitCode !== 0) return;
+
+  var failed = filter(mustCallChecks, function(context) {
+    if ('minimum' in context) {
+      context.messageSegment = 'at least ' + context.minimum;
+      return context.actual < context.minimum;
+    } else {
+      context.messageSegment = 'exactly ' + context.exact;
+      return context.actual !== context.exact;
+    }
+  });
+
+  for (var i = 0; i < failed.length; i++) {
+    var context = failed[i];
+    console.log('Mismatched %s function calls. Expected %s, actual %d.',
+        context.name,
+        context.messageSegment,
+        context.actual);
+    // IE8 has no .stack
+    if (context.stack) console.log(context.stack.split('\n').slice(2).join('\n'));
+  }
+
+  assert.strictEqual(failed.length, 0);
+}
+
+exports.mustCall = function(fn, exact) {
+  return _mustCallInner(fn, exact, 'exact');
+};
+
+function _mustCallInner(fn, criteria, field) {
+  if (typeof criteria == 'undefined') criteria = 1;
+
+  if (typeof fn === 'number') {
+    criteria = fn;
+    fn = noop;
+  } else if (fn === undefined) {
+    fn = noop;
+  }
+
+  if (typeof criteria !== 'number')
+    throw new TypeError('Invalid ' + field + ' value: ' + criteria);
+
+  var context = {
+    actual: 0,
+    stack: (new Error()).stack,
+    name: fn.name || '<anonymous>'
+  };
+
+  context[field] = criteria;
+
+  // add the exit listener only once to avoid listener leak warnings
+  if (mustCallChecks.length === 0) test.onFinish(function() { runCallChecks(0); });
+
+  mustCallChecks.push(context);
+
+  return function() {
+    context.actual++;
+    return fn.apply(this, arguments);
+  };
+}
+
+exports.mustNotCall = function(msg) {
+  return function mustNotCall() {
+    assert.fail(msg || 'function should not have been called');
+  };
+};
+
+function filter(arr, fn) {
+  if (arr.filter) return arr.filter(fn);
+  var filtered = [];
+  for (var i = 0; i < arr.length; i++) {
+    if (fn(arr[i], i, arr)) filtered.push(arr[i]);
+  }
+  return filtered
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/errors.js b/libs/shared/graph-layout/node_modules/events/tests/errors.js
new file mode 100644
index 0000000000000000000000000000000000000000..a23df437f05d39628251c8fa9dfb72baf72b749e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/errors.js
@@ -0,0 +1,13 @@
+'use strict';
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var EE = new EventEmitter();
+
+assert.throws(function () {
+  EE.emit('error', 'Accepts a string');
+}, 'Error: Unhandled error. (Accepts a string)');
+
+assert.throws(function () {
+  EE.emit('error', { message: 'Error!' });
+}, 'Unhandled error. ([object Object])');
diff --git a/libs/shared/graph-layout/node_modules/events/tests/events-list.js b/libs/shared/graph-layout/node_modules/events/tests/events-list.js
new file mode 100644
index 0000000000000000000000000000000000000000..08aa62177e2c291efcbcc57a9010f4db8625a05c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/events-list.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var EE = new EventEmitter();
+var m = function() {};
+EE.on('foo', function() {});
+assert.equal(1, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+EE.on('bar', m);
+assert.equal(2, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+assert.equal('bar', EE.eventNames()[1]);
+EE.removeListener('bar', m);
+assert.equal(1, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+
+if (typeof Symbol !== 'undefined') {
+  var s = Symbol('s');
+  EE.on(s, m);
+  assert.equal(2, EE.eventNames().length);
+  assert.equal('foo', EE.eventNames()[0]);
+  assert.equal(s, EE.eventNames()[1]);
+  EE.removeListener(s, m);
+  assert.equal(1, EE.eventNames().length);
+  assert.equal('foo', EE.eventNames()[0]);
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/events-once.js b/libs/shared/graph-layout/node_modules/events/tests/events-once.js
new file mode 100644
index 0000000000000000000000000000000000000000..dae864963daae6fbac8b459a6358008189e82197
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/events-once.js
@@ -0,0 +1,234 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../').EventEmitter;
+var once = require('../').once;
+var has = require('has');
+var assert = require('assert');
+
+function Event(type) {
+  this.type = type;
+}
+
+function EventTargetMock() {
+  this.events = {};
+
+  this.addEventListener = common.mustCall(this.addEventListener);
+  this.removeEventListener = common.mustCall(this.removeEventListener);
+}
+
+EventTargetMock.prototype.addEventListener = function addEventListener(name, listener, options) {
+  if (!(name in this.events)) {
+    this.events[name] = { listeners: [], options: options || {} }
+  }
+  this.events[name].listeners.push(listener);
+};
+
+EventTargetMock.prototype.removeEventListener = function removeEventListener(name, callback) {
+  if (!(name in this.events)) {
+    return;
+  }
+  var event = this.events[name];
+  var stack = event.listeners;
+
+  for (var i = 0, l = stack.length; i < l; i++) {
+    if (stack[i] === callback) {
+      stack.splice(i, 1);
+      if (stack.length === 0) {
+        delete this.events[name];
+      }
+      return;
+    }
+  }
+};
+
+EventTargetMock.prototype.dispatchEvent = function dispatchEvent(arg) {
+  if (!(arg.type in this.events)) {
+    return true;
+  }
+
+  var event = this.events[arg.type];
+  var stack = event.listeners.slice();
+
+  for (var i = 0, l = stack.length; i < l; i++) {
+    stack[i].call(null, arg);
+    if (event.options.once) {
+      this.removeEventListener(arg.type, stack[i]);
+    }
+  }
+  return !arg.defaultPrevented;
+};
+
+function onceAnEvent() {
+  var ee = new EventEmitter();
+
+  process.nextTick(function () {
+    ee.emit('myevent', 42);
+  });
+
+  return once(ee, 'myevent').then(function (args) {
+    var value = args[0]
+    assert.strictEqual(value, 42);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceAnEventWithTwoArgs() {
+  var ee = new EventEmitter();
+
+  process.nextTick(function () {
+    ee.emit('myevent', 42, 24);
+  });
+
+  return once(ee, 'myevent').then(function (value) {
+    assert.strictEqual(value.length, 2);
+    assert.strictEqual(value[0], 42);
+    assert.strictEqual(value[1], 24);
+  });
+}
+
+function catchesErrors() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  var err;
+  process.nextTick(function () {
+    ee.emit('error', expected);
+  });
+
+  return once(ee, 'myevent').then(function () {
+    throw new Error('should reject')
+  }, function (err) {
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function stopListeningAfterCatchingError() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  var err;
+  process.nextTick(function () {
+    ee.emit('error', expected);
+    ee.emit('myevent', 42, 24);
+  });
+
+  // process.on('multipleResolves', common.mustNotCall());
+
+  return once(ee, 'myevent').then(common.mustNotCall, function (err) {
+    // process.removeAllListeners('multipleResolves');
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceError() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  process.nextTick(function () {
+    ee.emit('error', expected);
+  });
+
+  var promise = once(ee, 'error');
+  assert.strictEqual(ee.listenerCount('error'), 1);
+  return promise.then(function (args) {
+    var err = args[0]
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceWithEventTarget() {
+  var et = new EventTargetMock();
+  var event = new Event('myevent');
+  process.nextTick(function () {
+    et.dispatchEvent(event);
+  });
+  return once(et, 'myevent').then(function (args) {
+    var value = args[0];
+    assert.strictEqual(value, event);
+    assert.strictEqual(has(et.events, 'myevent'), false);
+  });
+}
+
+function onceWithEventTargetError() {
+  var et = new EventTargetMock();
+  var error = new Event('error');
+  process.nextTick(function () {
+    et.dispatchEvent(error);
+  });
+  return once(et, 'error').then(function (args) {
+    var err = args[0];
+    assert.strictEqual(err, error);
+    assert.strictEqual(has(et.events, 'error'), false);
+  });
+}
+
+function prioritizesEventEmitter() {
+  var ee = new EventEmitter();
+  ee.addEventListener = assert.fail;
+  ee.removeAllListeners = assert.fail;
+  process.nextTick(function () {
+    ee.emit('foo');
+  });
+  return once(ee, 'foo');
+}
+
+var allTests = [
+  onceAnEvent(),
+  onceAnEventWithTwoArgs(),
+  catchesErrors(),
+  stopListeningAfterCatchingError(),
+  onceError(),
+  onceWithEventTarget(),
+  onceWithEventTargetError(),
+  prioritizesEventEmitter()
+];
+
+var hasBrowserEventTarget = false;
+try {
+  hasBrowserEventTarget = typeof (new window.EventTarget().addEventListener) === 'function' &&
+    new window.Event('xyz').type === 'xyz';
+} catch (err) {}
+
+if (hasBrowserEventTarget) {
+  var onceWithBrowserEventTarget = function onceWithBrowserEventTarget() {
+    var et = new window.EventTarget();
+    var event = new window.Event('myevent');
+    process.nextTick(function () {
+      et.dispatchEvent(event);
+    });
+    return once(et, 'myevent').then(function (args) {
+      var value = args[0];
+      assert.strictEqual(value, event);
+      assert.strictEqual(has(et.events, 'myevent'), false);
+    });
+  }
+
+  var onceWithBrowserEventTargetError = function onceWithBrowserEventTargetError() {
+    var et = new window.EventTarget();
+    var error = new window.Event('error');
+    process.nextTick(function () {
+      et.dispatchEvent(error);
+    });
+    return once(et, 'error').then(function (args) {
+      var err = args[0];
+      assert.strictEqual(err, error);
+      assert.strictEqual(has(et.events, 'error'), false);
+    });
+  }
+
+  common.test.comment('Testing with browser built-in EventTarget');
+  allTests.push([
+    onceWithBrowserEventTarget(),
+    onceWithBrowserEventTargetError()
+  ]);
+}
+
+module.exports = Promise.all(allTests);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/index.js b/libs/shared/graph-layout/node_modules/events/tests/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d739e670ca0287debfd9bcbc6164e862c74b133
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/index.js
@@ -0,0 +1,64 @@
+var test = require('tape');
+var functionsHaveNames = require('functions-have-names');
+var hasSymbols = require('has-symbols');
+
+require('./legacy-compat');
+var common = require('./common');
+
+// we do this to easily wrap each file in a mocha test
+// and also have browserify be able to statically analyze this file
+var orig_require = require;
+var require = function(file) {
+    test(file, function(t) {
+        // Store the tape object so tests can access it.
+        t.on('end', function () { delete common.test; });
+        common.test = t;
+
+        try {
+          var exp = orig_require(file);
+          if (exp && exp.then) {
+            exp.then(function () { t.end(); }, t.fail);
+            return;
+          }
+        } catch (err) {
+          t.fail(err);
+        }
+        t.end();
+    });
+};
+
+require('./add-listeners.js');
+require('./check-listener-leaks.js');
+require('./errors.js');
+require('./events-list.js');
+if (typeof Promise === 'function') {
+  require('./events-once.js');
+} else {
+  // Promise support is not available.
+  test('./events-once.js', { skip: true }, function () {});
+}
+require('./listener-count.js');
+require('./listeners-side-effects.js');
+require('./listeners.js');
+require('./max-listeners.js');
+if (functionsHaveNames()) {
+  require('./method-names.js');
+} else {
+  // Function.name is not supported in IE
+  test('./method-names.js', { skip: true }, function () {});
+}
+require('./modify-in-emit.js');
+require('./num-args.js');
+require('./once.js');
+require('./prepend.js');
+require('./set-max-listeners-side-effects.js');
+require('./special-event-names.js');
+require('./subclass.js');
+if (hasSymbols()) {
+  require('./symbols.js');
+} else {
+  // Symbol is not available.
+  test('./symbols.js', { skip: true }, function () {});
+}
+require('./remove-all-listeners.js');
+require('./remove-listeners.js');
diff --git a/libs/shared/graph-layout/node_modules/events/tests/legacy-compat.js b/libs/shared/graph-layout/node_modules/events/tests/legacy-compat.js
new file mode 100644
index 0000000000000000000000000000000000000000..a402be6e2f42d11acafd5dea42fabe0a21df06c6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/legacy-compat.js
@@ -0,0 +1,16 @@
+// sigh... life is hard
+if (!global.console) {
+    console = {}
+}
+
+var fns = ['log', 'error', 'trace'];
+for (var i=0 ; i<fns.length ; ++i) {
+    var fn = fns[i];
+    if (!console[fn]) {
+        console[fn] = function() {};
+    }
+}
+
+if (!Array.isArray) {
+    Array.isArray = require('isarray');
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/listener-count.js b/libs/shared/graph-layout/node_modules/events/tests/listener-count.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d422d872a4ddaa57b323604aa893d53269d32f1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/listener-count.js
@@ -0,0 +1,37 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var emitter = new EventEmitter();
+emitter.on('foo', function() {});
+emitter.on('foo', function() {});
+emitter.on('baz', function() {});
+// Allow any type
+emitter.on(123, function() {});
+
+assert.strictEqual(EventEmitter.listenerCount(emitter, 'foo'), 2);
+assert.strictEqual(emitter.listenerCount('foo'), 2);
+assert.strictEqual(emitter.listenerCount('bar'), 0);
+assert.strictEqual(emitter.listenerCount('baz'), 1);
+assert.strictEqual(emitter.listenerCount(123), 1);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/listeners-side-effects.js b/libs/shared/graph-layout/node_modules/events/tests/listeners-side-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..180f833128b0732e4942727f00be02fac2e06d5c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/listeners-side-effects.js
@@ -0,0 +1,56 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+
+var EventEmitter = require('../').EventEmitter;
+
+var e = new EventEmitter();
+var fl;  // foo listeners
+
+fl = e.listeners('foo');
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 0);
+if (Object.create) assert.ok(!(e._events instanceof Object));
+assert.strictEqual(Object.keys(e._events).length, 0);
+
+e.on('foo', assert.fail);
+fl = e.listeners('foo');
+assert.strictEqual(e._events.foo, assert.fail);
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 1);
+assert.strictEqual(fl[0], assert.fail);
+
+e.listeners('bar');
+
+e.on('foo', assert.ok);
+fl = e.listeners('foo');
+
+assert.ok(Array.isArray(e._events.foo));
+assert.strictEqual(e._events.foo.length, 2);
+assert.strictEqual(e._events.foo[0], assert.fail);
+assert.strictEqual(e._events.foo[1], assert.ok);
+
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 2);
+assert.strictEqual(fl[0], assert.fail);
+assert.strictEqual(fl[1], assert.ok);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/listeners.js b/libs/shared/graph-layout/node_modules/events/tests/listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..1909d2dfe2d1879c57ee2224749b491c9be495da
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/listeners.js
@@ -0,0 +1,168 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+var util = require('util');
+
+function listener() {}
+function listener2() {}
+function listener3() {
+  return 0;
+}
+function listener4() {
+  return 1;
+}
+
+function TestStream() {}
+util.inherits(TestStream, events.EventEmitter);
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var fooListeners = ee.listeners('foo');
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  ee.removeAllListeners('foo');
+  listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+
+  assert.ok(Array.isArray(fooListeners));
+  assert.strictEqual(fooListeners.length, 1);
+  assert.strictEqual(fooListeners[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+
+  var eeListenersCopy = ee.listeners('foo');
+  assert.ok(Array.isArray(eeListenersCopy));
+  assert.strictEqual(eeListenersCopy.length, 1);
+  assert.strictEqual(eeListenersCopy[0], listener);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  eeListenersCopy.push(listener2);
+  listeners = ee.listeners('foo');
+  
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  assert.strictEqual(eeListenersCopy.length, 2);
+  assert.strictEqual(eeListenersCopy[0], listener);
+  assert.strictEqual(eeListenersCopy[1], listener2);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var eeListenersCopy = ee.listeners('foo');
+  ee.on('foo', listener2);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener);
+  assert.strictEqual(listeners[1], listener2);
+
+  assert.ok(Array.isArray(eeListenersCopy));
+  assert.strictEqual(eeListenersCopy.length, 1);
+  assert.strictEqual(eeListenersCopy[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.once('foo', listener);
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  ee.once('foo', listener2);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener);
+  assert.strictEqual(listeners[1], listener2);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee._events = undefined;
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var s = new TestStream();
+  var listeners = s.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var wrappedListener = ee.rawListeners('foo');
+  assert.strictEqual(wrappedListener.length, 1);
+  assert.strictEqual(wrappedListener[0], listener);
+  assert.notStrictEqual(wrappedListener, ee.rawListeners('foo'));
+  ee.once('foo', listener);
+  var wrappedListeners = ee.rawListeners('foo');
+  assert.strictEqual(wrappedListeners.length, 2);
+  assert.strictEqual(wrappedListeners[0], listener);
+  assert.notStrictEqual(wrappedListeners[1], listener);
+  assert.strictEqual(wrappedListeners[1].listener, listener);
+  assert.notStrictEqual(wrappedListeners, ee.rawListeners('foo'));
+  ee.emit('foo');
+  assert.strictEqual(wrappedListeners.length, 2);
+  assert.strictEqual(wrappedListeners[1].listener, listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.once('foo', listener3);
+  ee.on('foo', listener4);
+  var rawListeners = ee.rawListeners('foo');
+  assert.strictEqual(rawListeners.length, 2);
+  assert.strictEqual(rawListeners[0](), 0);
+  var rawListener = ee.rawListeners('foo');
+  assert.strictEqual(rawListener.length, 1);
+  assert.strictEqual(rawListener[0](), 1);
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/max-listeners.js b/libs/shared/graph-layout/node_modules/events/tests/max-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b43953853252a2a1a3684899304e568f9259fb9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/max-listeners.js
@@ -0,0 +1,47 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+var e = new events.EventEmitter();
+
+var hasDefineProperty = !!Object.defineProperty;
+try { Object.defineProperty({}, 'x', { value: 0 }); } catch (err) { hasDefineProperty = false }
+
+e.on('maxListeners', common.mustCall());
+
+// Should not corrupt the 'maxListeners' queue.
+e.setMaxListeners(42);
+
+var throwsObjs = [NaN, -1, 'and even this'];
+var maxError = /^RangeError: The value of "n" is out of range\. It must be a non-negative number\./;
+var defError = /^RangeError: The value of "defaultMaxListeners" is out of range\. It must be a non-negative number\./;
+
+for (var i = 0; i < throwsObjs.length; i++) {
+  var obj = throwsObjs[i];
+  assert.throws(function() { e.setMaxListeners(obj); }, maxError);
+  if (hasDefineProperty) {
+    assert.throws(function() { events.defaultMaxListeners = obj; }, defError);
+  }
+}
+
+e.emit('maxListeners');
diff --git a/libs/shared/graph-layout/node_modules/events/tests/method-names.js b/libs/shared/graph-layout/node_modules/events/tests/method-names.js
new file mode 100644
index 0000000000000000000000000000000000000000..364a161fece00a8f525cd8d7f3a61539341a6d59
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/method-names.js
@@ -0,0 +1,35 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var E = events.EventEmitter.prototype;
+assert.strictEqual(E.constructor.name, 'EventEmitter');
+assert.strictEqual(E.on, E.addListener);  // Same method.
+assert.strictEqual(E.off, E.removeListener);  // Same method.
+Object.getOwnPropertyNames(E).forEach(function(name) {
+  if (name === 'constructor' || name === 'on' || name === 'off') return;
+  if (typeof E[name] !== 'function') return;
+  assert.strictEqual(E[name].name, name);
+});
diff --git a/libs/shared/graph-layout/node_modules/events/tests/modify-in-emit.js b/libs/shared/graph-layout/node_modules/events/tests/modify-in-emit.js
new file mode 100644
index 0000000000000000000000000000000000000000..53fa63395c620287e36ed5329443474d22daa241
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/modify-in-emit.js
@@ -0,0 +1,90 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var callbacks_called = [];
+
+var e = new events.EventEmitter();
+
+function callback1() {
+  callbacks_called.push('callback1');
+  e.on('foo', callback2);
+  e.on('foo', callback3);
+  e.removeListener('foo', callback1);
+}
+
+function callback2() {
+  callbacks_called.push('callback2');
+  e.removeListener('foo', callback2);
+}
+
+function callback3() {
+  callbacks_called.push('callback3');
+  e.removeListener('foo', callback3);
+}
+
+e.on('foo', callback1);
+assert.strictEqual(e.listeners('foo').length, 1);
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 2);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 1);
+assert.strictEqual(callbacks_called[0], 'callback1');
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 3);
+assert.strictEqual(callbacks_called[0], 'callback1');
+assert.strictEqual(callbacks_called[1], 'callback2');
+assert.strictEqual(callbacks_called[2], 'callback3');
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 3);
+assert.strictEqual(callbacks_called[0], 'callback1');
+assert.strictEqual(callbacks_called[1], 'callback2');
+assert.strictEqual(callbacks_called[2], 'callback3');
+
+e.on('foo', callback1);
+e.on('foo', callback2);
+assert.strictEqual(e.listeners('foo').length, 2);
+e.removeAllListeners('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+
+// Verify that removing callbacks while in emit allows emits to propagate to
+// all listeners
+callbacks_called = [];
+
+e.on('foo', callback2);
+e.on('foo', callback3);
+assert.strictEqual(2, e.listeners('foo').length);
+e.emit('foo');
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 2);
+assert.strictEqual(callbacks_called[0], 'callback2');
+assert.strictEqual(callbacks_called[1], 'callback3');
+assert.strictEqual(0, e.listeners('foo').length);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/num-args.js b/libs/shared/graph-layout/node_modules/events/tests/num-args.js
new file mode 100644
index 0000000000000000000000000000000000000000..c9b0deb9c960b3e1037424bf717986da0222bb73
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/num-args.js
@@ -0,0 +1,60 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var e = new events.EventEmitter();
+var num_args_emitted = [];
+
+e.on('numArgs', function() {
+  var numArgs = arguments.length;
+  num_args_emitted.push(numArgs);
+});
+
+e.on('foo', function() {
+  num_args_emitted.push(arguments.length);
+});
+
+e.on('foo', function() {
+  num_args_emitted.push(arguments.length);
+});
+
+e.emit('numArgs');
+e.emit('numArgs', null);
+e.emit('numArgs', null, null);
+e.emit('numArgs', null, null, null);
+e.emit('numArgs', null, null, null, null);
+e.emit('numArgs', null, null, null, null, null);
+
+e.emit('foo', null, null, null, null);
+
+assert.ok(Array.isArray(num_args_emitted));
+assert.strictEqual(num_args_emitted.length, 8);
+assert.strictEqual(num_args_emitted[0], 0);
+assert.strictEqual(num_args_emitted[1], 1);
+assert.strictEqual(num_args_emitted[2], 2);
+assert.strictEqual(num_args_emitted[3], 3);
+assert.strictEqual(num_args_emitted[4], 4);
+assert.strictEqual(num_args_emitted[5], 5);
+assert.strictEqual(num_args_emitted[6], 4);
+assert.strictEqual(num_args_emitted[6], 4);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/once.js b/libs/shared/graph-layout/node_modules/events/tests/once.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b36c055e4e31b94109f0d089f9eb2244e787f6b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/once.js
@@ -0,0 +1,83 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var e = new EventEmitter();
+
+e.once('hello', common.mustCall());
+
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+
+function remove() {
+  assert.fail('once->foo should not be emitted');
+}
+
+e.once('foo', remove);
+e.removeListener('foo', remove);
+e.emit('foo');
+
+e.once('e', common.mustCall(function() {
+  e.emit('e');
+}));
+
+e.once('e', common.mustCall());
+
+e.emit('e');
+
+// Verify that the listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.once('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function. Received type object$/);
+
+{
+  // once() has different code paths based on the number of arguments being
+  // emitted. Verify that all of the cases are covered.
+  var maxArgs = 4;
+
+  for (var i = 0; i <= maxArgs; ++i) {
+    var ee = new EventEmitter();
+    var args = ['foo'];
+
+    for (var j = 0; j < i; ++j)
+      args.push(j);
+
+    ee.once('foo', common.mustCall(function() {
+      var params = Array.prototype.slice.call(arguments);
+      var restArgs = args.slice(1);
+      assert.ok(Array.isArray(params));
+      assert.strictEqual(params.length, restArgs.length);
+      for (var index = 0; index < params.length; index++) {
+        var param = params[index];
+        assert.strictEqual(param, restArgs[index]);
+      }
+  	}));
+
+    EventEmitter.prototype.emit.apply(ee, args);
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/prepend.js b/libs/shared/graph-layout/node_modules/events/tests/prepend.js
new file mode 100644
index 0000000000000000000000000000000000000000..79afde0bf3971c638041df756918c25f239e8d79
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/prepend.js
@@ -0,0 +1,31 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var myEE = new EventEmitter();
+var m = 0;
+// This one comes last.
+myEE.on('foo', common.mustCall(function () {
+  assert.strictEqual(m, 2);
+}));
+
+// This one comes second.
+myEE.prependListener('foo', common.mustCall(function () {
+  assert.strictEqual(m++, 1);
+}));
+
+// This one comes first.
+myEE.prependOnceListener('foo',
+                         common.mustCall(function () {
+                           assert.strictEqual(m++, 0);
+                         }));
+
+myEE.emit('foo');
+
+// Verify that the listener must be a function
+assert.throws(function () {
+  var ee = new EventEmitter();
+  ee.prependOnceListener('foo', null);
+}, 'TypeError: The "listener" argument must be of type Function. Received type object');
diff --git a/libs/shared/graph-layout/node_modules/events/tests/remove-all-listeners.js b/libs/shared/graph-layout/node_modules/events/tests/remove-all-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..622941cfa604c0af52faabea35868f32ebf1237f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/remove-all-listeners.js
@@ -0,0 +1,133 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+var test = require('tape');
+
+function expect(expected) {
+  var actual = [];
+  test.onFinish(function() {
+    var sortedActual = actual.sort();
+    var sortedExpected = expected.sort();
+    assert.strictEqual(sortedActual.length, sortedExpected.length);
+    for (var index = 0; index < sortedActual.length; index++) {
+      var value = sortedActual[index];
+      assert.strictEqual(value, sortedExpected[index]);
+    }
+  });
+  function listener(name) {
+    actual.push(name);
+  }
+  return common.mustCall(listener, expected.length);
+}
+
+{
+  var ee = new events.EventEmitter();
+  var noop = common.mustNotCall();
+  ee.on('foo', noop);
+  ee.on('bar', noop);
+  ee.on('baz', noop);
+  ee.on('baz', noop);
+  var fooListeners = ee.listeners('foo');
+  var barListeners = ee.listeners('bar');
+  var bazListeners = ee.listeners('baz');
+  ee.on('removeListener', expect(['bar', 'baz', 'baz']));
+  ee.removeAllListeners('bar');
+  ee.removeAllListeners('baz');
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], noop);
+
+  listeners = ee.listeners('bar');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  listeners = ee.listeners('baz');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  // After calling removeAllListeners(),
+  // the old listeners array should stay unchanged.
+  assert.strictEqual(fooListeners.length, 1);
+  assert.strictEqual(fooListeners[0], noop);
+  assert.strictEqual(barListeners.length, 1);
+  assert.strictEqual(barListeners[0], noop);
+  assert.strictEqual(bazListeners.length, 2);
+  assert.strictEqual(bazListeners[0], noop);
+  assert.strictEqual(bazListeners[1], noop);
+  // After calling removeAllListeners(),
+  // new listeners arrays is different from the old.
+  assert.notStrictEqual(ee.listeners('bar'), barListeners);
+  assert.notStrictEqual(ee.listeners('baz'), bazListeners);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', common.mustNotCall());
+  ee.on('bar', common.mustNotCall());
+  // Expect LIFO order
+  ee.on('removeListener', expect(['foo', 'bar', 'removeListener']));
+  ee.on('removeListener', expect(['foo', 'bar']));
+  ee.removeAllListeners();
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  listeners = ee.listeners('bar');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('removeListener', common.mustNotCall());
+  // Check for regression where removeAllListeners() throws when
+  // there exists a 'removeListener' listener, but there exists
+  // no listeners for the provided event type.
+  assert.doesNotThrow(function () { ee.removeAllListeners(ee, 'foo') });
+}
+
+{
+  var ee = new events.EventEmitter();
+  var expectLength = 2;
+  ee.on('removeListener', function() {
+    assert.strictEqual(expectLength--, this.listeners('baz').length);
+  });
+  ee.on('baz', common.mustNotCall());
+  ee.on('baz', common.mustNotCall());
+  ee.on('baz', common.mustNotCall());
+  assert.strictEqual(ee.listeners('baz').length, expectLength + 1);
+  ee.removeAllListeners('baz');
+  assert.strictEqual(ee.listeners('baz').length, 0);
+}
+
+{
+  var ee = new events.EventEmitter();
+  assert.strictEqual(ee, ee.removeAllListeners());
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee._events = undefined;
+  assert.strictEqual(ee, ee.removeAllListeners());
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/remove-listeners.js b/libs/shared/graph-layout/node_modules/events/tests/remove-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..18e4d1651fa2546a810774fd59147aee25624154
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/remove-listeners.js
@@ -0,0 +1,212 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var listener1 = function listener1() {};
+var listener2 = function listener2() {};
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+  }));
+  ee.removeListener('hello', listener1);
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('removeListener', common.mustNotCall());
+  ee.removeListener('hello', listener2);
+
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener1);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('hello', listener2);
+
+  var listeners;
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 1);
+    assert.strictEqual(listeners[0], listener2);
+  }));
+  ee.removeListener('hello', listener1);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener2);
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener2);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  }));
+  ee.removeListener('hello', listener2);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+
+  function remove1() {
+    assert.fail('remove1 should not have been called');
+  }
+
+  function remove2() {
+    assert.fail('remove2 should not have been called');
+  }
+
+  ee.on('removeListener', common.mustCall(function(name, cb) {
+    if (cb !== remove1) return;
+    this.removeListener('quux', remove2);
+    this.emit('quux');
+  }, 2));
+  ee.on('quux', remove1);
+  ee.on('quux', remove2);
+  ee.removeListener('quux', remove1);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('hello', listener2);
+
+  var listeners;
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 1);
+    assert.strictEqual(listeners[0], listener2);
+    ee.once('removeListener', common.mustCall(function(name, cb) {
+      assert.strictEqual(name, 'hello');
+      assert.strictEqual(cb, listener2);
+      listeners = ee.listeners('hello');
+      assert.ok(Array.isArray(listeners));
+      assert.strictEqual(listeners.length, 0);
+    }));
+    ee.removeListener('hello', listener2);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  }));
+  ee.removeListener('hello', listener1);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+  var listener3 = common.mustCall(function() {
+    ee.removeListener('hello', listener4);
+  }, 2);
+  var listener4 = common.mustCall();
+
+  ee.on('hello', listener3);
+  ee.on('hello', listener4);
+
+  // listener4 will still be called although it is removed by listener 3.
+  ee.emit('hello');
+  // This is so because the interal listener array at time of emit
+  // was [listener3,listener4]
+
+  // Interal listener array [listener3]
+  ee.emit('hello');
+}
+
+{
+  var ee = new EventEmitter();
+
+  ee.once('hello', listener1);
+  ee.on('removeListener', common.mustCall(function(eventName, listener) {
+    assert.strictEqual(eventName, 'hello');
+    assert.strictEqual(listener, listener1);
+  }));
+  ee.emit('hello');
+}
+
+{
+  var ee = new EventEmitter();
+
+  assert.strictEqual(ee, ee.removeListener('foo', function() {}));
+}
+
+// Verify that the removed listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.removeListener('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function\. Received type object$/);
+
+{
+  var ee = new EventEmitter();
+  var listener = function() {};
+  ee._events = undefined;
+  var e = ee.removeListener('foo', listener);
+  assert.strictEqual(e, ee);
+}
+
+{
+  var ee = new EventEmitter();
+
+  ee.on('foo', listener1);
+  ee.on('foo', listener2);
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener1);
+  assert.strictEqual(listeners[1], listener2);
+
+  ee.removeListener('foo', listener1);
+  assert.strictEqual(ee._events.foo, listener2);
+
+  ee.on('foo', listener1);
+  listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener2);
+  assert.strictEqual(listeners[1], listener1);
+
+  ee.removeListener('foo', listener1);
+  assert.strictEqual(ee._events.foo, listener2);
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/set-max-listeners-side-effects.js b/libs/shared/graph-layout/node_modules/events/tests/set-max-listeners-side-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..13dbb671e9024280b2e9be42206a23b2db355074
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/set-max-listeners-side-effects.js
@@ -0,0 +1,31 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var e = new events.EventEmitter();
+
+if (Object.create) assert.ok(!(e._events instanceof Object));
+assert.strictEqual(Object.keys(e._events).length, 0);
+e.setMaxListeners(5);
+assert.strictEqual(Object.keys(e._events).length, 0);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/special-event-names.js b/libs/shared/graph-layout/node_modules/events/tests/special-event-names.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2f0b744a706c9ae6181a9723b2b92492df7e4d1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/special-event-names.js
@@ -0,0 +1,45 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var ee = new EventEmitter();
+var handler = function() {};
+
+assert.strictEqual(ee.eventNames().length, 0);
+
+assert.strictEqual(ee._events.hasOwnProperty, undefined);
+assert.strictEqual(ee._events.toString, undefined);
+
+ee.on('__defineGetter__', handler);
+ee.on('toString', handler);
+ee.on('__proto__', handler);
+
+assert.strictEqual(ee.eventNames()[0], '__defineGetter__');
+assert.strictEqual(ee.eventNames()[1], 'toString');
+
+assert.strictEqual(ee.listeners('__defineGetter__').length, 1);
+assert.strictEqual(ee.listeners('__defineGetter__')[0], handler);
+assert.strictEqual(ee.listeners('toString').length, 1);
+assert.strictEqual(ee.listeners('toString')[0], handler);
+
+// Only run __proto__ tests if that property can actually be set
+if ({ __proto__: 'ok' }.__proto__ === 'ok') {
+  assert.strictEqual(ee.eventNames().length, 3);
+  assert.strictEqual(ee.eventNames()[2], '__proto__');
+  assert.strictEqual(ee.listeners('__proto__').length, 1);
+  assert.strictEqual(ee.listeners('__proto__')[0], handler);
+
+  ee.on('__proto__', common.mustCall(function(val) {
+    assert.strictEqual(val, 1);
+  }));
+  ee.emit('__proto__', 1);
+
+  process.on('__proto__', common.mustCall(function(val) {
+    assert.strictEqual(val, 1);
+  }));
+  process.emit('__proto__', 1);
+} else {
+  console.log('# skipped __proto__')
+}
diff --git a/libs/shared/graph-layout/node_modules/events/tests/subclass.js b/libs/shared/graph-layout/node_modules/events/tests/subclass.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd033fff4d26696cddcb6a87f25da89d67a04e59
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/subclass.js
@@ -0,0 +1,66 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var test = require('tape');
+var assert = require('assert');
+var EventEmitter = require('../').EventEmitter;
+var util = require('util');
+
+util.inherits(MyEE, EventEmitter);
+
+function MyEE(cb) {
+  this.once(1, cb);
+  this.emit(1);
+  this.removeAllListeners();
+  EventEmitter.call(this);
+}
+
+var myee = new MyEE(common.mustCall());
+
+
+util.inherits(ErrorEE, EventEmitter);
+function ErrorEE() {
+  this.emit('error', new Error('blerg'));
+}
+
+assert.throws(function() {
+  new ErrorEE();
+}, /blerg/);
+
+test.onFinish(function() {
+  assert.ok(!(myee._events instanceof Object));
+  assert.strictEqual(Object.keys(myee._events).length, 0);
+});
+
+
+function MyEE2() {
+  EventEmitter.call(this);
+}
+
+MyEE2.prototype = new EventEmitter();
+
+var ee1 = new MyEE2();
+var ee2 = new MyEE2();
+
+ee1.on('x', function() {});
+
+assert.strictEqual(ee2.listenerCount('x'), 0);
diff --git a/libs/shared/graph-layout/node_modules/events/tests/symbols.js b/libs/shared/graph-layout/node_modules/events/tests/symbols.js
new file mode 100644
index 0000000000000000000000000000000000000000..0721f0ec0b5d6ef520f30eb3c711551eaabd2204
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/events/tests/symbols.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var ee = new EventEmitter();
+var foo = Symbol('foo');
+var listener = common.mustCall();
+
+ee.on(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 1);
+assert.strictEqual(ee.listeners(foo)[0], listener);
+
+ee.emit(foo);
+
+ee.removeAllListeners();
+assert.strictEqual(ee.listeners(foo).length, 0);
+
+ee.on(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 1);
+assert.strictEqual(ee.listeners(foo)[0], listener);
+
+ee.removeListener(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 0);
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-generators/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/README.md b/libs/shared/graph-layout/node_modules/graphology-generators/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..cab65da88e7a735568b1e62c67bcfc1bb2f85f72
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/README.md
@@ -0,0 +1,299 @@
+# Graphology Generators
+
+Various graph generators to be used with [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-generators
+```
+
+## Usage
+
+- [Classic graphs](#classic-graphs)
+  - [Complete](#complete)
+  - [Empty](#empty)
+  - [Ladder](#ladder)
+  - [Path](#path)
+- [Community graphs](#community-graphs)
+  - [Caveman](#caveman)
+  - [Connected Caveman](#connected-caveman)
+- [Random graphs](#random-graphs)
+  - [Clusters](#clusters)
+  - [Erdos-Renyi](#erdos-renyi)
+  - [Girvan-Newman](#girvan-newman)
+- [Small graphs](#small-graphs)
+  - [Krackhardt Kite](#krackhardt-kite)
+- [Social graphs](#social-graphs)
+  - [Florentine Families](#florentine-families)
+  - [Karate Club](#karate-club)
+
+### Classic graphs
+
+#### Complete
+
+Creates a [complete](https://en.wikipedia.org/wiki/Complete_graph) graph.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {complete} from 'graphology-generators/classic';
+// Alternatively, if you only want to load relevant code
+import complete from 'graphology-generators/classic/complete';
+
+// Creating a complete graph
+const graph = complete(Graph, 10);
+
+// Using another constuctor to create, say, a complete undirected graph
+const graph = complete(UndirectedGraph, 10);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **order** _number_: number of nodes in the generated graph.
+
+#### Empty
+
+Creates an empty graph with the desired number of nodes and no edges.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {empty} from 'graphology-generators/classic';
+// Alternatively, if you only want to load relevant code
+import empty from 'graphology-generators/classic/empty';
+
+// Creating an empty graph
+const graph = empty(Graph, 10);
+
+// Using another constuctor to create, say, an empty undirected graph
+const graph = empty(UndirectedGraph, 10);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **order** _number_: number of nodes in the generated graph.
+
+#### Ladder
+
+Creates a ladder graph with the desired length. Note that the generated graph will logically have twice the number of nodes.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {ladder} from 'graphology-generators/classic';
+// Alternatively, if you only want to load relevant code
+import ladder from 'graphology-generators/classic/ladder';
+
+// Creating a ladder graph
+const graph = ladder(Graph, 10);
+
+// Using another constuctor to create, say, a undirected ladder graph
+const graph = ladder(UndirectedGraph, 10);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **length** _number_: length of the ladder.
+
+#### Path
+
+Creates a path graph.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {path} from 'graphology-generators/classic';
+// Alternatively, if you only want to load relevant code
+import path from 'graphology-generators/classic/path';
+
+// Creating a path graph
+const graph = path(Graph, 10);
+
+// Using another constuctor to create, say, a path undirected graph
+const graph = path(UndirectedGraph, 10);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **order** _number_: number of nodes in the generated graph.
+
+### Community graphs
+
+#### Caveman
+
+Creates a Caveman graph containing `l` components of `k` nodes.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {caveman} from 'graphology-generators/community';
+// Alternatively, if you only want to load relevant code
+import caveman from 'graphology-generators/community/caveman';
+
+// Creating a caveman graph
+const graph = caveman(Graph, 6, 8);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **l** _number_: number of components in the graph.
+- **k** _number_: number of nodes of the components.
+
+#### Connected Caveman
+
+Creates a Connected Caveman graph containing `l` components of `k` nodes.
+
+```js
+import Graph, {UndirectedGraph} from 'graphology';
+import {connectedCaveman} from 'graphology-generators/community';
+// Alternatively, if you only want to load relevant code
+import connectedCaveman from 'graphology-generators/community/connected-caveman';
+
+// Creating a connected caveman graph
+const graph = connectedCaveman(Graph, 6, 8);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **l** _number_: number of components in the graph.
+- **k** _number_: number of nodes of the components.
+
+### Random graphs
+
+#### Clusters
+
+Creates a graph with the desired number of nodes & edges and having a given number of clusters.
+
+```js
+import Graph from 'graphology';
+import {clusters} from 'graphology-generators/random';
+// Alternatively, if you only want to load relevant code
+import clusters from 'graphology-generators/random/clusters';
+
+// Creating a random clustered graph
+const graph = clusters(Graph, {
+  order: 100,
+  size: 1000,
+  clusters: 5
+});
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **options** _object_: options:
+  - **order** _number_: number of nodes of the generated graph.
+  - **size** _number_: number of edges of the generated graph.
+  - **clusters** _number_: number of clusters of the generated graph.
+  - **clusterDensity** _?number_ [`0.5`]: Probability that an edge will link two nodes of the same cluster.
+  - **rng** _?function_: custom RNG function.
+
+#### Erdos-Renyi
+
+Creates an [Erdos-Renyi](https://en.wikipedia.org/wiki/Erd%C5%91s%E2%80%93R%C3%A9nyi_model), or binomial graph.
+
+```js
+import Graph from 'graphology';
+import {erdosRenyi} from 'graphology-generators/random';
+// Alternatively, if you only want to load relevant code
+import erdosRenyi from 'graphology-generators/random/erdos-renyi';
+
+// Creating a binomial graph
+const graph = erdosRenyi(Graph, {order: 10, probability: 0.5});
+
+// If your graph is sparse (low probability), you can use the `sparse` version
+// which runs in O(m + n) rather than O(n^2)
+const graph = erdosRenyi.sparse(Graph, {order: 1000, probability: 0.1});
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **options** _object_: options:
+  - **order** _number_: number of nodes of the generated graph.
+  - **probability** _number_: probability for edge creation. (i.e. density you try to approximate in the generated graph).
+  - **approximateSize**: alternatively, you can pass an approximate number of edges you are trying to get in the generated graph.
+  - **rng** _?function_: custom RNG function.
+
+#### Girvan-Newman
+
+Creates a [Girvan-Newman](http://www.pnas.org/content/99/12/7821.full.pdf) random graph as described in:
+
+> Community Structure in social and biological networks. Girvan Newman, 2002. PNAS June, vol 99 n 12
+
+```js
+import Graph from 'graphology';
+import {girvanNewman} from 'graphology-generators/random';
+// Alternatively, if you only want to load relevant code
+import girvanNewman from 'graphology-generators/random/girvan-newman';
+
+// Creating a binomial graph
+const graph = girvanNewman(Graph, {zOut: 4});
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+- **options** _object_: options:
+  - **zOut** _number_: _zout_ parameter.
+  - **rng** _?function_: custom RNG function.
+
+### Small graphs
+
+#### Krackhardt kite
+
+Returns the [Krackhardt kite](https://en.wikipedia.org/wiki/Krackhardt_kite_graph) graph.
+
+```js
+import Graph from 'graphology';
+import {krackhardtKite} from 'graphology-generators/small';
+// Alternatively, if you only want to load relevant code
+import krackhardtKite from 'graphology-generators/small/krackhardt-kite';
+
+// Creating a random clustered graph
+const graph = krackhardtKite(Graph);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+
+### Social graphs
+
+#### Florentine Families
+
+Returns the Florentine families' graph.
+
+```js
+import Graph from 'graphology';
+import {florentineFamilies} from 'graphology-generators/florentine-families';
+// Alternatively, if you only want to load relevant code
+import florentineFamilies from 'graphology-generators/social/florentine-families';
+
+// Generating the graph
+const graph = florentineFamilies(Graph);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
+
+#### Karate Club
+
+Returns [Zachary's karate club](https://en.wikipedia.org/wiki/Zachary%27s_karate_club) graph.
+
+```js
+import Graph from 'graphology';
+import {karateClub} from 'graphology-generators/karate-club';
+// Alternatively, if you only want to load relevant code
+import karateClub from 'graphology-generators/social/karate-club';
+
+// Generating the graph
+const graph = karateClub(Graph);
+```
+
+**Arguments**
+
+- **constructor** _Class_: a `graphology` constructor.
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..62db57086e6ce1f902047be4f487e366ba6e0810
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.d.ts
@@ -0,0 +1,10 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function complete<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  order: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.js b/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.js
new file mode 100644
index 0000000000000000000000000000000000000000..38e712308929201d698513773bfe86ff21c9b10a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/complete.js
@@ -0,0 +1,40 @@
+/**
+ * Graphology Complete Graph Generator
+ * ====================================
+ *
+ * Function generating complete graphs.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates a complete graph with n nodes.
+ *
+ * @param  {Class}  GraphClass - The Graph Class to instantiate.
+ * @param  {number} order      - Number of nodes of the graph.
+ * @return {Graph}
+ */
+module.exports = function complete(GraphClass, order) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/classic/complete: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass();
+
+  var i, j;
+
+  for (i = 0; i < order; i++) graph.addNode(i);
+
+  for (i = 0; i < order; i++) {
+    for (j = i + 1; j < order; j++) {
+      if (graph.type !== 'directed') graph.addUndirectedEdge(i, j);
+
+      if (graph.type !== 'undirected') {
+        graph.addDirectedEdge(i, j);
+        graph.addDirectedEdge(j, i);
+      }
+    }
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8676ca9a2d9ded110b7f47dfd7c6312e2292abfc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.d.ts
@@ -0,0 +1,10 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function empty<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  order: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.js b/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.js
new file mode 100644
index 0000000000000000000000000000000000000000..56c0c225dcf93a8761ce207ed6e12e25ef672173
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/empty.js
@@ -0,0 +1,29 @@
+/**
+ * Graphology Empty Graph Generator
+ * =================================
+ *
+ * Function generating empty graphs.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates an empty graph with n nodes and 0 edges.
+ *
+ * @param  {Class}  GraphClass - The Graph Class to instantiate.
+ * @param  {number} order      - Number of nodes of the graph.
+ * @return {Graph}
+ */
+module.exports = function empty(GraphClass, order) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/classic/empty: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass();
+
+  var i;
+
+  for (i = 0; i < order; i++) graph.addNode(i);
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..154bd5563dbf478cd0411f3f560da4f858c03e8a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.d.ts
@@ -0,0 +1,4 @@
+export {default as complete} from './complete';
+export {default as empty} from './empty';
+export {default as ladder} from './ladder';
+export {default as path} from './path';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3bf8b2927b0fefcdbcfe084a7074d9e436fb99dd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/index.js
@@ -0,0 +1,10 @@
+/**
+ * Graphology Classic Graph Generators
+ * ====================================
+ *
+ * Classic graph generators endpoint.
+ */
+exports.complete = require('./complete.js');
+exports.empty = require('./empty.js');
+exports.ladder = require('./ladder.js');
+exports.path = require('./path.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06eab74a7f3e524a901ca62f2daef9826c95d05d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.d.ts
@@ -0,0 +1,10 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function ladder<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  length: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.js b/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.js
new file mode 100644
index 0000000000000000000000000000000000000000..3949bf93cc8c9b2a934c86c62febff3247951b5e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/ladder.js
@@ -0,0 +1,31 @@
+/**
+ * Graphology Ladder Graph Generator
+ * ==================================
+ *
+ * Function generating ladder graphs.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates a ladder graph of length n (order will therefore be 2 * n).
+ *
+ * @param  {Class}  GraphClass - The Graph Class to instantiate.
+ * @param  {number} length     - Length of the ladder.
+ * @return {Graph}
+ */
+module.exports = function ladder(GraphClass, length) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/classic/ladder: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass();
+
+  var i;
+
+  for (i = 0; i < length - 1; i++) graph.mergeEdge(i, i + 1);
+  for (i = length; i < length * 2 - 1; i++) graph.mergeEdge(i, i + 1);
+  for (i = 0; i < length; i++) graph.addEdge(i, i + length);
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..25c46911b253e4ca4076f014fee579d9a67414a1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.d.ts
@@ -0,0 +1,10 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function path<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  order: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.js b/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.js
new file mode 100644
index 0000000000000000000000000000000000000000..6538ceb3d777c0cba194499a5356f20c18e223d7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/classic/path.js
@@ -0,0 +1,27 @@
+/**
+ * Graphology Path Graph Generator
+ * ================================
+ *
+ * Function generating path graphs.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates a path graph with n nodes.
+ *
+ * @param  {Class}  GraphClass - The Graph Class to instantiate.
+ * @param  {number} order      - Number of nodes of the graph.
+ * @return {Graph}
+ */
+module.exports = function path(GraphClass, order) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/classic/path: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass();
+
+  for (var i = 0; i < order - 1; i++) graph.mergeEdge(i, i + 1);
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..104d12ba35c6f713c27c63acd19b621734823765
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.d.ts
@@ -0,0 +1,11 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function caveman<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  l: number,
+  k: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.js b/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.js
new file mode 100644
index 0000000000000000000000000000000000000000..d88638f880764869f8ccb3431acd57ce282fa710
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/caveman.js
@@ -0,0 +1,43 @@
+/**
+ * Graphology Caveman Graph Generator
+ * ===================================
+ *
+ * Function generating caveman graphs.
+ *
+ * [Article]:
+ * Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.'
+ * Amer. J. Soc. 105, 493-527, 1999.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor'),
+  empty = require('../classic/empty.js');
+
+/**
+ * Function returning a caveman graph with desired properties.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {number}   l             - The number of cliques in the graph.
+ * @param  {number}   k             - Size of the cliques.
+ * @return {Graph}
+ */
+module.exports = function caveman(GraphClass, l, k) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/community/caveman: invalid Graph constructor.'
+    );
+
+  var m = l * k;
+
+  var graph = empty(GraphClass, m);
+
+  if (k < 2) return graph;
+
+  var i, j, s;
+
+  for (i = 0; i < m; i += k) {
+    for (j = i; j < i + k; j++) {
+      for (s = j + 1; s < i + k; s++) graph.addEdge(j, s);
+    }
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..715c1f3c984b713c786699dce91426886aaa219c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.d.ts
@@ -0,0 +1,11 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function connectedCaveman<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  l: number,
+  k: number
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.js b/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.js
new file mode 100644
index 0000000000000000000000000000000000000000..b29b93a388b11bde762bfc26811f3dbc7d6dde5e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/connected-caveman.js
@@ -0,0 +1,49 @@
+/**
+ * Graphology Connected Caveman Graph Generator
+ * =============================================
+ *
+ * Function generating connected caveman graphs.
+ *
+ * [Article]:
+ * Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.'
+ * Amer. J. Soc. 105, 493-527, 1999.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor'),
+  empty = require('../classic/empty.js');
+
+/**
+ * Function returning a connected caveman graph with desired properties.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {number}   l             - The number of cliques in the graph.
+ * @param  {number}   k             - Size of the cliques.
+ * @return {Graph}
+ */
+module.exports = function connectedCaveman(GraphClass, l, k) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/community/connected-caveman: invalid Graph constructor.'
+    );
+
+  var m = l * k;
+
+  var graph = empty(GraphClass, m);
+
+  if (k < 2) return graph;
+
+  var i, j, s;
+
+  for (i = 0; i < m; i += k) {
+    for (j = i; j < i + k; j++) {
+      for (s = j + 1; s < i + k; s++) {
+        if (j !== i || j !== s - 1) graph.addEdge(j, s);
+      }
+    }
+
+    if (i > 0) graph.addEdge(i, (i - 1) % m);
+  }
+
+  graph.addEdge(0, m - 1);
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/community/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..794f0db9e97432e646901bd8b43deeac6cfe1b65
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/index.d.ts
@@ -0,0 +1,2 @@
+export {default as caveman} from './caveman';
+export {default as connectedCaveman} from './connected-caveman';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/community/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/community/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..759a9c0f69160853b2655d8d8ec71a566cc5821e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/community/index.js
@@ -0,0 +1,8 @@
+/**
+ * Graphology Community Graph Generators
+ * ======================================
+ *
+ * Community graph generators endpoint.
+ */
+exports.caveman = require('./caveman.js');
+exports.connectedCaveman = require('./connected-caveman.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae4702859589f7e4343d40d27d2ed7a838869118
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/index.d.ts
@@ -0,0 +1,5 @@
+export * from './classic';
+export * from './community';
+export * from './random';
+export * from './small';
+export * from './social';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..3375a304beaa472bb6679969ab6c81bd0ac1f020
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/index.js
@@ -0,0 +1,11 @@
+/**
+ * Graphology Graph Generators
+ * ============================
+ *
+ * Library endpoint.
+ */
+exports.classic = require('./classic');
+exports.community = require('./community');
+exports.random = require('./random');
+exports.small = require('./small');
+exports.social = require('./social');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/package.json b/libs/shared/graph-layout/node_modules/graphology-generators/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..f0d7d70c784709205bdc9073b331926a53f96ecf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/package.json
@@ -0,0 +1,49 @@
+{
+  "name": "graphology-generators",
+  "version": "0.11.2",
+  "description": "Various graph generators for graphology.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "files": [
+    "*.d.ts",
+    "index.js",
+    "classic",
+    "community",
+    "random",
+    "small",
+    "social"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha test.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "generators",
+    "erdos renyi",
+    "karate club",
+    "krackhardt kite",
+    "girvan newman"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.19.0"
+  },
+  "dependencies": {
+    "graphology-metrics": "^2.0.0",
+    "graphology-utils": "^2.3.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5e3e85bfd5a8bf5157c5e7e32b979ba90e657df6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.d.ts
@@ -0,0 +1,18 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export type ClustersGeneratorOptions = {
+  clusterDensity?: number;
+  order: number;
+  size: number;
+  clusters: number;
+  rng?: () => number;
+};
+
+export default function clusters<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  options: ClustersGeneratorOptions
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.js b/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.js
new file mode 100644
index 0000000000000000000000000000000000000000..c77aa3a7146ac27aed68003f73b2b9c49cd321b6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/clusters.js
@@ -0,0 +1,129 @@
+/**
+ * Graphology Random Clusters Graph Generator
+ * ===========================================
+ *
+ * Function generating a graph containing the desired number of nodes & edges
+ * and organized in the desired number of clusters.
+ *
+ * [Author]:
+ * Alexis Jacomy
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates a random graph with clusters.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {object}   options       - Options:
+ * @param  {number}     clusterDensity - Probability that an edge will link two
+ *                                       nodes of the same cluster.
+ * @param  {number}     order          - Number of nodes.
+ * @param  {number}     size           - Number of edges.
+ * @param  {number}     clusters       - Number of clusters.
+ * @param  {function}   rng            - Custom RNG function.
+ * @return {Graph}
+ */
+module.exports = function (GraphClass, options) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/random/clusters: invalid Graph constructor.'
+    );
+
+  options = options || {};
+
+  var clusterDensity =
+      'clusterDensity' in options ? options.clusterDensity : 0.5,
+    rng = options.rng || Math.random,
+    N = options.order,
+    E = options.size,
+    C = options.clusters;
+
+  if (
+    typeof clusterDensity !== 'number' ||
+    clusterDensity > 1 ||
+    clusterDensity < 0
+  )
+    throw new Error(
+      'graphology-generators/random/clusters: `clusterDensity` option should be a number between 0 and 1.'
+    );
+
+  if (typeof rng !== 'function')
+    throw new Error(
+      'graphology-generators/random/clusters: `rng` option should be a function.'
+    );
+
+  if (typeof N !== 'number' || N <= 0)
+    throw new Error(
+      'graphology-generators/random/clusters: `order` option should be a positive number.'
+    );
+
+  if (typeof E !== 'number' || E <= 0)
+    throw new Error(
+      'graphology-generators/random/clusters: `size` option should be a positive number.'
+    );
+
+  if (typeof C !== 'number' || C <= 0)
+    throw new Error(
+      'graphology-generators/random/clusters: `clusters` option should be a positive number.'
+    );
+
+  // Creating graph
+  var graph = new GraphClass();
+
+  // Adding nodes
+  if (!N) return graph;
+
+  // Initializing clusters
+  var clusters = new Array(C),
+    cluster,
+    nodes,
+    i;
+
+  for (i = 0; i < C; i++) clusters[i] = [];
+
+  for (i = 0; i < N; i++) {
+    cluster = (rng() * C) | 0;
+    graph.addNode(i, {cluster: cluster});
+    clusters[cluster].push(i);
+  }
+
+  // Adding edges
+  if (!E) return graph;
+
+  var source, target, l;
+
+  for (i = 0; i < E; i++) {
+    // Adding a link between two random nodes
+    if (rng() < 1 - clusterDensity) {
+      source = (rng() * N) | 0;
+
+      do {
+        target = (rng() * N) | 0;
+      } while (source === target);
+    }
+
+    // Adding a link between two nodes from the same cluster
+    else {
+      cluster = (rng() * C) | 0;
+      nodes = clusters[cluster];
+      l = nodes.length;
+
+      if (!l || l < 2) {
+        // TODO: in those case we may have fewer edges than required
+        // TODO: check where E is over full clusterDensity
+        continue;
+      }
+
+      source = nodes[(rng() * l) | 0];
+
+      do {
+        target = nodes[(rng() * l) | 0];
+      } while (source === target);
+    }
+
+    if (!graph.multi) graph.mergeEdge(source, target);
+    else graph.addEdge(source, target);
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4f1baaed91b75a6d25d2ff82553ccbe2fe221119
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.d.ts
@@ -0,0 +1,30 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export type ErdosRenyiGeneratorOptions = {
+  order: number;
+  probability?: number;
+  approximateSize?: number;
+  rng?: () => number;
+};
+
+declare const erdosRenyi: {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes,
+    GraphAttributes extends Attributes = Attributes
+  >(
+    Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+    options: ErdosRenyiGeneratorOptions
+  ): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
+
+  sparse<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes,
+    GraphAttributes extends Attributes = Attributes
+  >(
+    Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+    options: ErdosRenyiGeneratorOptions
+  ): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
+};
+
+export default erdosRenyi;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.js b/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.js
new file mode 100644
index 0000000000000000000000000000000000000000..c75e3fca8a6ea79aeefd94d4617294eaa926b508
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/erdos-renyi.js
@@ -0,0 +1,177 @@
+/**
+ * Graphology Erdos-Renyi Graph Generator
+ * =======================================
+ *
+ * Function generating binomial graphs.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+var density = require('graphology-metrics/graph/density').abstractDensity;
+
+/**
+ * Generates a binomial graph graph with n nodes.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {object}   options       - Options:
+ * @param  {number}     order       - Number of nodes in the graph.
+ * @param  {number}     probability - Probability for edge creation.
+ * @param  {function}   rng         - Custom RNG function.
+ * @return {Graph}
+ */
+function erdosRenyi(GraphClass, options) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid Graph constructor.'
+    );
+
+  var order = options.order;
+  var probability = options.probability;
+  var rng = options.rng || Math.random;
+
+  var graph = new GraphClass();
+
+  // If user gave a size, we need to compute probability
+  if (typeof options.approximateSize === 'number') {
+    probability = density(graph.type, false, order, options.approximateSize);
+  }
+
+  if (typeof order !== 'number' || order <= 0)
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid `order`. Should be a positive number.'
+    );
+
+  if (typeof probability !== 'number' || probability < 0 || probability > 1)
+    throw new Error(
+      "graphology-generators/random/erdos-renyi: invalid `probability`. Should be a number between 0 and 1. Or maybe you gave an `approximateSize` exceeding the graph's density."
+    );
+
+  if (typeof rng !== 'function')
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid `rng`. Should be a function.'
+    );
+
+  var i, j;
+
+  for (i = 0; i < order; i++) graph.addNode(i);
+
+  if (probability <= 0) return graph;
+
+  for (i = 0; i < order; i++) {
+    for (j = i + 1; j < order; j++) {
+      if (graph.type !== 'directed') {
+        if (rng() < probability) graph.addUndirectedEdge(i, j);
+      }
+
+      if (graph.type !== 'undirected') {
+        if (rng() < probability) graph.addDirectedEdge(i, j);
+
+        if (rng() < probability) graph.addDirectedEdge(j, i);
+      }
+    }
+  }
+
+  return graph;
+}
+
+/**
+ * Generates a binomial graph graph with n nodes using a faster algorithm
+ * for sparse graphs.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {object}   options       - Options:
+ * @param  {number}     order       - Number of nodes in the graph.
+ * @param  {number}     probability - Probability for edge creation.
+ * @param  {function}   rng         - Custom RNG function.
+ * @return {Graph}
+ */
+function erdosRenyiSparse(GraphClass, options) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid Graph constructor.'
+    );
+
+  var order = options.order;
+  var probability = options.probability;
+  var rng = options.rng || Math.random;
+
+  var graph = new GraphClass();
+
+  // If user gave a size, we need to compute probability
+  if (typeof options.approximateSize === 'number') {
+    probability = density(graph.type, false, order, options.approximateSize);
+  }
+
+  if (typeof order !== 'number' || order <= 0)
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid `order`. Should be a positive number.'
+    );
+
+  if (typeof probability !== 'number' || probability < 0 || probability > 1)
+    throw new Error(
+      "graphology-generators/random/erdos-renyi: invalid `probability`. Should be a number between 0 and 1. Or maybe you gave an `approximateSize` exceeding the graph's density."
+    );
+
+  if (typeof rng !== 'function')
+    throw new Error(
+      'graphology-generators/random/erdos-renyi: invalid `rng`. Should be a function.'
+    );
+
+  for (var i = 0; i < order; i++) graph.addNode(i);
+
+  if (probability <= 0) return graph;
+
+  var w = -1,
+    lp = Math.log(1 - probability),
+    lr,
+    v;
+
+  if (graph.type !== 'undirected') {
+    v = 0;
+
+    while (v < order) {
+      lr = Math.log(1 - rng());
+      w += 1 + ((lr / lp) | 0);
+
+      // Avoiding self loops
+      if (v === w) {
+        w++;
+      }
+
+      while (v < order && order <= w) {
+        w -= order;
+        v++;
+
+        // Avoiding self loops
+        if (v === w) w++;
+      }
+
+      if (v < order) graph.addDirectedEdge(v, w);
+    }
+  }
+
+  w = -1;
+
+  if (graph.type !== 'directed') {
+    v = 1;
+
+    while (v < order) {
+      lr = Math.log(1 - rng());
+
+      w += 1 + ((lr / lp) | 0);
+
+      while (w >= v && v < order) {
+        w -= v;
+        v++;
+      }
+
+      if (v < order) graph.addUndirectedEdge(v, w);
+    }
+  }
+
+  return graph;
+}
+
+/**
+ * Exporting.
+ */
+erdosRenyi.sparse = erdosRenyiSparse;
+module.exports = erdosRenyi;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf382f75c1ffae192c976deb78a0f2437d3db90c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.d.ts
@@ -0,0 +1,15 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export type GirvanNewmanGeneratorOptions = {
+  zOut: number;
+  rng?: () => number;
+};
+
+export default function girvanNewman<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>,
+  options: GirvanNewmanGeneratorOptions
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.js b/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.js
new file mode 100644
index 0000000000000000000000000000000000000000..a26eb4aee757177897c7074d2027f459c9e050b1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/girvan-newman.js
@@ -0,0 +1,67 @@
+/**
+ * Graphology Girvan-Newman Graph Generator
+ * =========================================
+ *
+ * Function generating graphs liks the one used to test the Girvan-Newman
+ * community algorithm.
+ *
+ * [Reference]:
+ * http://www.pnas.org/content/99/12/7821.full.pdf
+ *
+ * [Article]:
+ * Community Structure in  social and biological networks.
+ * Girvan Newman, 2002. PNAS June, vol 99 n 12
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Generates a binomial graph graph with n nodes.
+ *
+ * @param  {Class}    GraphClass    - The Graph Class to instantiate.
+ * @param  {object}   options       - Options:
+ * @param  {number}     zOut        - zOut parameter.
+ * @param  {function}   rng         - Custom RNG function.
+ * @return {Graph}
+ */
+module.exports = function girvanNewman(GraphClass, options) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/random/girvan-newman: invalid Graph constructor.'
+    );
+
+  var zOut = options.zOut,
+    rng = options.rng || Math.random;
+
+  if (typeof zOut !== 'number')
+    throw new Error(
+      'graphology-generators/random/girvan-newman: invalid `zOut`. Should be a number.'
+    );
+
+  if (typeof rng !== 'function')
+    throw new Error(
+      'graphology-generators/random/girvan-newman: invalid `rng`. Should be a function.'
+    );
+
+  var pOut = zOut / 96,
+    pIn = (16 - pOut * 96) / 31,
+    graph = new GraphClass(),
+    random,
+    i,
+    j;
+
+  for (i = 0; i < 128; i++) graph.addNode(i);
+
+  for (i = 0; i < 128; i++) {
+    for (j = i + 1; j < 128; j++) {
+      random = rng();
+
+      if (i % 4 === j % 4) {
+        if (random < pIn) graph.addEdge(i, j);
+      } else {
+        if (random < pOut) graph.addEdge(i, j);
+      }
+    }
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/random/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bbb75d52b9227c0155ac338d23d9d2b1bb6fd0db
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/index.d.ts
@@ -0,0 +1,3 @@
+export {default as clusters} from './clusters';
+export {default as erdosRenyi} from './erdos-renyi';
+export {default as girvanNewman} from './girvan-newman';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/random/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/random/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b64c720278be9ad98a604f211df674b9db2d5ed9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/random/index.js
@@ -0,0 +1,9 @@
+/**
+ * Graphology Random Graph Generators
+ * ===================================
+ *
+ * Random graph generators endpoint.
+ */
+exports.clusters = require('./clusters.js');
+exports.erdosRenyi = require('./erdos-renyi.js');
+exports.girvanNewman = require('./girvan-newman.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/small/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/small/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ebcc0871319016d66262c288c05eb46a075be862
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/small/index.d.ts
@@ -0,0 +1 @@
+export {default as krackhardtKite} from './krackhardt-kite';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/small/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/small/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..5220ba752aad9bc9b4128a060a1e8362b428eeb2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/small/index.js
@@ -0,0 +1,7 @@
+/**
+ * Graphology Small Graph Generators
+ * ==================================
+ *
+ * Small graph generators endpoint.
+ */
+exports.krackhardtKite = require('./krackhardt-kite.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a8b522e90506786816d3a79e2832f7d862fd1ce
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.d.ts
@@ -0,0 +1,9 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function krackhardtKite<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.js b/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c3885ea10d7b3ea91a1a93648c411e14d456cdb
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/small/krackhardt-kite.js
@@ -0,0 +1,45 @@
+/**
+ * Graphology Krackhardt Kite Graph Generator
+ * ===========================================
+ *
+ * Function generating the Krackhardt kite graph.
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor'),
+  mergeStar = require('graphology-utils/merge-star');
+
+/**
+ * Data.
+ */
+var ADJACENCY = [
+  ['Andre', 'Beverley', 'Carol', 'Diane', 'Fernando'],
+  ['Beverley', 'Andre', 'Ed', 'Garth'],
+  ['Carol', 'Andre', 'Diane', 'Fernando'],
+  ['Diane', 'Andre', 'Beverley', 'Carol', 'Ed', 'Fernando', 'Garth'],
+  ['Ed', 'Beverley', 'Diane', 'Garth'],
+  ['Fernando', 'Andre', 'Carol', 'Diane', 'Garth', 'Heather'],
+  ['Garth', 'Beverley', 'Diane', 'Ed', 'Fernando', 'Heather'],
+  ['Heather', 'Fernando', 'Garth', 'Ike'],
+  ['Ike', 'Heather', 'Jane'],
+  ['Jane', 'Ike']
+];
+
+/**
+ * Function generating the Krackhardt kite graph.
+ *
+ * @param  {Class} GraphClass - The Graph Class to instantiate.
+ * @return {Graph}
+ */
+module.exports = function krackhardtKite(GraphClass) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/social/krackhardt-kite: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass(),
+    i,
+    l;
+
+  for (i = 0, l = ADJACENCY.length; i < l; i++) mergeStar(graph, ADJACENCY[i]);
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c916b7b3375df944e40809504d48a84dc33e7739
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.d.ts
@@ -0,0 +1,9 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function florentineFamilies<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.js b/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.js
new file mode 100644
index 0000000000000000000000000000000000000000..519df3192ce0b9ec5963daaa9d561b1ef88d9972
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/florentine-families.js
@@ -0,0 +1,64 @@
+/**
+ * Graphology Florentine Families Graph Generator
+ * ===============================================
+ *
+ * Function generating the Florentine Families graph.
+ *
+ * [Reference]:
+ * Ronald L. Breiger and Philippa E. Pattison
+ * Cumulated social roles: The duality of persons and their algebras,1
+ * Social Networks, Volume 8, Issue 3, September 1986, Pages 215-256
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Data.
+ */
+var EDGES = [
+  ['Acciaiuoli', 'Medici'],
+  ['Castellani', 'Peruzzi'],
+  ['Castellani', 'Strozzi'],
+  ['Castellani', 'Barbadori'],
+  ['Medici', 'Barbadori'],
+  ['Medici', 'Ridolfi'],
+  ['Medici', 'Tornabuoni'],
+  ['Medici', 'Albizzi'],
+  ['Medici', 'Salviati'],
+  ['Salviati', 'Pazzi'],
+  ['Peruzzi', 'Strozzi'],
+  ['Peruzzi', 'Bischeri'],
+  ['Strozzi', 'Ridolfi'],
+  ['Strozzi', 'Bischeri'],
+  ['Ridolfi', 'Tornabuoni'],
+  ['Tornabuoni', 'Guadagni'],
+  ['Albizzi', 'Ginori'],
+  ['Albizzi', 'Guadagni'],
+  ['Bischeri', 'Guadagni'],
+  ['Guadagni', 'Lamberteschi']
+];
+
+/**
+ * Function generating the florentine families graph.
+ *
+ * @param  {Class} GraphClass - The Graph Class to instantiate.
+ * @return {Graph}
+ */
+module.exports = function florentineFamilies(GraphClass) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/social/florentine-families: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass(),
+    edge,
+    i,
+    l;
+
+  for (i = 0, l = EDGES.length; i < l; i++) {
+    edge = EDGES[i];
+
+    graph.mergeEdge(edge[0], edge[1]);
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/social/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..73e94dea349fc3c3082480b6442835fb964ae0d0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/index.d.ts
@@ -0,0 +1,2 @@
+export {default as florentineFamilies} from './florentine-families';
+export {default as karateClub} from './karate-club';
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/index.js b/libs/shared/graph-layout/node_modules/graphology-generators/social/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..799e5ad97a59d3fa9890e1f9f7719396df5f92a1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/index.js
@@ -0,0 +1,8 @@
+/**
+ * Graphology Social Graph Generators
+ * ===================================
+ *
+ * Social graph generators endpoint.
+ */
+exports.florentineFamilies = require('./florentine-families.js');
+exports.karateClub = require('./karate-club.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.d.ts b/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e8054ed57b9ff09a52059970b7a3c5d00e6bbc4f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.d.ts
@@ -0,0 +1,9 @@
+import Graph, {Attributes, GraphConstructor} from 'graphology-types';
+
+export default function karateClub<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+>(
+  Graph: GraphConstructor<NodeAttributes, EdgeAttributes, GraphAttributes>
+): Graph<NodeAttributes, EdgeAttributes, GraphAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.js b/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.js
new file mode 100644
index 0000000000000000000000000000000000000000..5f0c550f5fd7bc0cbc70066b73e97fba17ad13d5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-generators/social/karate-club.js
@@ -0,0 +1,92 @@
+/**
+ * Graphology Karate Graph Generator
+ * ==================================
+ *
+ * Function generating Zachary's karate club graph.
+ *
+ * [Reference]:
+ * Zachary, Wayne W.
+ * "An Information Flow Model for Conflict and Fission in Small Groups."
+ * Journal of Anthropological Research, 33, 452--473, (1977).
+ */
+var isGraphConstructor = require('graphology-utils/is-graph-constructor');
+
+/**
+ * Data.
+ */
+var DATA = [
+  '0111111110111100010101000000000100',
+  '1011000100000100010101000000001000',
+  '1101000111000100000000000001100010',
+  '1110000100001100000000000000000000',
+  '1000001000100000000000000000000000',
+  '1000001000100000100000000000000000',
+  '1000110000000000100000000000000000',
+  '1111000000000000000000000000000000',
+  '1010000000000000000000000000001011',
+  '0010000000000000000000000000000001',
+  '1000110000000000000000000000000000',
+  '1000000000000000000000000000000000',
+  '1001000000000000000000000000000000',
+  '1111000000000000000000000000000001',
+  '0000000000000000000000000000000011',
+  '0000000000000000000000000000000011',
+  '0000011000000000000000000000000000',
+  '1100000000000000000000000000000000',
+  '0000000000000000000000000000000011',
+  '1100000000000000000000000000000001',
+  '0000000000000000000000000000000011',
+  '1100000000000000000000000000000000',
+  '0000000000000000000000000000000011',
+  '0000000000000000000000000101010011',
+  '0000000000000000000000000101000100',
+  '0000000000000000000000011000000100',
+  '0000000000000000000000000000010001',
+  '0010000000000000000000011000000001',
+  '0010000000000000000000000000000101',
+  '0000000000000000000000010010000011',
+  '0100000010000000000000000000000011',
+  '1000000000000000000000001100100011',
+  '0010000010000011001010110000011101',
+  '0000000011000111001110110011111110'
+];
+
+var CLUB1 = new Set([
+  0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21
+]);
+
+/**
+ * Function generating the karate club graph.
+ *
+ * @param  {Class} GraphClass - The Graph Class to instantiate.
+ * @return {Graph}
+ */
+module.exports = function karateClub(GraphClass) {
+  if (!isGraphConstructor(GraphClass))
+    throw new Error(
+      'graphology-generators/social/karate: invalid Graph constructor.'
+    );
+
+  var graph = new GraphClass(),
+    club;
+
+  for (var i = 0; i < 34; i++) {
+    club = CLUB1.has(i) ? 'Mr. Hi' : 'Officer';
+
+    graph.addNode(i, {club: club});
+  }
+
+  var line, entry, row, column, l, m;
+
+  for (row = 0, l = DATA.length; row < l; row++) {
+    line = DATA[row].split('');
+
+    for (column = row + 1, m = line.length; column < m; column++) {
+      entry = +line[column];
+
+      if (entry) graph.addEdgeWithKey(row + '->' + column, row, column);
+    }
+  }
+
+  return graph;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-indices/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8607086012125253fc8fa77c2a8f0fbcce551429
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/README.md b/libs/shared/graph-layout/node_modules/graphology-indices/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..19fc6bbac20e458590bc61b4f13297a4e8cb97e4
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/README.md
@@ -0,0 +1,21 @@
+# Graphology Indices
+
+Miscellaneous indices to be used with [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-indices
+```
+
+## Contents
+
+This library contains multiple low-level indexation structures used to optimize graph computations in other `graphology` libraries. This library is not meant to be used as such and this is why it is not thoroughly documented.
+
+For now, here are the exposed indices:
+
+- An unweighted and weighted neighborhood index used to speed up computations requiring many successive BSTs in a graph.
+- A directed and undirected index used to track an evolving community structure when running the Louvain community detection algorithm.
+- An indexed view of a graph's connected components sorted by order.
+- A specialized stack/set that can be used to perform memory-efficient DFS traversals.
+- A specialized queue/set that can be used to perform memory-efficient BFS traversals.
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..53399c502c8340812551a7428a4cfa811e3a3869
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.d.ts
@@ -0,0 +1,9 @@
+export default class BFSQueue<T = string> {
+  size: number;
+  seen: Set<string>;
+  constructor(order: number);
+  has(node: string): boolean;
+  push(node: string): boolean;
+  pushWith(node: string, item: T): boolean;
+  shift(): T | undefined;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.js b/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2bdd59c954115c9f6490946c1dee24ba3df3a80
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/bfs-queue.js
@@ -0,0 +1,56 @@
+/**
+ * Graphology BFS Queue
+ * =====================
+ *
+ * An experiment to speed up BFS in graphs and connected component detection.
+ *
+ * It should mostly save memory and not improve theoretical runtime.
+ */
+var FixedDeque = require('mnemonist/fixed-deque');
+
+function BFSQueue(order) {
+  this.queue = new FixedDeque(Array, order);
+  this.seen = new Set();
+  this.size = 0;
+}
+
+BFSQueue.prototype.has = function (node) {
+  return this.seen.has(node);
+};
+
+BFSQueue.prototype.push = function (node) {
+  var seenSizeBefore = this.seen.size;
+
+  this.seen.add(node);
+
+  // If node was already seen
+  if (seenSizeBefore === this.seen.size) return false;
+
+  this.queue.push(node);
+  this.size++;
+
+  return true;
+};
+
+BFSQueue.prototype.pushWith = function (node, item) {
+  var seenSizeBefore = this.seen.size;
+
+  this.seen.add(node);
+
+  // If node was already seen
+  if (seenSizeBefore === this.seen.size) return false;
+
+  this.queue.push(item);
+  this.size++;
+
+  return true;
+};
+
+BFSQueue.prototype.shift = function () {
+  var item = this.queue.shift();
+  this.size = this.queue.size;
+
+  return item;
+};
+
+module.exports = BFSQueue;
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..70eec7c5f1b50be244b013f214691bce09744ecf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.d.ts
@@ -0,0 +1,9 @@
+export default class DFSStack<T = string> {
+  size: number;
+  seen: Set<string>;
+  constructor(order: number);
+  has(node: string): boolean;
+  push(node: string): boolean;
+  pushWith(node: string, item: T): boolean;
+  pop(): T | undefined;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.js b/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ffe5302218e583e07a27d37e173b0311346893d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/dfs-stack.js
@@ -0,0 +1,51 @@
+/**
+ * Graphology DFS Stack
+ * =====================
+ *
+ * An experiment to speed up DFS in graphs and connected component detection.
+ *
+ * It should mostly save memory and not improve theoretical runtime.
+ */
+function DFSStack(order) {
+  this.stack = new Array(order);
+  this.seen = new Set();
+  this.size = 0;
+}
+
+DFSStack.prototype.has = function (node) {
+  return this.seen.has(node);
+};
+
+DFSStack.prototype.push = function (node) {
+  var seenSizeBefore = this.seen.size;
+
+  this.seen.add(node);
+
+  // If node was already seen
+  if (seenSizeBefore === this.seen.size) return false;
+
+  this.stack[this.size++] = node;
+
+  return true;
+};
+
+DFSStack.prototype.pushWith = function (node, item) {
+  var seenSizeBefore = this.seen.size;
+
+  this.seen.add(node);
+
+  // If node was already seen
+  if (seenSizeBefore === this.seen.size) return false;
+
+  this.stack[this.size++] = item;
+
+  return true;
+};
+
+DFSStack.prototype.pop = function () {
+  if (this.size === 0) return;
+
+  return this.stack[--this.size];
+};
+
+module.exports = DFSStack;
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5af84c5ed7d4ebee99e0a98c66cf13f85bb3df3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/index.d.ts
@@ -0,0 +1,5 @@
+export {default as BFSQueue} from './bfs-queue';
+export {default as DFSStack} from './dfs-stack';
+export {UndirectedLouvainIndex, DirectedLouvainIndex} from './louvain';
+export {NeighborhoodIndex, WeightedNeighborhoodIndex} from './neighborhood';
+export {default as SortedComponentsIndex} from './sorted-components';
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/index.js b/libs/shared/graph-layout/node_modules/graphology-indices/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b0eab1bede12df9e5a761c8d16a7df7ded4c308
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/index.js
@@ -0,0 +1,16 @@
+/**
+ * Graphology Indices
+ * ===================
+ *
+ * Library endpoint.
+ */
+var louvain = require('./louvain.js');
+var neighborhood = require('./neighborhood.js');
+
+exports.BFSQueue = require('./bfs-queue.js');
+exports.DFSStack = require('./dfs-stack.js');
+exports.UndirectedLouvainIndex = louvain.UndirectedLouvainIndex;
+exports.DirectedLouvainIndex = louvain.DirectedLouvainIndex;
+exports.NeighborhoodIndex = neighborhood.NeighborhoodIndex;
+exports.WeightedNeighborhoodIndex = neighborhood.WeightedNeighborhoodIndex;
+exports.SortedComponentsIndex = require('./sorted-components.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/louvain.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/louvain.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eab448e4b3617347ee8e0fa441c86994ce85da96
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/louvain.d.ts
@@ -0,0 +1,128 @@
+import Graph, {Attributes, EdgeMapper} from 'graphology-types';
+
+type PointerArray = Uint8Array | Uint16Array | Uint32Array | Float64Array;
+
+type LouvainIndexOptions<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> = {
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | EdgeMapper<number, NodeAttributes, EdgeAttributes>
+    | null;
+  keepDendrogram?: boolean;
+  resolution?: number;
+};
+
+type CommunityMapping = {[key: string]: number};
+type NeighborhoodProjection = {[key: string]: Array<string>};
+
+export class UndirectedLouvainIndex<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> {
+  constructor(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: LouvainIndexOptions<NodeAttributes, EdgeAttributes>
+  );
+
+  M: number;
+  C: number;
+  E: number;
+  level: number;
+  graph: Graph;
+  neighborhood: PointerArray;
+  starts: PointerArray;
+  nodes: Array<string>;
+
+  bounds(index: number): [number, number];
+  project(): NeighborhoodProjection;
+  isolate(index: number, degree: number): number;
+  move(index: number, degree: number, targetCommunity: number): void;
+  computeNodeDegree(index: number): number;
+  expensiveMove(index: number, targetCommunity: number): void;
+  expensiveIsolate(index: number): number;
+  zoomOut(): {[key: string]: number};
+  modularity(): number;
+  delta(
+    index: number,
+    degree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  deltaWithOwnCommunity(
+    index: number,
+    degree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  fastDelta(
+    index: number,
+    degree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  fastDeltaWithOwnCommunity(
+    index: number,
+    degree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  collect(level?: number): CommunityMapping;
+  assign(prop: string, level?: number): void;
+}
+
+export class DirectedLouvainIndex<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> {
+  constructor(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: LouvainIndexOptions<NodeAttributes, EdgeAttributes>
+  );
+
+  M: number;
+  C: number;
+  E: number;
+  level: number;
+  graph: Graph;
+  neighborhood: PointerArray;
+  starts: PointerArray;
+  offsets: PointerArray;
+  nodes: Array<string>;
+
+  bounds(index: number): [number, number];
+  inBounds(index: number): [number, number];
+  outBounds(index: number): [number, number];
+  project(): NeighborhoodProjection;
+  projectIn(): NeighborhoodProjection;
+  projectOut(): NeighborhoodProjection;
+  isolate(index: number, inDegree: number, outDegree: number): number;
+  move(
+    index: number,
+    inDegree: number,
+    outDegree: number,
+    targetCommunity: number
+  ): void;
+  computeNodeInDegree(index: number): number;
+  computeNodeOutDegree(index: number): number;
+  expensiveMove(index: number, targetCommunity: number): void;
+  zoomOut(): {[key: string]: number};
+  modularity(): number;
+  delta(
+    index: number,
+    inDegree: number,
+    outDegree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  deltaWithOwnCommunity(
+    index: number,
+    inDegree: number,
+    outDegree: number,
+    targetCommunityDegree: number,
+    targetCommunity: number
+  ): number;
+  collect(level?: number): CommunityMapping;
+  assign(prop: string, level?: number): void;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/louvain.js b/libs/shared/graph-layout/node_modules/graphology-indices/louvain.js
new file mode 100644
index 0000000000000000000000000000000000000000..111037b4b54e7bdec7f9e3cb547ceafba53e26e2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/louvain.js
@@ -0,0 +1,1021 @@
+/**
+ * Graphology Louvain Indices
+ * ===========================
+ *
+ * Undirected & Directed Louvain Index structures used to compute the famous
+ * Louvain community detection algorithm.
+ *
+ * Most of the rationale is explained in `graphology-metrics`.
+ *
+ * Note that this index shares a lot with the classic Union-Find data
+ * structure. It also relies on a unused id stack to make sure we can
+ * increase again the number of communites when isolating nodes.
+ *
+ * [Articles]
+ * M. E. J. Newman, « Modularity and community structure in networks »,
+ * Proc. Natl. Acad. Sci. USA, vol. 103, no 23, 2006, p. 8577–8582
+ * https://dx.doi.org/10.1073%2Fpnas.0601602103
+ *
+ * Newman, M. E. J. « Community detection in networks: Modularity optimization
+ * and maximum likelihood are equivalent ». Physical Review E, vol. 94, no 5,
+ * novembre 2016, p. 052315. arXiv.org, doi:10.1103/PhysRevE.94.052315.
+ * https://arxiv.org/pdf/1606.02319.pdf
+ *
+ * Blondel, Vincent D., et al. « Fast unfolding of communities in large
+ * networks ». Journal of Statistical Mechanics: Theory and Experiment,
+ * vol. 2008, no 10, octobre 2008, p. P10008. DOI.org (Crossref),
+ * doi:10.1088/1742-5468/2008/10/P10008.
+ * https://arxiv.org/pdf/0803.0476.pdf
+ *
+ * Nicolas Dugué, Anthony Perez. Directed Louvain: maximizing modularity in
+ * directed networks. [Research Report] Université d’Orléans. 2015. hal-01231784
+ * https://hal.archives-ouvertes.fr/hal-01231784
+ *
+ * R. Lambiotte, J.-C. Delvenne and M. Barahona. Laplacian Dynamics and
+ * Multiscale Modular Structure in Networks,
+ * doi:10.1109/TNSE.2015.2391998.
+ * https://arxiv.org/abs/0812.1770
+ *
+ * [Latex]:
+ *
+ * Undirected Case:
+ * ----------------
+ *
+ * \Delta Q=\bigg{[}\frac{\sum^{c}_{in}-(2d_{c}+l)}{2m}-\bigg{(}\frac{\sum^{c}_{tot}-(d+l)}{2m}\bigg{)}^{2}+\frac{\sum^{t}_{in}+(2d_{t}+l)}{2m}-\bigg{(}\frac{\sum^{t}_{tot}+(d+l)}{2m}\bigg{)}^{2}\bigg{]}-\bigg{[}\frac{\sum^{c}_{in}}{2m}-\bigg{(}\frac{\sum^{c}_{tot}}{2m}\bigg{)}^{2}+\frac{\sum^{t}_{in}}{2m}-\bigg{(}\frac{\sum^{t}_{tot}}{2m}\bigg{)}^{2}\bigg{]}
+ * \Delta Q=\frac{d_{t}-d_{c}}{m}+\frac{l\sum^{c}_{tot}+d\sum^{c}_{tot}-d^{2}-l^{2}-2dl-l\sum^{t}_{tot}-d\sum^{t}_{tot}}{2m^{2}}
+ * \Delta Q=\frac{d_{t}-d_{c}}{m}+\frac{(l+d)\sum^{c}_{tot}-d^{2}-l^{2}-2dl-(l+d)\sum^{t}_{tot}}{2m^{2}}
+ *
+ * Directed Case:
+ * --------------
+ * \Delta Q_d=\bigg{[}\frac{\sum^{c}_{in}-(d_{c.in}+d_{c.out}+l)}{m}-\frac{(\sum^{c}_{tot.in}-(d_{in}+l))(\sum^{c}_{tot.out}-(d_{out}+l))}{m^{2}}+\frac{\sum^{t}_{in}+(d_{t.in}+d_{t.out}+l)}{m}-\frac{(\sum^{t}_{tot.in}+(d_{in}+l))(\sum^{t}_{tot.out}+(d_{out}+l))}{m^{2}}\bigg{]}-\bigg{[}\frac{\sum^{c}_{in}}{m}-\frac{\sum^{c}_{tot.in}\sum^{c}_{tot.out}}{m^{2}}+\frac{\sum^{t}_{in}}{m}-\frac{\sum^{t}_{tot.in}\sum^{t}_{tot.out}}{m^{2}}\bigg{]}
+ *
+ * [Notes]:
+ * Louvain is a bit unclear on this but delta computation are not derived from
+ * Q1 - Q2 but rather between Q when considered node is isolated in its own
+ * community versus Q with this node in target community. This is in fact
+ * an optimization because the subtract part is constant in the formulae and
+ * does not affect delta comparisons.
+ */
+var typed = require('mnemonist/utils/typed-arrays');
+var resolveDefaults = require('graphology-utils/defaults');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+
+var INSPECT = Symbol.for('nodejs.util.inspect.custom');
+
+var DEFAULTS = {
+  getEdgeWeight: 'weight',
+  keepDendrogram: false,
+  resolution: 1
+};
+
+function UndirectedLouvainIndex(graph, options) {
+  // Solving options
+  options = resolveDefaults(options, DEFAULTS);
+
+  var resolution = options.resolution;
+
+  // Weight getters
+  var getEdgeWeight = createEdgeWeightGetter(options.getEdgeWeight).fromEntry;
+
+  // Building the index
+  var size = (graph.size - graph.selfLoopCount) * 2;
+
+  var NeighborhoodPointerArray = typed.getPointerArray(size);
+  var NodesPointerArray = typed.getPointerArray(graph.order + 1);
+
+  // NOTE: this memory optimization can yield overflow deopt when computing deltas
+  var WeightsArray = options.getEdgeWeight
+    ? Float64Array
+    : typed.getPointerArray(graph.size * 2);
+
+  // Properties
+  this.C = graph.order;
+  this.M = 0;
+  this.E = size;
+  this.U = 0;
+  this.resolution = resolution;
+  this.level = 0;
+  this.graph = graph;
+  this.nodes = new Array(graph.order);
+  this.keepDendrogram = options.keepDendrogram;
+
+  // Edge-level
+  this.neighborhood = new NodesPointerArray(size);
+  this.weights = new WeightsArray(size);
+
+  // Node-level
+  this.loops = new WeightsArray(graph.order);
+  this.starts = new NeighborhoodPointerArray(graph.order + 1);
+  this.belongings = new NodesPointerArray(graph.order);
+  this.dendrogram = [];
+  this.mapping = null;
+
+  // Community-level
+  this.counts = new NodesPointerArray(graph.order);
+  this.unused = new NodesPointerArray(graph.order);
+  this.totalWeights = new WeightsArray(graph.order);
+
+  var ids = {};
+
+  var weight;
+
+  var i = 0,
+    n = 0;
+
+  var self = this;
+
+  graph.forEachNode(function (node) {
+    self.nodes[i] = node;
+
+    // Node map to index
+    ids[node] = i;
+
+    // Initializing starts
+    n += graph.undirectedDegreeWithoutSelfLoops(node);
+    self.starts[i] = n;
+
+    // Belongings
+    self.belongings[i] = i;
+    self.counts[i] = 1;
+    i++;
+  });
+
+  // Single sweep over the edges
+  graph.forEachEdge(function (edge, attr, source, target, sa, ta, u) {
+    weight = getEdgeWeight(edge, attr, source, target, sa, ta, u);
+
+    source = ids[source];
+    target = ids[target];
+
+    self.M += weight;
+
+    // Self loop?
+    if (source === target) {
+      self.totalWeights[source] += weight * 2;
+      self.loops[source] = weight * 2;
+    } else {
+      self.totalWeights[source] += weight;
+      self.totalWeights[target] += weight;
+
+      var startSource = --self.starts[source],
+        startTarget = --self.starts[target];
+
+      self.neighborhood[startSource] = target;
+      self.neighborhood[startTarget] = source;
+
+      self.weights[startSource] = weight;
+      self.weights[startTarget] = weight;
+    }
+  });
+
+  this.starts[i] = this.E;
+
+  if (this.keepDendrogram) this.dendrogram.push(this.belongings.slice());
+  else this.mapping = this.belongings.slice();
+}
+
+UndirectedLouvainIndex.prototype.isolate = function (i, degree) {
+  var currentCommunity = this.belongings[i];
+
+  // The node is already isolated
+  if (this.counts[currentCommunity] === 1) return currentCommunity;
+
+  var newCommunity = this.unused[--this.U];
+
+  var loops = this.loops[i];
+
+  this.totalWeights[currentCommunity] -= degree + loops;
+  this.totalWeights[newCommunity] += degree + loops;
+
+  this.belongings[i] = newCommunity;
+
+  this.counts[currentCommunity]--;
+  this.counts[newCommunity]++;
+
+  return newCommunity;
+};
+
+UndirectedLouvainIndex.prototype.move = function (i, degree, targetCommunity) {
+  var currentCommunity = this.belongings[i],
+    loops = this.loops[i];
+
+  this.totalWeights[currentCommunity] -= degree + loops;
+  this.totalWeights[targetCommunity] += degree + loops;
+
+  this.belongings[i] = targetCommunity;
+
+  var nowEmpty = this.counts[currentCommunity]-- === 1;
+  this.counts[targetCommunity]++;
+
+  if (nowEmpty) this.unused[this.U++] = currentCommunity;
+};
+
+UndirectedLouvainIndex.prototype.computeNodeDegree = function (i) {
+  var o, l, weight;
+
+  var degree = 0;
+
+  for (o = this.starts[i], l = this.starts[i + 1]; o < l; o++) {
+    weight = this.weights[o];
+
+    degree += weight;
+  }
+
+  return degree;
+};
+
+UndirectedLouvainIndex.prototype.expensiveIsolate = function (i) {
+  var degree = this.computeNodeDegree(i);
+  return this.isolate(i, degree);
+};
+
+UndirectedLouvainIndex.prototype.expensiveMove = function (i, ci) {
+  var degree = this.computeNodeDegree(i);
+  this.move(i, degree, ci);
+};
+
+UndirectedLouvainIndex.prototype.zoomOut = function () {
+  var inducedGraph = new Array(this.C - this.U),
+    newLabels = {};
+
+  var N = this.nodes.length;
+
+  var C = 0,
+    E = 0;
+
+  var i, j, l, m, n, ci, cj, data, adj;
+
+  // Renumbering communities
+  for (i = 0, l = this.C; i < l; i++) {
+    ci = this.belongings[i];
+
+    if (!(ci in newLabels)) {
+      newLabels[ci] = C;
+      inducedGraph[C] = {
+        adj: {},
+        totalWeights: this.totalWeights[ci],
+        internalWeights: 0
+      };
+      C++;
+    }
+
+    // We do this to otpimize the number of lookups in next loop
+    this.belongings[i] = newLabels[ci];
+  }
+
+  // Actualizing dendrogram
+  var currentLevel, nextLevel;
+
+  if (this.keepDendrogram) {
+    currentLevel = this.dendrogram[this.level];
+    nextLevel = new (typed.getPointerArray(C))(N);
+
+    for (i = 0; i < N; i++) nextLevel[i] = this.belongings[currentLevel[i]];
+
+    this.dendrogram.push(nextLevel);
+  } else {
+    for (i = 0; i < N; i++) this.mapping[i] = this.belongings[this.mapping[i]];
+  }
+
+  // Building induced graph matrix
+  for (i = 0, l = this.C; i < l; i++) {
+    ci = this.belongings[i];
+
+    data = inducedGraph[ci];
+    adj = data.adj;
+    data.internalWeights += this.loops[i];
+
+    for (j = this.starts[i], m = this.starts[i + 1]; j < m; j++) {
+      n = this.neighborhood[j];
+      cj = this.belongings[n];
+
+      if (ci === cj) {
+        data.internalWeights += this.weights[j];
+        continue;
+      }
+
+      if (!(cj in adj)) adj[cj] = 0;
+
+      adj[cj] += this.weights[j];
+    }
+  }
+
+  // Rewriting neighborhood
+  this.C = C;
+
+  n = 0;
+
+  for (ci = 0; ci < C; ci++) {
+    data = inducedGraph[ci];
+    adj = data.adj;
+
+    ci = +ci;
+
+    this.totalWeights[ci] = data.totalWeights;
+    this.loops[ci] = data.internalWeights;
+    this.counts[ci] = 1;
+
+    this.starts[ci] = n;
+    this.belongings[ci] = ci;
+
+    for (cj in adj) {
+      this.neighborhood[n] = +cj;
+      this.weights[n] = adj[cj];
+
+      E++;
+      n++;
+    }
+  }
+
+  this.starts[C] = E;
+
+  this.E = E;
+  this.U = 0;
+  this.level++;
+
+  return newLabels;
+};
+
+UndirectedLouvainIndex.prototype.modularity = function () {
+  var ci, cj, i, j, m;
+
+  var Q = 0;
+  var M2 = this.M * 2;
+  var internalWeights = new Float64Array(this.C);
+
+  for (i = 0; i < this.C; i++) {
+    ci = this.belongings[i];
+    internalWeights[ci] += this.loops[i];
+
+    for (j = this.starts[i], m = this.starts[i + 1]; j < m; j++) {
+      cj = this.belongings[this.neighborhood[j]];
+
+      if (ci !== cj) continue;
+
+      internalWeights[ci] += this.weights[j];
+    }
+  }
+
+  for (i = 0; i < this.C; i++) {
+    Q +=
+      internalWeights[i] / M2 -
+      Math.pow(this.totalWeights[i] / M2, 2) * this.resolution;
+  }
+
+  return Q;
+};
+
+UndirectedLouvainIndex.prototype.delta = function (
+  i,
+  degree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalWeight = this.totalWeights[targetCommunity];
+
+  degree += this.loops[i];
+
+  return (
+    targetCommunityDegree / M - // NOTE: formula is a bit different here because targetCommunityDegree is passed without * 2
+    (targetCommunityTotalWeight * degree * this.resolution) / (2 * M * M)
+  );
+};
+
+UndirectedLouvainIndex.prototype.deltaWithOwnCommunity = function (
+  i,
+  degree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalWeight = this.totalWeights[targetCommunity];
+
+  degree += this.loops[i];
+
+  return (
+    targetCommunityDegree / M - // NOTE: formula is a bit different here because targetCommunityDegree is passed without * 2
+    ((targetCommunityTotalWeight - degree) * degree * this.resolution) /
+      (2 * M * M)
+  );
+};
+
+// NOTE: this is just a faster but equivalent version of #.delta
+// It is just off by a constant factor and is just faster to compute
+UndirectedLouvainIndex.prototype.fastDelta = function (
+  i,
+  degree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalWeight = this.totalWeights[targetCommunity];
+
+  degree += this.loops[i];
+
+  return (
+    targetCommunityDegree -
+    (degree * targetCommunityTotalWeight * this.resolution) / (2 * M)
+  );
+};
+
+UndirectedLouvainIndex.prototype.fastDeltaWithOwnCommunity = function (
+  i,
+  degree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalWeight = this.totalWeights[targetCommunity];
+
+  degree += this.loops[i];
+
+  return (
+    targetCommunityDegree -
+    (degree * (targetCommunityTotalWeight - degree) * this.resolution) / (2 * M)
+  );
+};
+
+UndirectedLouvainIndex.prototype.bounds = function (i) {
+  return [this.starts[i], this.starts[i + 1]];
+};
+
+UndirectedLouvainIndex.prototype.project = function () {
+  var self = this;
+
+  var projection = {};
+
+  self.nodes.slice(0, this.C).forEach(function (node, i) {
+    projection[node] = Array.from(
+      self.neighborhood.slice(self.starts[i], self.starts[i + 1])
+    ).map(function (j) {
+      return self.nodes[j];
+    });
+  });
+
+  return projection;
+};
+
+UndirectedLouvainIndex.prototype.collect = function (level) {
+  if (arguments.length < 1) level = this.level;
+
+  var o = {};
+
+  var mapping = this.keepDendrogram ? this.dendrogram[level] : this.mapping;
+
+  var i, l;
+
+  for (i = 0, l = mapping.length; i < l; i++) o[this.nodes[i]] = mapping[i];
+
+  return o;
+};
+
+UndirectedLouvainIndex.prototype.assign = function (prop, level) {
+  if (arguments.length < 2) level = this.level;
+
+  var mapping = this.keepDendrogram ? this.dendrogram[level] : this.mapping;
+
+  var i, l;
+
+  for (i = 0, l = mapping.length; i < l; i++)
+    this.graph.setNodeAttribute(this.nodes[i], prop, mapping[i]);
+};
+
+UndirectedLouvainIndex.prototype[INSPECT] = function () {
+  var proxy = {};
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: UndirectedLouvainIndex,
+    enumerable: false
+  });
+
+  proxy.C = this.C;
+  proxy.M = this.M;
+  proxy.E = this.E;
+  proxy.U = this.U;
+  proxy.resolution = this.resolution;
+  proxy.level = this.level;
+  proxy.nodes = this.nodes;
+  proxy.starts = this.starts.slice(0, proxy.C + 1);
+
+  var eTruncated = ['neighborhood', 'weights'];
+  var cTruncated = ['counts', 'loops', 'belongings', 'totalWeights'];
+
+  var self = this;
+
+  eTruncated.forEach(function (key) {
+    proxy[key] = self[key].slice(0, proxy.E);
+  });
+
+  cTruncated.forEach(function (key) {
+    proxy[key] = self[key].slice(0, proxy.C);
+  });
+
+  proxy.unused = this.unused.slice(0, this.U);
+
+  if (this.keepDendrogram) proxy.dendrogram = this.dendrogram;
+  else proxy.mapping = this.mapping;
+
+  return proxy;
+};
+
+function DirectedLouvainIndex(graph, options) {
+  // Solving options
+  options = resolveDefaults(options, DEFAULTS);
+
+  var resolution = options.resolution;
+
+  // Weight getters
+  var getEdgeWeight = createEdgeWeightGetter(options.getEdgeWeight).fromEntry;
+
+  // Building the index
+  var size = (graph.size - graph.selfLoopCount) * 2;
+
+  var NeighborhoodPointerArray = typed.getPointerArray(size);
+  var NodesPointerArray = typed.getPointerArray(graph.order + 1);
+
+  // NOTE: this memory optimization can yield overflow deopt when computing deltas
+  var WeightsArray = options.getEdgeWeight
+    ? Float64Array
+    : typed.getPointerArray(graph.size * 2);
+
+  // Properties
+  this.C = graph.order;
+  this.M = 0;
+  this.E = size;
+  this.U = 0;
+  this.resolution = resolution;
+  this.level = 0;
+  this.graph = graph;
+  this.nodes = new Array(graph.order);
+  this.keepDendrogram = options.keepDendrogram;
+
+  // Edge-level
+  // NOTE: edges are stored out then in, in this order
+  this.neighborhood = new NodesPointerArray(size);
+  this.weights = new WeightsArray(size);
+
+  // Node-level
+  this.loops = new WeightsArray(graph.order);
+  this.starts = new NeighborhoodPointerArray(graph.order + 1);
+  this.offsets = new NeighborhoodPointerArray(graph.order);
+  this.belongings = new NodesPointerArray(graph.order);
+  this.dendrogram = [];
+
+  // Community-level
+  this.counts = new NodesPointerArray(graph.order);
+  this.unused = new NodesPointerArray(graph.order);
+  this.totalInWeights = new WeightsArray(graph.order);
+  this.totalOutWeights = new WeightsArray(graph.order);
+
+  var ids = {};
+
+  var weight;
+
+  var i = 0,
+    n = 0;
+
+  var self = this;
+
+  graph.forEachNode(function (node) {
+    self.nodes[i] = node;
+
+    // Node map to index
+    ids[node] = i;
+
+    // Initializing starts & offsets
+    n += graph.outDegreeWithoutSelfLoops(node);
+    self.starts[i] = n;
+
+    n += graph.inDegreeWithoutSelfLoops(node);
+    self.offsets[i] = n;
+
+    // Belongings
+    self.belongings[i] = i;
+    self.counts[i] = 1;
+    i++;
+  });
+
+  // Single sweep over the edges
+  graph.forEachEdge(function (edge, attr, source, target, sa, ta, u) {
+    weight = getEdgeWeight(edge, attr, source, target, sa, ta, u);
+
+    source = ids[source];
+    target = ids[target];
+
+    self.M += weight;
+
+    // Self loop?
+    if (source === target) {
+      self.loops[source] += weight;
+      self.totalInWeights[source] += weight;
+      self.totalOutWeights[source] += weight;
+    } else {
+      self.totalOutWeights[source] += weight;
+      self.totalInWeights[target] += weight;
+
+      var startSource = --self.starts[source],
+        startTarget = --self.offsets[target];
+
+      self.neighborhood[startSource] = target;
+      self.neighborhood[startTarget] = source;
+
+      self.weights[startSource] = weight;
+      self.weights[startTarget] = weight;
+    }
+  });
+
+  this.starts[i] = this.E;
+
+  if (this.keepDendrogram) this.dendrogram.push(this.belongings.slice());
+  else this.mapping = this.belongings.slice();
+}
+
+DirectedLouvainIndex.prototype.bounds = UndirectedLouvainIndex.prototype.bounds;
+
+DirectedLouvainIndex.prototype.inBounds = function (i) {
+  return [this.offsets[i], this.starts[i + 1]];
+};
+
+DirectedLouvainIndex.prototype.outBounds = function (i) {
+  return [this.starts[i], this.offsets[i]];
+};
+
+DirectedLouvainIndex.prototype.project =
+  UndirectedLouvainIndex.prototype.project;
+
+DirectedLouvainIndex.prototype.projectIn = function () {
+  var self = this;
+
+  var projection = {};
+
+  self.nodes.slice(0, this.C).forEach(function (node, i) {
+    projection[node] = Array.from(
+      self.neighborhood.slice(self.offsets[i], self.starts[i + 1])
+    ).map(function (j) {
+      return self.nodes[j];
+    });
+  });
+
+  return projection;
+};
+
+DirectedLouvainIndex.prototype.projectOut = function () {
+  var self = this;
+
+  var projection = {};
+
+  self.nodes.slice(0, this.C).forEach(function (node, i) {
+    projection[node] = Array.from(
+      self.neighborhood.slice(self.starts[i], self.offsets[i])
+    ).map(function (j) {
+      return self.nodes[j];
+    });
+  });
+
+  return projection;
+};
+
+DirectedLouvainIndex.prototype.isolate = function (i, inDegree, outDegree) {
+  var currentCommunity = this.belongings[i];
+
+  // The node is already isolated
+  if (this.counts[currentCommunity] === 1) return currentCommunity;
+
+  var newCommunity = this.unused[--this.U];
+
+  var loops = this.loops[i];
+
+  this.totalInWeights[currentCommunity] -= inDegree + loops;
+  this.totalInWeights[newCommunity] += inDegree + loops;
+
+  this.totalOutWeights[currentCommunity] -= outDegree + loops;
+  this.totalOutWeights[newCommunity] += outDegree + loops;
+
+  this.belongings[i] = newCommunity;
+
+  this.counts[currentCommunity]--;
+  this.counts[newCommunity]++;
+
+  return newCommunity;
+};
+
+DirectedLouvainIndex.prototype.move = function (
+  i,
+  inDegree,
+  outDegree,
+  targetCommunity
+) {
+  var currentCommunity = this.belongings[i],
+    loops = this.loops[i];
+
+  this.totalInWeights[currentCommunity] -= inDegree + loops;
+  this.totalInWeights[targetCommunity] += inDegree + loops;
+
+  this.totalOutWeights[currentCommunity] -= outDegree + loops;
+  this.totalOutWeights[targetCommunity] += outDegree + loops;
+
+  this.belongings[i] = targetCommunity;
+
+  var nowEmpty = this.counts[currentCommunity]-- === 1;
+  this.counts[targetCommunity]++;
+
+  if (nowEmpty) this.unused[this.U++] = currentCommunity;
+};
+
+DirectedLouvainIndex.prototype.computeNodeInDegree = function (i) {
+  var o, l, weight;
+
+  var inDegree = 0;
+
+  for (o = this.offsets[i], l = this.starts[i + 1]; o < l; o++) {
+    weight = this.weights[o];
+
+    inDegree += weight;
+  }
+
+  return inDegree;
+};
+
+DirectedLouvainIndex.prototype.computeNodeOutDegree = function (i) {
+  var o, l, weight;
+
+  var outDegree = 0;
+
+  for (o = this.starts[i], l = this.offsets[i]; o < l; o++) {
+    weight = this.weights[o];
+
+    outDegree += weight;
+  }
+
+  return outDegree;
+};
+
+DirectedLouvainIndex.prototype.expensiveMove = function (i, ci) {
+  var inDegree = this.computeNodeInDegree(i),
+    outDegree = this.computeNodeOutDegree(i);
+
+  this.move(i, inDegree, outDegree, ci);
+};
+
+DirectedLouvainIndex.prototype.zoomOut = function () {
+  var inducedGraph = new Array(this.C - this.U),
+    newLabels = {};
+
+  var N = this.nodes.length;
+
+  var C = 0,
+    E = 0;
+
+  var i, j, l, m, n, ci, cj, data, offset, out, adj, inAdj, outAdj;
+
+  // Renumbering communities
+  for (i = 0, l = this.C; i < l; i++) {
+    ci = this.belongings[i];
+
+    if (!(ci in newLabels)) {
+      newLabels[ci] = C;
+      inducedGraph[C] = {
+        inAdj: {},
+        outAdj: {},
+        totalInWeights: this.totalInWeights[ci],
+        totalOutWeights: this.totalOutWeights[ci],
+        internalWeights: 0
+      };
+      C++;
+    }
+
+    // We do this to otpimize the number of lookups in next loop
+    this.belongings[i] = newLabels[ci];
+  }
+
+  // Actualizing dendrogram
+  var currentLevel, nextLevel;
+
+  if (this.keepDendrogram) {
+    currentLevel = this.dendrogram[this.level];
+    nextLevel = new (typed.getPointerArray(C))(N);
+
+    for (i = 0; i < N; i++) nextLevel[i] = this.belongings[currentLevel[i]];
+
+    this.dendrogram.push(nextLevel);
+  } else {
+    for (i = 0; i < N; i++) this.mapping[i] = this.belongings[this.mapping[i]];
+  }
+
+  // Building induced graph matrix
+  for (i = 0, l = this.C; i < l; i++) {
+    ci = this.belongings[i];
+    offset = this.offsets[i];
+
+    data = inducedGraph[ci];
+    inAdj = data.inAdj;
+    outAdj = data.outAdj;
+    data.internalWeights += this.loops[i];
+
+    for (j = this.starts[i], m = this.starts[i + 1]; j < m; j++) {
+      n = this.neighborhood[j];
+      cj = this.belongings[n];
+      out = j < offset;
+
+      adj = out ? outAdj : inAdj;
+
+      if (ci === cj) {
+        if (out) data.internalWeights += this.weights[j];
+
+        continue;
+      }
+
+      if (!(cj in adj)) adj[cj] = 0;
+
+      adj[cj] += this.weights[j];
+    }
+  }
+
+  // Rewriting neighborhood
+  this.C = C;
+
+  n = 0;
+
+  for (ci = 0; ci < C; ci++) {
+    data = inducedGraph[ci];
+    inAdj = data.inAdj;
+    outAdj = data.outAdj;
+
+    ci = +ci;
+
+    this.totalInWeights[ci] = data.totalInWeights;
+    this.totalOutWeights[ci] = data.totalOutWeights;
+    this.loops[ci] = data.internalWeights;
+    this.counts[ci] = 1;
+
+    this.starts[ci] = n;
+    this.belongings[ci] = ci;
+
+    for (cj in outAdj) {
+      this.neighborhood[n] = +cj;
+      this.weights[n] = outAdj[cj];
+
+      E++;
+      n++;
+    }
+
+    this.offsets[ci] = n;
+
+    for (cj in inAdj) {
+      this.neighborhood[n] = +cj;
+      this.weights[n] = inAdj[cj];
+
+      E++;
+      n++;
+    }
+  }
+
+  this.starts[C] = E;
+
+  this.E = E;
+  this.U = 0;
+  this.level++;
+
+  return newLabels;
+};
+
+DirectedLouvainIndex.prototype.modularity = function () {
+  var ci, cj, i, j, m;
+
+  var Q = 0;
+  var M = this.M;
+  var internalWeights = new Float64Array(this.C);
+
+  for (i = 0; i < this.C; i++) {
+    ci = this.belongings[i];
+    internalWeights[ci] += this.loops[i];
+
+    for (j = this.starts[i], m = this.offsets[i]; j < m; j++) {
+      cj = this.belongings[this.neighborhood[j]];
+
+      if (ci !== cj) continue;
+
+      internalWeights[ci] += this.weights[j];
+    }
+  }
+
+  for (i = 0; i < this.C; i++)
+    Q +=
+      internalWeights[i] / M -
+      ((this.totalInWeights[i] * this.totalOutWeights[i]) / Math.pow(M, 2)) *
+        this.resolution;
+
+  return Q;
+};
+
+DirectedLouvainIndex.prototype.delta = function (
+  i,
+  inDegree,
+  outDegree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalInWeight = this.totalInWeights[targetCommunity],
+    targetCommunityTotalOutWeight = this.totalOutWeights[targetCommunity];
+
+  var loops = this.loops[i];
+
+  inDegree += loops;
+  outDegree += loops;
+
+  return (
+    targetCommunityDegree / M -
+    ((outDegree * targetCommunityTotalInWeight +
+      inDegree * targetCommunityTotalOutWeight) *
+      this.resolution) /
+      (M * M)
+  );
+};
+
+DirectedLouvainIndex.prototype.deltaWithOwnCommunity = function (
+  i,
+  inDegree,
+  outDegree,
+  targetCommunityDegree,
+  targetCommunity
+) {
+  var M = this.M;
+
+  var targetCommunityTotalInWeight = this.totalInWeights[targetCommunity],
+    targetCommunityTotalOutWeight = this.totalOutWeights[targetCommunity];
+
+  var loops = this.loops[i];
+
+  inDegree += loops;
+  outDegree += loops;
+
+  return (
+    targetCommunityDegree / M -
+    ((outDegree * (targetCommunityTotalInWeight - inDegree) +
+      inDegree * (targetCommunityTotalOutWeight - outDegree)) *
+      this.resolution) /
+      (M * M)
+  );
+};
+
+DirectedLouvainIndex.prototype.collect =
+  UndirectedLouvainIndex.prototype.collect;
+DirectedLouvainIndex.prototype.assign = UndirectedLouvainIndex.prototype.assign;
+
+DirectedLouvainIndex.prototype[INSPECT] = function () {
+  var proxy = {};
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: DirectedLouvainIndex,
+    enumerable: false
+  });
+
+  proxy.C = this.C;
+  proxy.M = this.M;
+  proxy.E = this.E;
+  proxy.U = this.U;
+  proxy.resolution = this.resolution;
+  proxy.level = this.level;
+  proxy.nodes = this.nodes;
+  proxy.starts = this.starts.slice(0, proxy.C + 1);
+
+  var eTruncated = ['neighborhood', 'weights'];
+  var cTruncated = [
+    'counts',
+    'offsets',
+    'loops',
+    'belongings',
+    'totalInWeights',
+    'totalOutWeights'
+  ];
+
+  var self = this;
+
+  eTruncated.forEach(function (key) {
+    proxy[key] = self[key].slice(0, proxy.E);
+  });
+
+  cTruncated.forEach(function (key) {
+    proxy[key] = self[key].slice(0, proxy.C);
+  });
+
+  proxy.unused = this.unused.slice(0, this.U);
+
+  if (this.keepDendrogram) proxy.dendrogram = this.dendrogram;
+  else proxy.mapping = this.mapping;
+
+  return proxy;
+};
+
+exports.UndirectedLouvainIndex = UndirectedLouvainIndex;
+exports.DirectedLouvainIndex = DirectedLouvainIndex;
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..531999a26ab79bf8c1e7c773dbe741ffd7dc0212
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.d.ts
@@ -0,0 +1,42 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+
+type PointerArray = Uint8Array | Uint16Array | Uint32Array | Float64Array;
+
+type NeighborhoodMethod =
+  | 'in'
+  | 'out'
+  | 'directed'
+  | 'undirected'
+  | 'inbound'
+  | 'outbound';
+
+export class NeighborhoodIndex {
+  constructor(graph: Graph, method?: NeighborhoodMethod);
+
+  graph: Graph;
+  neighborhood: PointerArray;
+  starts: PointerArray;
+  nodes: Array<string>;
+
+  bounds(index: number): [number, number];
+  project(): {[key: string]: Array<string>};
+  collect<T>(results: Array<T>): {[key: string]: T};
+  assign<T>(name: string, results: Array<T>): void;
+}
+
+export class WeightedNeighborhoodIndex<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> extends NeighborhoodIndex {
+  constructor(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    weightAttribute?:
+      | keyof EdgeAttributes
+      | MinimalEdgeMapper<number, EdgeAttributes>,
+    method?: NeighborhoodMethod
+  );
+
+  weights: Float64Array;
+  outDegrees: Float64Array;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.js b/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.js
new file mode 100644
index 0000000000000000000000000000000000000000..e70a4e148c2a62ba996e50c11b127a32bd2200a2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/neighborhood.js
@@ -0,0 +1,163 @@
+/**
+ * Graphology Neighborhood Indices
+ * ================================
+ */
+var typed = require('mnemonist/utils/typed-arrays');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+
+function upperBoundPerMethod(method, graph) {
+  if (method === 'outbound' || method === 'inbound')
+    return graph.directedSize + graph.undirectedSize * 2;
+
+  if (method === 'in' || method === 'out' || method === 'directed')
+    return graph.directedSize;
+
+  return graph.undirectedSize * 2;
+}
+
+function NeighborhoodIndex(graph, method) {
+  method = method || 'outbound';
+  var getNeighbors = graph[method + 'Neighbors'].bind(graph);
+
+  var upperBound = upperBoundPerMethod(method, graph);
+
+  var NeighborhoodPointerArray = typed.getPointerArray(upperBound);
+  var NodesPointerArray = typed.getPointerArray(graph.order);
+
+  // NOTE: directedSize + undirectedSize * 2 is an upper bound for
+  // neighborhood size
+  this.graph = graph;
+  this.neighborhood = new NodesPointerArray(upperBound);
+
+  this.starts = new NeighborhoodPointerArray(graph.order + 1);
+
+  this.nodes = graph.nodes();
+
+  var ids = {};
+
+  var i, l, j, m, node, neighbors;
+
+  var n = 0;
+
+  for (i = 0, l = graph.order; i < l; i++) ids[this.nodes[i]] = i;
+
+  for (i = 0, l = graph.order; i < l; i++) {
+    node = this.nodes[i];
+    neighbors = getNeighbors(node);
+
+    this.starts[i] = n;
+
+    for (j = 0, m = neighbors.length; j < m; j++)
+      this.neighborhood[n++] = ids[neighbors[j]];
+  }
+
+  // NOTE: we keep one more index as upper bound to simplify iteration
+  this.starts[i] = upperBound;
+}
+
+NeighborhoodIndex.prototype.bounds = function (i) {
+  return [this.starts[i], this.starts[i + 1]];
+};
+
+NeighborhoodIndex.prototype.project = function () {
+  var self = this;
+
+  var projection = {};
+
+  self.nodes.forEach(function (node, i) {
+    projection[node] = Array.from(
+      self.neighborhood.slice(self.starts[i], self.starts[i + 1])
+    ).map(function (j) {
+      return self.nodes[j];
+    });
+  });
+
+  return projection;
+};
+
+NeighborhoodIndex.prototype.collect = function (results) {
+  var i, l;
+
+  var o = {};
+
+  for (i = 0, l = results.length; i < l; i++) o[this.nodes[i]] = results[i];
+
+  return o;
+};
+
+NeighborhoodIndex.prototype.assign = function (prop, results) {
+  var i = 0;
+
+  this.graph.updateEachNodeAttributes(
+    function (_, attr) {
+      attr[prop] = results[i++];
+
+      return attr;
+    },
+    {attributes: [prop]}
+  );
+};
+
+exports.NeighborhoodIndex = NeighborhoodIndex;
+
+function WeightedNeighborhoodIndex(graph, getEdgeWeight, method) {
+  method = method || 'outbound';
+  var getEdges = graph[method + 'Edges'].bind(graph);
+
+  var upperBound = upperBoundPerMethod(method, graph);
+
+  var NeighborhoodPointerArray = typed.getPointerArray(upperBound);
+  var NodesPointerArray = typed.getPointerArray(graph.order);
+
+  var weightGetter = createEdgeWeightGetter(getEdgeWeight).fromMinimalEntry;
+
+  // NOTE: directedSize + undirectedSize * 2 is an upper bound for
+  // neighborhood size
+  this.graph = graph;
+  this.neighborhood = new NodesPointerArray(upperBound);
+  this.weights = new Float64Array(upperBound);
+  this.outDegrees = new Float64Array(graph.order);
+
+  this.starts = new NeighborhoodPointerArray(graph.order + 1);
+
+  this.nodes = graph.nodes();
+
+  var ids = {};
+
+  var i, l, j, m, node, neighbor, edges, edge, weight;
+
+  var n = 0;
+
+  for (i = 0, l = graph.order; i < l; i++) ids[this.nodes[i]] = i;
+
+  for (i = 0, l = graph.order; i < l; i++) {
+    node = this.nodes[i];
+    edges = getEdges(node);
+
+    this.starts[i] = n;
+
+    for (j = 0, m = edges.length; j < m; j++) {
+      edge = edges[j];
+      neighbor = graph.opposite(node, edge);
+      weight = weightGetter(edge, graph.getEdgeAttributes(edge));
+
+      // NOTE: for weighted mixed beware of merging weights if twice the same neighbor
+      this.neighborhood[n] = ids[neighbor];
+      this.weights[n++] = weight;
+      this.outDegrees[i] += weight;
+    }
+  }
+
+  // NOTE: we keep one more index as upper bound to simplify iteration
+  this.starts[i] = upperBound;
+}
+
+WeightedNeighborhoodIndex.prototype.bounds = NeighborhoodIndex.prototype.bounds;
+WeightedNeighborhoodIndex.prototype.project =
+  NeighborhoodIndex.prototype.project;
+WeightedNeighborhoodIndex.prototype.collect =
+  NeighborhoodIndex.prototype.collect;
+WeightedNeighborhoodIndex.prototype.assign = NeighborhoodIndex.prototype.assign;
+
+exports.WeightedNeighborhoodIndex = WeightedNeighborhoodIndex;
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/package.json b/libs/shared/graph-layout/node_modules/graphology-indices/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e88740f0c187bfaff5163bcaf632de5341de6b4a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "graphology-indices",
+  "version": "0.16.6",
+  "description": "Miscellaneous indices for graphology.",
+  "main": "index.js",
+  "files": [
+    "*.d.ts",
+    "bfs-queue.js",
+    "dfs-stack.js",
+    "index.js",
+    "louvain.js",
+    "neighborhood.js",
+    "sorted-components.js"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha test"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "indices"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.20.0"
+  },
+  "dependencies": {
+    "graphology-utils": "^2.4.2",
+    "mnemonist": "^0.39.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.d.ts b/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..34e39dfe55474865d12314b99174e4b4985e0564
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.d.ts
@@ -0,0 +1,10 @@
+import Graph from 'graphology-types';
+
+export default class SortedComponentsIndex {
+  orders: Uint32Array;
+  offsets: Uint32Array;
+  nodes: Array<string>;
+  count: number;
+
+  constructor(graph: Graph);
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.js b/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.js
new file mode 100644
index 0000000000000000000000000000000000000000..f894f5b4f412ef99a4c71658c7e53fed2cdc1779
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-indices/sorted-components.js
@@ -0,0 +1,83 @@
+/**
+ * Graphology Sorted Component Index
+ * ==================================
+ *
+ * An index storing the connected components of given graph sorted by order.
+ */
+var DFSStack = require('./dfs-stack.js');
+
+function SortedComponentsIndex(graph) {
+  var orders = [];
+  var offsets = [];
+  var nodes = new Array(graph.order);
+  var count = 0;
+  var n = 0;
+
+  if (!graph.order) return;
+
+  var stack = new DFSStack(graph.order);
+  var push = stack.push.bind(stack);
+
+  var componentOrder;
+
+  // Performing DFS
+  graph.forEachNode(function (node) {
+    if (stack.has(node)) return;
+
+    componentOrder = 0;
+    offsets.push(n);
+    stack.push(node);
+
+    var source;
+
+    while (stack.size !== 0) {
+      source = stack.pop();
+
+      componentOrder += 1;
+      nodes[n++] = source;
+
+      graph.forEachNeighbor(source, push);
+    }
+
+    orders.push(componentOrder);
+    count++;
+  });
+
+  // Sorting by decreasing component order
+  var argsort = new Array(orders.length);
+  var i;
+
+  for (i = 0; i < orders.length; i++) argsort[i] = i;
+
+  function comparator(a, b) {
+    a = orders[a];
+    b = orders[b];
+
+    if (a < b) return 1;
+
+    if (a > b) return -1;
+
+    return 0;
+  }
+
+  argsort.sort(comparator);
+
+  var sortedOrders = new Uint32Array(orders.length);
+  var sortedOffsets = new Uint32Array(orders.length);
+
+  var j;
+
+  for (i = 0; i < argsort.length; i++) {
+    j = argsort[i];
+    sortedOrders[i] = orders[j];
+    sortedOffsets[i] = offsets[j];
+  }
+
+  // Exposed properties
+  this.nodes = nodes;
+  this.orders = sortedOrders;
+  this.offsets = sortedOffsets;
+  this.count = count;
+}
+
+module.exports = SortedComponentsIndex;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/README.md b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..9f87320d0f61a37026c508ae446a1ae11c5ce283
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/README.md
@@ -0,0 +1,113 @@
+# Graphology ForceAtlas2
+
+JavaScript implementation of the [ForceAtlas2](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0098679) algorithm for [graphology](https://graphology.github.io).
+
+## Reference
+
+> Jacomy M, Venturini T, Heymann S, Bastian M (2014) ForceAtlas2, a Continuous Graph Layout Algorithm for Handy Network Visualization Designed for the Gephi Software. PLoS ONE 9(6): e98679. https://doi.org/10.1371/journal.pone.0098679
+
+## Installation
+
+```
+npm install graphology-layout-forceatlas2
+```
+
+## Usage
+
+- [Pre-requisite](#pre-requisite)
+- [Settings](#settings)
+- [Synchronous layout](#synchronous-layout)
+- [Webworker](#webworker)
+- [#.inferSettings](#infersettings)
+
+### Pre-requisites
+
+Each node's starting position must be set before running ForceAtlas 2 layout. Two attributes called `x` and `y` must therefore be defined for all the graph nodes. [graphology-layout](https://github.com/graphology/graphology-layout) can be used to initialize these attributes to a [random](https://github.com/graphology/graphology-layout#random) or [circular](https://github.com/graphology/graphology-layout#circular) layout, if needed.
+
+Note also that the algorithm has an edge-case where the layout cannot be computed if all of your nodes starts with `x=0` and `y=0`.
+
+### Settings
+
+- **adjustSizes** _?boolean_ [`false`]: should the node's sizes be taken into account?
+- **barnesHutOptimize** _?boolean_ [`false`]: whether to use the Barnes-Hut approximation to compute repulsion in `O(n*log(n))` rather than default `O(n^2)`, `n` being the number of nodes.
+- **barnesHutTheta** _?number_ [`0.5`]: Barnes-Hut approximation theta parameter.
+- **edgeWeightInfluence** _?number_ [`1`]: influence of the edge's weights on the layout. To consider edge weight, don't forget to pass `weighted` as `true` when applying the [synchronous layout](#synchronous-layout) or when instantiating the [worker](#webworker).
+- **gravity** _?number_ [`1`]: strength of the layout's gravity.
+- **linLogMode** _?boolean_ [`false`]: whether to use Noack's LinLog model.
+- **outboundAttractionDistribution** _?boolean_ [`false`]
+- **scalingRatio** _?number_ [`1`]
+- **slowDown** _?number_ [`1`]
+- **strongGravityMode** _?boolean_ [`false`]
+
+### Synchronous layout
+
+```js
+import forceAtlas2 from 'graphology-layout-forceatlas2';
+
+const positions = forceAtlas2(graph, {iterations: 50});
+
+// With settings:
+const positions = forceAtlas2(graph, {
+  iterations: 50,
+  settings: {
+    gravity: 10
+  }
+});
+
+// To directly assign the positions to the nodes:
+forceAtlas2.assign(graph);
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _object_: options:
+  - **iterations** _number_: number of iterations to perform.
+  - **attributes** _?object_: an object containing custom attribute name mapping:
+    - **weight** _?string_ [`weight`]: name of the edge weight attribute.
+  - **weighted** _?boolean_ [`false`]: whether to consider edge weight.
+  - **settings** _?object_: the layout's settings (see [#settings](#settings)).
+
+### Webworker
+
+If you need to run the layout's computation in a web worker, the library comes with a utility to do so:
+
+_Example_
+
+```js
+import FA2Layout from 'graphology-layout-forceatlas2/worker';
+
+// The parameters are the same as for the synchronous version, minus `iterations` of course
+const layout = new FA2Layout(graph, {settings: {gravity: 1}, weighted: true});
+
+// To start the layout
+layout.start();
+
+// To stop the layout
+layout.stop();
+
+// To kill the layout and release attached memory
+layout.kill();
+
+// Assess whether the layout is currently running
+layout.isRunning();
+```
+
+**WARNING!**: if you are using [`webpack`](https://webpack.js.org/) to bundle your code, avoid the `cheap-eval`-like options for the [`devtool`](https://webpack.js.org/configuration/devtool/) setting. Some users noticed that it interacts in mysterious ways with the library's code and cause performance to drop dramatically when using the worker. Note that this should have been fixed from v0.5.0.
+
+### #.inferSettings
+
+If you don't know how to tune the layout's settings and want to infer them from your graph, you can use the `#.inferSettings` method:
+
+```js
+import forceAtlas2 from 'graphology-layout-forceatlas2';
+
+const sensibleSettings = forceAtlas2.inferSettings(graph);
+const positions = forceAtlas2(graph, {
+  iterations: 50,
+  settings: sensibleSettings
+});
+
+// Alternatively using the graph's order instead of a graph instance
+const sensibleSettings = forceAtlas2.inferSettings(500);
+```
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/defaults.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/defaults.js
new file mode 100644
index 0000000000000000000000000000000000000000..e4e22893dd27fb7334ee86b9d4b5a299b2e52fba
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/defaults.js
@@ -0,0 +1,16 @@
+/**
+ * Graphology ForceAtlas2 Layout Default Settings
+ * ===============================================
+ */
+module.exports = {
+  linLogMode: false,
+  outboundAttractionDistribution: false,
+  adjustSizes: false,
+  edgeWeightInfluence: 1,
+  scalingRatio: 1,
+  strongGravityMode: false,
+  gravity: 1,
+  slowDown: 1,
+  barnesHutOptimize: false,
+  barnesHutTheta: 0.5
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/helpers.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac847e460e1f2f9eb8c5518be7ae7e97f6296a78
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/helpers.js
@@ -0,0 +1,260 @@
+/**
+ * Graphology ForceAtlas2 Helpers
+ * ===============================
+ *
+ * Miscellaneous helper functions.
+ */
+
+/**
+ * Constants.
+ */
+var PPN = 10;
+var PPE = 3;
+
+/**
+ * Very simple Object.assign-like function.
+ *
+ * @param  {object} target       - First object.
+ * @param  {object} [...objects] - Objects to merge.
+ * @return {object}
+ */
+exports.assign = function (target) {
+  target = target || {};
+
+  var objects = Array.prototype.slice.call(arguments).slice(1),
+    i,
+    k,
+    l;
+
+  for (i = 0, l = objects.length; i < l; i++) {
+    if (!objects[i]) continue;
+
+    for (k in objects[i]) target[k] = objects[i][k];
+  }
+
+  return target;
+};
+
+/**
+ * Function used to validate the given settings.
+ *
+ * @param  {object}      settings - Settings to validate.
+ * @return {object|null}
+ */
+exports.validateSettings = function (settings) {
+  if ('linLogMode' in settings && typeof settings.linLogMode !== 'boolean')
+    return {message: 'the `linLogMode` setting should be a boolean.'};
+
+  if (
+    'outboundAttractionDistribution' in settings &&
+    typeof settings.outboundAttractionDistribution !== 'boolean'
+  )
+    return {
+      message:
+        'the `outboundAttractionDistribution` setting should be a boolean.'
+    };
+
+  if ('adjustSizes' in settings && typeof settings.adjustSizes !== 'boolean')
+    return {message: 'the `adjustSizes` setting should be a boolean.'};
+
+  if (
+    'edgeWeightInfluence' in settings &&
+    typeof settings.edgeWeightInfluence !== 'number'
+  )
+    return {
+      message: 'the `edgeWeightInfluence` setting should be a number.'
+    };
+
+  if (
+    'scalingRatio' in settings &&
+    !(typeof settings.scalingRatio === 'number' && settings.scalingRatio >= 0)
+  )
+    return {message: 'the `scalingRatio` setting should be a number >= 0.'};
+
+  if (
+    'strongGravityMode' in settings &&
+    typeof settings.strongGravityMode !== 'boolean'
+  )
+    return {message: 'the `strongGravityMode` setting should be a boolean.'};
+
+  if (
+    'gravity' in settings &&
+    !(typeof settings.gravity === 'number' && settings.gravity >= 0)
+  )
+    return {message: 'the `gravity` setting should be a number >= 0.'};
+
+  if (
+    'slowDown' in settings &&
+    !(typeof settings.slowDown === 'number' || settings.slowDown >= 0)
+  )
+    return {message: 'the `slowDown` setting should be a number >= 0.'};
+
+  if (
+    'barnesHutOptimize' in settings &&
+    typeof settings.barnesHutOptimize !== 'boolean'
+  )
+    return {message: 'the `barnesHutOptimize` setting should be a boolean.'};
+
+  if (
+    'barnesHutTheta' in settings &&
+    !(
+      typeof settings.barnesHutTheta === 'number' &&
+      settings.barnesHutTheta >= 0
+    )
+  )
+    return {message: 'the `barnesHutTheta` setting should be a number >= 0.'};
+
+  return null;
+};
+
+/**
+ * Function generating a flat matrix for both nodes & edges of the given graph.
+ *
+ * @param  {Graph}       graph           - Target graph.
+ * @param  {string|null} weightAttribute - Name of the edge weight attribute.
+ * @return {object}                      - Both matrices.
+ */
+exports.graphToByteArrays = function (graph, weightAttribute) {
+  var order = graph.order;
+  var size = graph.size;
+  var index = {};
+  var j;
+
+  var NodeMatrix = new Float32Array(order * PPN);
+  var EdgeMatrix = new Float32Array(size * PPE);
+
+  // Iterate through nodes
+  j = 0;
+  graph.forEachNode(function (node, attr) {
+    // Node index
+    index[node] = j;
+
+    // Populating byte array
+    NodeMatrix[j] = attr.x;
+    NodeMatrix[j + 1] = attr.y;
+    NodeMatrix[j + 2] = 0;
+    NodeMatrix[j + 3] = 0;
+    NodeMatrix[j + 4] = 0;
+    NodeMatrix[j + 5] = 0;
+    NodeMatrix[j + 6] = 1 + graph.degree(node);
+    NodeMatrix[j + 7] = 1;
+    NodeMatrix[j + 8] = attr.size || 1;
+    NodeMatrix[j + 9] = attr.fixed ? 1 : 0;
+    j += PPN;
+  });
+
+  // Iterate through edges
+  var weightGetter = function (attr) {
+    if (!weightAttribute) return 1;
+
+    var w = attr[weightAttribute];
+
+    if (typeof w !== 'number' || isNaN(w)) w = 1;
+
+    return w;
+  };
+
+  j = 0;
+  graph.forEachEdge(function (_, attr, source, target) {
+    // Populating byte array
+    EdgeMatrix[j] = index[source];
+    EdgeMatrix[j + 1] = index[target];
+    EdgeMatrix[j + 2] = weightGetter(attr);
+    j += PPE;
+  });
+
+  return {
+    nodes: NodeMatrix,
+    edges: EdgeMatrix
+  };
+};
+
+/**
+ * Function applying the layout back to the graph.
+ *
+ * @param {Graph}         graph         - Target graph.
+ * @param {Float32Array}  NodeMatrix    - Node matrix.
+ * @param {function|null} outputReducer - A node reducer.
+ */
+exports.assignLayoutChanges = function (graph, NodeMatrix, outputReducer) {
+  var i = 0;
+
+  graph.updateEachNodeAttributes(function (node, attr) {
+    attr.x = NodeMatrix[i];
+    attr.y = NodeMatrix[i + 1];
+
+    i += PPN;
+
+    return outputReducer ? outputReducer(node, attr) : attr;
+  });
+};
+
+/**
+ * Function reading the positions (only) from the graph, to write them in the matrix.
+ *
+ * @param {Graph}        graph      - Target graph.
+ * @param {Float32Array} NodeMatrix - Node matrix.
+ */
+exports.readGraphPositions = function (graph, NodeMatrix) {
+  var i = 0;
+
+  graph.forEachNode(function (node, attr) {
+    NodeMatrix[i] = attr.x;
+    NodeMatrix[i + 1] = attr.y;
+
+    i += PPN;
+  });
+};
+
+/**
+ * Function collecting the layout positions.
+ *
+ * @param  {Graph}         graph         - Target graph.
+ * @param  {Float32Array}  NodeMatrix    - Node matrix.
+ * @param  {function|null} outputReducer - A nodes reducer.
+ * @return {object}                      - Map to node positions.
+ */
+exports.collectLayoutChanges = function (graph, NodeMatrix, outputReducer) {
+  var nodes = graph.nodes(),
+    positions = {};
+
+  for (var i = 0, j = 0, l = NodeMatrix.length; i < l; i += PPN) {
+    if (outputReducer) {
+      var newAttr = Object.assign({}, graph.getNodeAttributes(nodes[j]));
+      newAttr.x = NodeMatrix[i];
+      newAttr.y = NodeMatrix[i + 1];
+      newAttr = outputReducer(nodes[j], newAttr);
+      positions[nodes[j]] = {
+        x: newAttr.x,
+        y: newAttr.y
+      };
+    } else {
+      positions[nodes[j]] = {
+        x: NodeMatrix[i],
+        y: NodeMatrix[i + 1]
+      };
+    }
+
+    j++;
+  }
+
+  return positions;
+};
+
+/**
+ * Function returning a web worker from the given function.
+ *
+ * @param  {function}  fn - Function for the worker.
+ * @return {DOMString}
+ */
+exports.createWorker = function createWorker(fn) {
+  var xURL = window.URL || window.webkitURL;
+  var code = fn.toString();
+  var objectUrl = xURL.createObjectURL(
+    new Blob(['(' + code + ').call(this);'], {type: 'text/javascript'})
+  );
+  var worker = new Worker(objectUrl);
+  xURL.revokeObjectURL(objectUrl);
+
+  return worker;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f7988b12b43ba05a56e05c5f31cb5b75246044b3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.d.ts
@@ -0,0 +1,41 @@
+import Graph from 'graphology-types';
+
+type LayoutMapping = {[key: string]: {x: number; y: number}};
+
+export type ForceAtlas2Settings = {
+  linLogMode?: boolean;
+  outboundAttractionDistribution?: boolean;
+  adjustSizes?: boolean;
+  edgeWeightInfluence?: number;
+  scalingRatio?: number;
+  strongGravityMode?: boolean;
+  gravity?: number;
+  slowDown?: number;
+  barnesHutOptimize?: boolean;
+  barnesHutTheta?: number;
+};
+
+export type ForceAtlas2LayoutOptions = {
+  attributes?: {
+    weight?: string;
+  };
+  iterations: number;
+  settings?: ForceAtlas2Settings;
+  weighted?: boolean;
+  outputReducer?: (key: string, attributes: any) => any;
+};
+
+interface IForceAtlas2Layout {
+  (graph: Graph, iterations: number): LayoutMapping;
+  (graph: Graph, options: ForceAtlas2LayoutOptions): LayoutMapping;
+
+  assign(graph: Graph, iterations: number): void;
+  assign(graph: Graph, options: ForceAtlas2LayoutOptions): void;
+
+  inferSettings(order: number): ForceAtlas2Settings;
+  inferSettings(graph: Graph): ForceAtlas2Settings;
+}
+
+declare const forceAtlas2: IForceAtlas2Layout;
+
+export default forceAtlas2;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7507f8ec1a71ea8d6fd3ce956fdb58c297b51bfb
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/index.js
@@ -0,0 +1,105 @@
+/**
+ * Graphology ForceAtlas2 Layout
+ * ==============================
+ *
+ * Library endpoint.
+ */
+var isGraph = require('graphology-utils/is-graph'),
+  iterate = require('./iterate.js'),
+  helpers = require('./helpers.js');
+
+var DEFAULT_SETTINGS = require('./defaults.js');
+
+/**
+ * Asbtract function used to run a certain number of iterations.
+ *
+ * @param  {boolean}       assign          - Whether to assign positions.
+ * @param  {Graph}         graph           - Target graph.
+ * @param  {object|number} params          - If number, params.iterations, else:
+ * @param  {object}          attributes    - Attribute names:
+ * @param  {string}            weight      - Name of the edge weight attribute.
+ * @param  {boolean}         weighted      - Whether to take edge weights into account.
+ * @param  {number}          iterations    - Number of iterations.
+ * @param  {function|null}   outputReducer - A node reducer
+ * @param  {object}          [settings]    - Settings.
+ * @return {object|undefined}
+ */
+function abstractSynchronousLayout(assign, graph, params) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout-forceatlas2: the given graph is not a valid graphology instance.'
+    );
+
+  if (typeof params === 'number') params = {iterations: params};
+
+  var iterations = params.iterations;
+
+  if (typeof iterations !== 'number')
+    throw new Error(
+      'graphology-layout-forceatlas2: invalid number of iterations.'
+    );
+
+  if (iterations <= 0)
+    throw new Error(
+      'graphology-layout-forceatlas2: you should provide a positive number of iterations.'
+    );
+
+  var attributes = params.attributes || {};
+  var weightAttribute = params.weighted ? attributes.weight || 'weight' : null;
+
+  var outputReducer =
+    typeof params.outputReducer === 'function' ? params.outputReducer : null;
+
+  // Validating settings
+  var settings = helpers.assign({}, DEFAULT_SETTINGS, params.settings);
+  var validationError = helpers.validateSettings(settings);
+
+  if (validationError)
+    throw new Error(
+      'graphology-layout-forceatlas2: ' + validationError.message
+    );
+
+  // Building matrices
+  var matrices = helpers.graphToByteArrays(graph, weightAttribute);
+
+  var i;
+
+  // Iterating
+  for (i = 0; i < iterations; i++)
+    iterate(settings, matrices.nodes, matrices.edges);
+
+  // Applying
+  if (assign) {
+    helpers.assignLayoutChanges(graph, matrices.nodes, outputReducer);
+    return;
+  }
+
+  return helpers.collectLayoutChanges(graph, matrices.nodes);
+}
+
+/**
+ * Function returning sane layout settings for the given graph.
+ *
+ * @param  {Graph|number} graph - Target graph or graph order.
+ * @return {object}
+ */
+function inferSettings(graph) {
+  var order = typeof graph === 'number' ? graph : graph.order;
+
+  return {
+    barnesHutOptimize: order > 2000,
+    strongGravityMode: true,
+    gravity: 0.05,
+    scalingRatio: 10,
+    slowDown: 1 + Math.log(order)
+  };
+}
+
+/**
+ * Exporting.
+ */
+var synchronousLayout = abstractSynchronousLayout.bind(null, false);
+synchronousLayout.assign = abstractSynchronousLayout.bind(null, true);
+synchronousLayout.inferSettings = inferSettings;
+
+module.exports = synchronousLayout;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/iterate.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/iterate.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c978a7c19a31458537292b30c5bd517497a3901
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/iterate.js
@@ -0,0 +1,795 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Graphology ForceAtlas2 Iteration
+ * =================================
+ *
+ * Function used to perform a single iteration of the algorithm.
+ */
+
+/**
+ * Matrices properties accessors.
+ */
+var NODE_X = 0;
+var NODE_Y = 1;
+var NODE_DX = 2;
+var NODE_DY = 3;
+var NODE_OLD_DX = 4;
+var NODE_OLD_DY = 5;
+var NODE_MASS = 6;
+var NODE_CONVERGENCE = 7;
+var NODE_SIZE = 8;
+var NODE_FIXED = 9;
+
+var EDGE_SOURCE = 0;
+var EDGE_TARGET = 1;
+var EDGE_WEIGHT = 2;
+
+var REGION_NODE = 0;
+var REGION_CENTER_X = 1;
+var REGION_CENTER_Y = 2;
+var REGION_SIZE = 3;
+var REGION_NEXT_SIBLING = 4;
+var REGION_FIRST_CHILD = 5;
+var REGION_MASS = 6;
+var REGION_MASS_CENTER_X = 7;
+var REGION_MASS_CENTER_Y = 8;
+
+var SUBDIVISION_ATTEMPTS = 3;
+
+/**
+ * Constants.
+ */
+var PPN = 10;
+var PPE = 3;
+var PPR = 9;
+
+var MAX_FORCE = 10;
+
+/**
+ * Function used to perform a single interation of the algorithm.
+ *
+ * @param  {object}       options    - Layout options.
+ * @param  {Float32Array} NodeMatrix - Node data.
+ * @param  {Float32Array} EdgeMatrix - Edge data.
+ * @return {object}                  - Some metadata.
+ */
+module.exports = function iterate(options, NodeMatrix, EdgeMatrix) {
+  // Initializing variables
+  var l, r, n, n1, n2, rn, e, w, g, s;
+
+  var order = NodeMatrix.length,
+    size = EdgeMatrix.length;
+
+  var adjustSizes = options.adjustSizes;
+
+  var thetaSquared = options.barnesHutTheta * options.barnesHutTheta;
+
+  var outboundAttCompensation, coefficient, xDist, yDist, ewc, distance, factor;
+
+  var RegionMatrix = [];
+
+  // 1) Initializing layout data
+  //-----------------------------
+
+  // Resetting positions & computing max values
+  for (n = 0; n < order; n += PPN) {
+    NodeMatrix[n + NODE_OLD_DX] = NodeMatrix[n + NODE_DX];
+    NodeMatrix[n + NODE_OLD_DY] = NodeMatrix[n + NODE_DY];
+    NodeMatrix[n + NODE_DX] = 0;
+    NodeMatrix[n + NODE_DY] = 0;
+  }
+
+  // If outbound attraction distribution, compensate
+  if (options.outboundAttractionDistribution) {
+    outboundAttCompensation = 0;
+    for (n = 0; n < order; n += PPN) {
+      outboundAttCompensation += NodeMatrix[n + NODE_MASS];
+    }
+
+    outboundAttCompensation /= order / PPN;
+  }
+
+  // 1.bis) Barnes-Hut computation
+  //------------------------------
+
+  if (options.barnesHutOptimize) {
+    // Setting up
+    var minX = Infinity,
+      maxX = -Infinity,
+      minY = Infinity,
+      maxY = -Infinity,
+      q,
+      q2,
+      subdivisionAttempts;
+
+    // Computing min and max values
+    for (n = 0; n < order; n += PPN) {
+      minX = Math.min(minX, NodeMatrix[n + NODE_X]);
+      maxX = Math.max(maxX, NodeMatrix[n + NODE_X]);
+      minY = Math.min(minY, NodeMatrix[n + NODE_Y]);
+      maxY = Math.max(maxY, NodeMatrix[n + NODE_Y]);
+    }
+
+    // squarify bounds, it's a quadtree
+    var dx = maxX - minX,
+      dy = maxY - minY;
+    if (dx > dy) {
+      minY -= (dx - dy) / 2;
+      maxY = minY + dx;
+    } else {
+      minX -= (dy - dx) / 2;
+      maxX = minX + dy;
+    }
+
+    // Build the Barnes Hut root region
+    RegionMatrix[0 + REGION_NODE] = -1;
+    RegionMatrix[0 + REGION_CENTER_X] = (minX + maxX) / 2;
+    RegionMatrix[0 + REGION_CENTER_Y] = (minY + maxY) / 2;
+    RegionMatrix[0 + REGION_SIZE] = Math.max(maxX - minX, maxY - minY);
+    RegionMatrix[0 + REGION_NEXT_SIBLING] = -1;
+    RegionMatrix[0 + REGION_FIRST_CHILD] = -1;
+    RegionMatrix[0 + REGION_MASS] = 0;
+    RegionMatrix[0 + REGION_MASS_CENTER_X] = 0;
+    RegionMatrix[0 + REGION_MASS_CENTER_Y] = 0;
+
+    // Add each node in the tree
+    l = 1;
+    for (n = 0; n < order; n += PPN) {
+      // Current region, starting with root
+      r = 0;
+      subdivisionAttempts = SUBDIVISION_ATTEMPTS;
+
+      while (true) {
+        // Are there sub-regions?
+
+        // We look at first child index
+        if (RegionMatrix[r + REGION_FIRST_CHILD] >= 0) {
+          // There are sub-regions
+
+          // We just iterate to find a "leaf" of the tree
+          // that is an empty region or a region with a single node
+          // (see next case)
+
+          // Find the quadrant of n
+          if (NodeMatrix[n + NODE_X] < RegionMatrix[r + REGION_CENTER_X]) {
+            if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+              // Top Left quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD];
+            } else {
+              // Bottom Left quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+            }
+          } else {
+            if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+              // Top Right quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+            } else {
+              // Bottom Right quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+            }
+          }
+
+          // Update center of mass and mass (we only do it for non-leave regions)
+          RegionMatrix[r + REGION_MASS_CENTER_X] =
+            (RegionMatrix[r + REGION_MASS_CENTER_X] *
+              RegionMatrix[r + REGION_MASS] +
+              NodeMatrix[n + NODE_X] * NodeMatrix[n + NODE_MASS]) /
+            (RegionMatrix[r + REGION_MASS] + NodeMatrix[n + NODE_MASS]);
+
+          RegionMatrix[r + REGION_MASS_CENTER_Y] =
+            (RegionMatrix[r + REGION_MASS_CENTER_Y] *
+              RegionMatrix[r + REGION_MASS] +
+              NodeMatrix[n + NODE_Y] * NodeMatrix[n + NODE_MASS]) /
+            (RegionMatrix[r + REGION_MASS] + NodeMatrix[n + NODE_MASS]);
+
+          RegionMatrix[r + REGION_MASS] += NodeMatrix[n + NODE_MASS];
+
+          // Iterate on the right quadrant
+          r = q;
+          continue;
+        } else {
+          // There are no sub-regions: we are in a "leaf"
+
+          // Is there a node in this leave?
+          if (RegionMatrix[r + REGION_NODE] < 0) {
+            // There is no node in region:
+            // we record node n and go on
+            RegionMatrix[r + REGION_NODE] = n;
+            break;
+          } else {
+            // There is a node in this region
+
+            // We will need to create sub-regions, stick the two
+            // nodes (the old one r[0] and the new one n) in two
+            // subregions. If they fall in the same quadrant,
+            // we will iterate.
+
+            // Create sub-regions
+            RegionMatrix[r + REGION_FIRST_CHILD] = l * PPR;
+            w = RegionMatrix[r + REGION_SIZE] / 2; // new size (half)
+
+            // NOTE: we use screen coordinates
+            // from Top Left to Bottom Right
+
+            // Top Left sub-region
+            g = RegionMatrix[r + REGION_FIRST_CHILD];
+
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] - w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] - w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Bottom Left sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] - w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] + w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Top Right sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] + w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] - w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Bottom Right sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] + w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] + w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] =
+              RegionMatrix[r + REGION_NEXT_SIBLING];
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            l += 4;
+
+            // Now the goal is to find two different sub-regions
+            // for the two nodes: the one previously recorded (r[0])
+            // and the one we want to add (n)
+
+            // Find the quadrant of the old node
+            if (
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_X] <
+              RegionMatrix[r + REGION_CENTER_X]
+            ) {
+              if (
+                NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y] <
+                RegionMatrix[r + REGION_CENTER_Y]
+              ) {
+                // Top Left quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD];
+              } else {
+                // Bottom Left quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+              }
+            } else {
+              if (
+                NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y] <
+                RegionMatrix[r + REGION_CENTER_Y]
+              ) {
+                // Top Right quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+              } else {
+                // Bottom Right quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+              }
+            }
+
+            // We remove r[0] from the region r, add its mass to r and record it in q
+            RegionMatrix[r + REGION_MASS] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_MASS];
+            RegionMatrix[r + REGION_MASS_CENTER_X] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_X];
+            RegionMatrix[r + REGION_MASS_CENTER_Y] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y];
+
+            RegionMatrix[q + REGION_NODE] = RegionMatrix[r + REGION_NODE];
+            RegionMatrix[r + REGION_NODE] = -1;
+
+            // Find the quadrant of n
+            if (NodeMatrix[n + NODE_X] < RegionMatrix[r + REGION_CENTER_X]) {
+              if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+                // Top Left quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD];
+              } else {
+                // Bottom Left quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+              }
+            } else {
+              if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+                // Top Right quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+              } else {
+                // Bottom Right quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+              }
+            }
+
+            if (q === q2) {
+              // If both nodes are in the same quadrant,
+              // we have to try it again on this quadrant
+              if (subdivisionAttempts--) {
+                r = q;
+                continue; // while
+              } else {
+                // we are out of precision here, and we cannot subdivide anymore
+                // but we have to break the loop anyway
+                subdivisionAttempts = SUBDIVISION_ATTEMPTS;
+                break; // while
+              }
+            }
+
+            // If both quadrants are different, we record n
+            // in its quadrant
+            RegionMatrix[q2 + REGION_NODE] = n;
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  // 2) Repulsion
+  //--------------
+  // NOTES: adjustSizes = antiCollision & scalingRatio = coefficient
+
+  if (options.barnesHutOptimize) {
+    coefficient = options.scalingRatio;
+
+    // Applying repulsion through regions
+    for (n = 0; n < order; n += PPN) {
+      // Computing leaf quad nodes iteration
+
+      r = 0; // Starting with root region
+      while (true) {
+        if (RegionMatrix[r + REGION_FIRST_CHILD] >= 0) {
+          // The region has sub-regions
+
+          // We run the Barnes Hut test to see if we are at the right distance
+          distance =
+            Math.pow(
+              NodeMatrix[n + NODE_X] - RegionMatrix[r + REGION_MASS_CENTER_X],
+              2
+            ) +
+            Math.pow(
+              NodeMatrix[n + NODE_Y] - RegionMatrix[r + REGION_MASS_CENTER_Y],
+              2
+            );
+
+          s = RegionMatrix[r + REGION_SIZE];
+
+          if ((4 * s * s) / distance < thetaSquared) {
+            // We treat the region as a single body, and we repulse
+
+            xDist =
+              NodeMatrix[n + NODE_X] - RegionMatrix[r + REGION_MASS_CENTER_X];
+            yDist =
+              NodeMatrix[n + NODE_Y] - RegionMatrix[r + REGION_MASS_CENTER_Y];
+
+            if (adjustSizes === true) {
+              //-- Linear Anti-collision Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              } else if (distance < 0) {
+                factor =
+                  (-coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  Math.sqrt(distance);
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            } else {
+              //-- Linear Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            }
+
+            // When this is done, we iterate. We have to look at the next sibling.
+            r = RegionMatrix[r + REGION_NEXT_SIBLING];
+            if (r < 0) break; // No next sibling: we have finished the tree
+
+            continue;
+          } else {
+            // The region is too close and we have to look at sub-regions
+            r = RegionMatrix[r + REGION_FIRST_CHILD];
+            continue;
+          }
+        } else {
+          // The region has no sub-region
+          // If there is a node r[0] and it is not n, then repulse
+          rn = RegionMatrix[r + REGION_NODE];
+
+          if (rn >= 0 && rn !== n) {
+            xDist = NodeMatrix[n + NODE_X] - NodeMatrix[rn + NODE_X];
+            yDist = NodeMatrix[n + NODE_Y] - NodeMatrix[rn + NODE_Y];
+
+            distance = xDist * xDist + yDist * yDist;
+
+            if (adjustSizes === true) {
+              //-- Linear Anti-collision Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              } else if (distance < 0) {
+                factor =
+                  (-coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  Math.sqrt(distance);
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            } else {
+              //-- Linear Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            }
+          }
+
+          // When this is done, we iterate. We have to look at the next sibling.
+          r = RegionMatrix[r + REGION_NEXT_SIBLING];
+
+          if (r < 0) break; // No next sibling: we have finished the tree
+
+          continue;
+        }
+      }
+    }
+  } else {
+    coefficient = options.scalingRatio;
+
+    // Square iteration
+    for (n1 = 0; n1 < order; n1 += PPN) {
+      for (n2 = 0; n2 < n1; n2 += PPN) {
+        // Common to both methods
+        xDist = NodeMatrix[n1 + NODE_X] - NodeMatrix[n2 + NODE_X];
+        yDist = NodeMatrix[n1 + NODE_Y] - NodeMatrix[n2 + NODE_Y];
+
+        if (adjustSizes === true) {
+          //-- Anticollision Linear Repulsion
+          distance =
+            Math.sqrt(xDist * xDist + yDist * yDist) -
+            NodeMatrix[n1 + NODE_SIZE] -
+            NodeMatrix[n2 + NODE_SIZE];
+
+          if (distance > 0) {
+            factor =
+              (coefficient *
+                NodeMatrix[n1 + NODE_MASS] *
+                NodeMatrix[n2 + NODE_MASS]) /
+              distance /
+              distance;
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] += xDist * factor;
+            NodeMatrix[n2 + NODE_DY] += yDist * factor;
+          } else if (distance < 0) {
+            factor =
+              100 *
+              coefficient *
+              NodeMatrix[n1 + NODE_MASS] *
+              NodeMatrix[n2 + NODE_MASS];
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+            NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+          }
+        } else {
+          //-- Linear Repulsion
+          distance = Math.sqrt(xDist * xDist + yDist * yDist);
+
+          if (distance > 0) {
+            factor =
+              (coefficient *
+                NodeMatrix[n1 + NODE_MASS] *
+                NodeMatrix[n2 + NODE_MASS]) /
+              distance /
+              distance;
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+            NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+          }
+        }
+      }
+    }
+  }
+
+  // 3) Gravity
+  //------------
+  g = options.gravity / options.scalingRatio;
+  coefficient = options.scalingRatio;
+  for (n = 0; n < order; n += PPN) {
+    factor = 0;
+
+    // Common to both methods
+    xDist = NodeMatrix[n + NODE_X];
+    yDist = NodeMatrix[n + NODE_Y];
+    distance = Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
+
+    if (options.strongGravityMode) {
+      //-- Strong gravity
+      if (distance > 0) factor = coefficient * NodeMatrix[n + NODE_MASS] * g;
+    } else {
+      //-- Linear Anti-collision Repulsion n
+      if (distance > 0)
+        factor = (coefficient * NodeMatrix[n + NODE_MASS] * g) / distance;
+    }
+
+    // Updating node's dx and dy
+    NodeMatrix[n + NODE_DX] -= xDist * factor;
+    NodeMatrix[n + NODE_DY] -= yDist * factor;
+  }
+
+  // 4) Attraction
+  //---------------
+  coefficient =
+    1 * (options.outboundAttractionDistribution ? outboundAttCompensation : 1);
+
+  // TODO: simplify distance
+  // TODO: coefficient is always used as -c --> optimize?
+  for (e = 0; e < size; e += PPE) {
+    n1 = EdgeMatrix[e + EDGE_SOURCE];
+    n2 = EdgeMatrix[e + EDGE_TARGET];
+    w = EdgeMatrix[e + EDGE_WEIGHT];
+
+    // Edge weight influence
+    ewc = Math.pow(w, options.edgeWeightInfluence);
+
+    // Common measures
+    xDist = NodeMatrix[n1 + NODE_X] - NodeMatrix[n2 + NODE_X];
+    yDist = NodeMatrix[n1 + NODE_Y] - NodeMatrix[n2 + NODE_Y];
+
+    // Applying attraction to nodes
+    if (adjustSizes === true) {
+      distance = Math.sqrt(
+        Math.pow(xDist, 2) +
+          Math.pow(yDist, 2) -
+          NodeMatrix[n1 + NODE_SIZE] -
+          NodeMatrix[n2 + NODE_SIZE]
+      );
+
+      if (options.linLogMode) {
+        if (options.outboundAttractionDistribution) {
+          //-- LinLog Degree Distributed Anti-collision Attraction
+          if (distance > 0) {
+            factor =
+              (-coefficient * ewc * Math.log(1 + distance)) /
+              distance /
+              NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- LinLog Anti-collision Attraction
+          if (distance > 0) {
+            factor = (-coefficient * ewc * Math.log(1 + distance)) / distance;
+          }
+        }
+      } else {
+        if (options.outboundAttractionDistribution) {
+          //-- Linear Degree Distributed Anti-collision Attraction
+          if (distance > 0) {
+            factor = (-coefficient * ewc) / NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- Linear Anti-collision Attraction
+          if (distance > 0) {
+            factor = -coefficient * ewc;
+          }
+        }
+      }
+    } else {
+      distance = Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
+
+      if (options.linLogMode) {
+        if (options.outboundAttractionDistribution) {
+          //-- LinLog Degree Distributed Attraction
+          if (distance > 0) {
+            factor =
+              (-coefficient * ewc * Math.log(1 + distance)) /
+              distance /
+              NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- LinLog Attraction
+          if (distance > 0)
+            factor = (-coefficient * ewc * Math.log(1 + distance)) / distance;
+        }
+      } else {
+        if (options.outboundAttractionDistribution) {
+          //-- Linear Attraction Mass Distributed
+          // NOTE: Distance is set to 1 to override next condition
+          distance = 1;
+          factor = (-coefficient * ewc) / NodeMatrix[n1 + NODE_MASS];
+        } else {
+          //-- Linear Attraction
+          // NOTE: Distance is set to 1 to override next condition
+          distance = 1;
+          factor = -coefficient * ewc;
+        }
+      }
+    }
+
+    // Updating nodes' dx and dy
+    // TODO: if condition or factor = 1?
+    if (distance > 0) {
+      // Updating nodes' dx and dy
+      NodeMatrix[n1 + NODE_DX] += xDist * factor;
+      NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+      NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+      NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+    }
+  }
+
+  // 5) Apply Forces
+  //-----------------
+  var force, swinging, traction, nodespeed, newX, newY;
+
+  // MATH: sqrt and square distances
+  if (adjustSizes === true) {
+    for (n = 0; n < order; n += PPN) {
+      if (NodeMatrix[n + NODE_FIXED] !== 1) {
+        force = Math.sqrt(
+          Math.pow(NodeMatrix[n + NODE_DX], 2) +
+            Math.pow(NodeMatrix[n + NODE_DY], 2)
+        );
+
+        if (force > MAX_FORCE) {
+          NodeMatrix[n + NODE_DX] =
+            (NodeMatrix[n + NODE_DX] * MAX_FORCE) / force;
+          NodeMatrix[n + NODE_DY] =
+            (NodeMatrix[n + NODE_DY] * MAX_FORCE) / force;
+        }
+
+        swinging =
+          NodeMatrix[n + NODE_MASS] *
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY])
+          );
+
+        traction =
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY])
+          ) / 2;
+
+        nodespeed = (0.1 * Math.log(1 + traction)) / (1 + Math.sqrt(swinging));
+
+        // Updating node's positon
+        newX =
+          NodeMatrix[n + NODE_X] +
+          NodeMatrix[n + NODE_DX] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_X] = newX;
+
+        newY =
+          NodeMatrix[n + NODE_Y] +
+          NodeMatrix[n + NODE_DY] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_Y] = newY;
+      }
+    }
+  } else {
+    for (n = 0; n < order; n += PPN) {
+      if (NodeMatrix[n + NODE_FIXED] !== 1) {
+        swinging =
+          NodeMatrix[n + NODE_MASS] *
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY])
+          );
+
+        traction =
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY])
+          ) / 2;
+
+        nodespeed =
+          (NodeMatrix[n + NODE_CONVERGENCE] * Math.log(1 + traction)) /
+          (1 + Math.sqrt(swinging));
+
+        // Updating node convergence
+        NodeMatrix[n + NODE_CONVERGENCE] = Math.min(
+          1,
+          Math.sqrt(
+            (nodespeed *
+              (Math.pow(NodeMatrix[n + NODE_DX], 2) +
+                Math.pow(NodeMatrix[n + NODE_DY], 2))) /
+              (1 + Math.sqrt(swinging))
+          )
+        );
+
+        // Updating node's positon
+        newX =
+          NodeMatrix[n + NODE_X] +
+          NodeMatrix[n + NODE_DX] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_X] = newX;
+
+        newY =
+          NodeMatrix[n + NODE_Y] +
+          NodeMatrix[n + NODE_DY] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_Y] = newY;
+      }
+    }
+  }
+
+  // We return the information about the layout (no need to return the matrices)
+  return {};
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/package.json b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e848964190cae3aff004bda629a036c327140e57
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "graphology-layout-forceatlas2",
+  "version": "0.8.2",
+  "description": "ForceAtlas 2 layout algorithm for graphology.",
+  "main": "index.js",
+  "files": [
+    "*.d.ts",
+    "defaults.js",
+    "helpers.js",
+    "index.js",
+    "iterate.js",
+    "worker.js",
+    "webworker.js"
+  ],
+  "scripts": {
+    "bench": "node bench.js",
+    "clean": "rimraf webworker.js",
+    "prepublishOnly": "npm run clean && npm test && npm run template",
+    "template": "node ./scripts/template-webworker.js > webworker.js",
+    "test": "mocha test.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "layout",
+    "force atlas"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.19.0"
+  },
+  "dependencies": {
+    "graphology-utils": "^2.1.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/webworker.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/webworker.js
new file mode 100644
index 0000000000000000000000000000000000000000..c88cc257f00092b39638ca262dc11bb2d841e4e3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/webworker.js
@@ -0,0 +1,832 @@
+/**
+ * Graphology ForceAtlas2 Layout Webworker
+ * ========================================
+ *
+ * Web worker able to run the layout in a separate thread.
+ */
+module.exports = function worker() {
+  var NODES, EDGES;
+
+  var moduleShim = {};
+
+  (function () {
+    /* eslint no-constant-condition: 0 */
+/**
+ * Graphology ForceAtlas2 Iteration
+ * =================================
+ *
+ * Function used to perform a single iteration of the algorithm.
+ */
+
+/**
+ * Matrices properties accessors.
+ */
+var NODE_X = 0;
+var NODE_Y = 1;
+var NODE_DX = 2;
+var NODE_DY = 3;
+var NODE_OLD_DX = 4;
+var NODE_OLD_DY = 5;
+var NODE_MASS = 6;
+var NODE_CONVERGENCE = 7;
+var NODE_SIZE = 8;
+var NODE_FIXED = 9;
+
+var EDGE_SOURCE = 0;
+var EDGE_TARGET = 1;
+var EDGE_WEIGHT = 2;
+
+var REGION_NODE = 0;
+var REGION_CENTER_X = 1;
+var REGION_CENTER_Y = 2;
+var REGION_SIZE = 3;
+var REGION_NEXT_SIBLING = 4;
+var REGION_FIRST_CHILD = 5;
+var REGION_MASS = 6;
+var REGION_MASS_CENTER_X = 7;
+var REGION_MASS_CENTER_Y = 8;
+
+var SUBDIVISION_ATTEMPTS = 3;
+
+/**
+ * Constants.
+ */
+var PPN = 10;
+var PPE = 3;
+var PPR = 9;
+
+var MAX_FORCE = 10;
+
+/**
+ * Function used to perform a single interation of the algorithm.
+ *
+ * @param  {object}       options    - Layout options.
+ * @param  {Float32Array} NodeMatrix - Node data.
+ * @param  {Float32Array} EdgeMatrix - Edge data.
+ * @return {object}                  - Some metadata.
+ */
+moduleShim.exports = function iterate(options, NodeMatrix, EdgeMatrix) {
+  // Initializing variables
+  var l, r, n, n1, n2, rn, e, w, g, s;
+
+  var order = NodeMatrix.length,
+    size = EdgeMatrix.length;
+
+  var adjustSizes = options.adjustSizes;
+
+  var thetaSquared = options.barnesHutTheta * options.barnesHutTheta;
+
+  var outboundAttCompensation, coefficient, xDist, yDist, ewc, distance, factor;
+
+  var RegionMatrix = [];
+
+  // 1) Initializing layout data
+  //-----------------------------
+
+  // Resetting positions & computing max values
+  for (n = 0; n < order; n += PPN) {
+    NodeMatrix[n + NODE_OLD_DX] = NodeMatrix[n + NODE_DX];
+    NodeMatrix[n + NODE_OLD_DY] = NodeMatrix[n + NODE_DY];
+    NodeMatrix[n + NODE_DX] = 0;
+    NodeMatrix[n + NODE_DY] = 0;
+  }
+
+  // If outbound attraction distribution, compensate
+  if (options.outboundAttractionDistribution) {
+    outboundAttCompensation = 0;
+    for (n = 0; n < order; n += PPN) {
+      outboundAttCompensation += NodeMatrix[n + NODE_MASS];
+    }
+
+    outboundAttCompensation /= order / PPN;
+  }
+
+  // 1.bis) Barnes-Hut computation
+  //------------------------------
+
+  if (options.barnesHutOptimize) {
+    // Setting up
+    var minX = Infinity,
+      maxX = -Infinity,
+      minY = Infinity,
+      maxY = -Infinity,
+      q,
+      q2,
+      subdivisionAttempts;
+
+    // Computing min and max values
+    for (n = 0; n < order; n += PPN) {
+      minX = Math.min(minX, NodeMatrix[n + NODE_X]);
+      maxX = Math.max(maxX, NodeMatrix[n + NODE_X]);
+      minY = Math.min(minY, NodeMatrix[n + NODE_Y]);
+      maxY = Math.max(maxY, NodeMatrix[n + NODE_Y]);
+    }
+
+    // squarify bounds, it's a quadtree
+    var dx = maxX - minX,
+      dy = maxY - minY;
+    if (dx > dy) {
+      minY -= (dx - dy) / 2;
+      maxY = minY + dx;
+    } else {
+      minX -= (dy - dx) / 2;
+      maxX = minX + dy;
+    }
+
+    // Build the Barnes Hut root region
+    RegionMatrix[0 + REGION_NODE] = -1;
+    RegionMatrix[0 + REGION_CENTER_X] = (minX + maxX) / 2;
+    RegionMatrix[0 + REGION_CENTER_Y] = (minY + maxY) / 2;
+    RegionMatrix[0 + REGION_SIZE] = Math.max(maxX - minX, maxY - minY);
+    RegionMatrix[0 + REGION_NEXT_SIBLING] = -1;
+    RegionMatrix[0 + REGION_FIRST_CHILD] = -1;
+    RegionMatrix[0 + REGION_MASS] = 0;
+    RegionMatrix[0 + REGION_MASS_CENTER_X] = 0;
+    RegionMatrix[0 + REGION_MASS_CENTER_Y] = 0;
+
+    // Add each node in the tree
+    l = 1;
+    for (n = 0; n < order; n += PPN) {
+      // Current region, starting with root
+      r = 0;
+      subdivisionAttempts = SUBDIVISION_ATTEMPTS;
+
+      while (true) {
+        // Are there sub-regions?
+
+        // We look at first child index
+        if (RegionMatrix[r + REGION_FIRST_CHILD] >= 0) {
+          // There are sub-regions
+
+          // We just iterate to find a "leaf" of the tree
+          // that is an empty region or a region with a single node
+          // (see next case)
+
+          // Find the quadrant of n
+          if (NodeMatrix[n + NODE_X] < RegionMatrix[r + REGION_CENTER_X]) {
+            if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+              // Top Left quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD];
+            } else {
+              // Bottom Left quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+            }
+          } else {
+            if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+              // Top Right quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+            } else {
+              // Bottom Right quarter
+              q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+            }
+          }
+
+          // Update center of mass and mass (we only do it for non-leave regions)
+          RegionMatrix[r + REGION_MASS_CENTER_X] =
+            (RegionMatrix[r + REGION_MASS_CENTER_X] *
+              RegionMatrix[r + REGION_MASS] +
+              NodeMatrix[n + NODE_X] * NodeMatrix[n + NODE_MASS]) /
+            (RegionMatrix[r + REGION_MASS] + NodeMatrix[n + NODE_MASS]);
+
+          RegionMatrix[r + REGION_MASS_CENTER_Y] =
+            (RegionMatrix[r + REGION_MASS_CENTER_Y] *
+              RegionMatrix[r + REGION_MASS] +
+              NodeMatrix[n + NODE_Y] * NodeMatrix[n + NODE_MASS]) /
+            (RegionMatrix[r + REGION_MASS] + NodeMatrix[n + NODE_MASS]);
+
+          RegionMatrix[r + REGION_MASS] += NodeMatrix[n + NODE_MASS];
+
+          // Iterate on the right quadrant
+          r = q;
+          continue;
+        } else {
+          // There are no sub-regions: we are in a "leaf"
+
+          // Is there a node in this leave?
+          if (RegionMatrix[r + REGION_NODE] < 0) {
+            // There is no node in region:
+            // we record node n and go on
+            RegionMatrix[r + REGION_NODE] = n;
+            break;
+          } else {
+            // There is a node in this region
+
+            // We will need to create sub-regions, stick the two
+            // nodes (the old one r[0] and the new one n) in two
+            // subregions. If they fall in the same quadrant,
+            // we will iterate.
+
+            // Create sub-regions
+            RegionMatrix[r + REGION_FIRST_CHILD] = l * PPR;
+            w = RegionMatrix[r + REGION_SIZE] / 2; // new size (half)
+
+            // NOTE: we use screen coordinates
+            // from Top Left to Bottom Right
+
+            // Top Left sub-region
+            g = RegionMatrix[r + REGION_FIRST_CHILD];
+
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] - w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] - w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Bottom Left sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] - w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] + w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Top Right sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] + w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] - w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] = g + PPR;
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            // Bottom Right sub-region
+            g += PPR;
+            RegionMatrix[g + REGION_NODE] = -1;
+            RegionMatrix[g + REGION_CENTER_X] =
+              RegionMatrix[r + REGION_CENTER_X] + w;
+            RegionMatrix[g + REGION_CENTER_Y] =
+              RegionMatrix[r + REGION_CENTER_Y] + w;
+            RegionMatrix[g + REGION_SIZE] = w;
+            RegionMatrix[g + REGION_NEXT_SIBLING] =
+              RegionMatrix[r + REGION_NEXT_SIBLING];
+            RegionMatrix[g + REGION_FIRST_CHILD] = -1;
+            RegionMatrix[g + REGION_MASS] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_X] = 0;
+            RegionMatrix[g + REGION_MASS_CENTER_Y] = 0;
+
+            l += 4;
+
+            // Now the goal is to find two different sub-regions
+            // for the two nodes: the one previously recorded (r[0])
+            // and the one we want to add (n)
+
+            // Find the quadrant of the old node
+            if (
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_X] <
+              RegionMatrix[r + REGION_CENTER_X]
+            ) {
+              if (
+                NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y] <
+                RegionMatrix[r + REGION_CENTER_Y]
+              ) {
+                // Top Left quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD];
+              } else {
+                // Bottom Left quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+              }
+            } else {
+              if (
+                NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y] <
+                RegionMatrix[r + REGION_CENTER_Y]
+              ) {
+                // Top Right quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+              } else {
+                // Bottom Right quarter
+                q = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+              }
+            }
+
+            // We remove r[0] from the region r, add its mass to r and record it in q
+            RegionMatrix[r + REGION_MASS] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_MASS];
+            RegionMatrix[r + REGION_MASS_CENTER_X] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_X];
+            RegionMatrix[r + REGION_MASS_CENTER_Y] =
+              NodeMatrix[RegionMatrix[r + REGION_NODE] + NODE_Y];
+
+            RegionMatrix[q + REGION_NODE] = RegionMatrix[r + REGION_NODE];
+            RegionMatrix[r + REGION_NODE] = -1;
+
+            // Find the quadrant of n
+            if (NodeMatrix[n + NODE_X] < RegionMatrix[r + REGION_CENTER_X]) {
+              if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+                // Top Left quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD];
+              } else {
+                // Bottom Left quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR;
+              }
+            } else {
+              if (NodeMatrix[n + NODE_Y] < RegionMatrix[r + REGION_CENTER_Y]) {
+                // Top Right quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 2;
+              } else {
+                // Bottom Right quarter
+                q2 = RegionMatrix[r + REGION_FIRST_CHILD] + PPR * 3;
+              }
+            }
+
+            if (q === q2) {
+              // If both nodes are in the same quadrant,
+              // we have to try it again on this quadrant
+              if (subdivisionAttempts--) {
+                r = q;
+                continue; // while
+              } else {
+                // we are out of precision here, and we cannot subdivide anymore
+                // but we have to break the loop anyway
+                subdivisionAttempts = SUBDIVISION_ATTEMPTS;
+                break; // while
+              }
+            }
+
+            // If both quadrants are different, we record n
+            // in its quadrant
+            RegionMatrix[q2 + REGION_NODE] = n;
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  // 2) Repulsion
+  //--------------
+  // NOTES: adjustSizes = antiCollision & scalingRatio = coefficient
+
+  if (options.barnesHutOptimize) {
+    coefficient = options.scalingRatio;
+
+    // Applying repulsion through regions
+    for (n = 0; n < order; n += PPN) {
+      // Computing leaf quad nodes iteration
+
+      r = 0; // Starting with root region
+      while (true) {
+        if (RegionMatrix[r + REGION_FIRST_CHILD] >= 0) {
+          // The region has sub-regions
+
+          // We run the Barnes Hut test to see if we are at the right distance
+          distance =
+            Math.pow(
+              NodeMatrix[n + NODE_X] - RegionMatrix[r + REGION_MASS_CENTER_X],
+              2
+            ) +
+            Math.pow(
+              NodeMatrix[n + NODE_Y] - RegionMatrix[r + REGION_MASS_CENTER_Y],
+              2
+            );
+
+          s = RegionMatrix[r + REGION_SIZE];
+
+          if ((4 * s * s) / distance < thetaSquared) {
+            // We treat the region as a single body, and we repulse
+
+            xDist =
+              NodeMatrix[n + NODE_X] - RegionMatrix[r + REGION_MASS_CENTER_X];
+            yDist =
+              NodeMatrix[n + NODE_Y] - RegionMatrix[r + REGION_MASS_CENTER_Y];
+
+            if (adjustSizes === true) {
+              //-- Linear Anti-collision Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              } else if (distance < 0) {
+                factor =
+                  (-coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  Math.sqrt(distance);
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            } else {
+              //-- Linear Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    RegionMatrix[r + REGION_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            }
+
+            // When this is done, we iterate. We have to look at the next sibling.
+            r = RegionMatrix[r + REGION_NEXT_SIBLING];
+            if (r < 0) break; // No next sibling: we have finished the tree
+
+            continue;
+          } else {
+            // The region is too close and we have to look at sub-regions
+            r = RegionMatrix[r + REGION_FIRST_CHILD];
+            continue;
+          }
+        } else {
+          // The region has no sub-region
+          // If there is a node r[0] and it is not n, then repulse
+          rn = RegionMatrix[r + REGION_NODE];
+
+          if (rn >= 0 && rn !== n) {
+            xDist = NodeMatrix[n + NODE_X] - NodeMatrix[rn + NODE_X];
+            yDist = NodeMatrix[n + NODE_Y] - NodeMatrix[rn + NODE_Y];
+
+            distance = xDist * xDist + yDist * yDist;
+
+            if (adjustSizes === true) {
+              //-- Linear Anti-collision Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              } else if (distance < 0) {
+                factor =
+                  (-coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  Math.sqrt(distance);
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            } else {
+              //-- Linear Repulsion
+              if (distance > 0) {
+                factor =
+                  (coefficient *
+                    NodeMatrix[n + NODE_MASS] *
+                    NodeMatrix[rn + NODE_MASS]) /
+                  distance;
+
+                NodeMatrix[n + NODE_DX] += xDist * factor;
+                NodeMatrix[n + NODE_DY] += yDist * factor;
+              }
+            }
+          }
+
+          // When this is done, we iterate. We have to look at the next sibling.
+          r = RegionMatrix[r + REGION_NEXT_SIBLING];
+
+          if (r < 0) break; // No next sibling: we have finished the tree
+
+          continue;
+        }
+      }
+    }
+  } else {
+    coefficient = options.scalingRatio;
+
+    // Square iteration
+    for (n1 = 0; n1 < order; n1 += PPN) {
+      for (n2 = 0; n2 < n1; n2 += PPN) {
+        // Common to both methods
+        xDist = NodeMatrix[n1 + NODE_X] - NodeMatrix[n2 + NODE_X];
+        yDist = NodeMatrix[n1 + NODE_Y] - NodeMatrix[n2 + NODE_Y];
+
+        if (adjustSizes === true) {
+          //-- Anticollision Linear Repulsion
+          distance =
+            Math.sqrt(xDist * xDist + yDist * yDist) -
+            NodeMatrix[n1 + NODE_SIZE] -
+            NodeMatrix[n2 + NODE_SIZE];
+
+          if (distance > 0) {
+            factor =
+              (coefficient *
+                NodeMatrix[n1 + NODE_MASS] *
+                NodeMatrix[n2 + NODE_MASS]) /
+              distance /
+              distance;
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] += xDist * factor;
+            NodeMatrix[n2 + NODE_DY] += yDist * factor;
+          } else if (distance < 0) {
+            factor =
+              100 *
+              coefficient *
+              NodeMatrix[n1 + NODE_MASS] *
+              NodeMatrix[n2 + NODE_MASS];
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+            NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+          }
+        } else {
+          //-- Linear Repulsion
+          distance = Math.sqrt(xDist * xDist + yDist * yDist);
+
+          if (distance > 0) {
+            factor =
+              (coefficient *
+                NodeMatrix[n1 + NODE_MASS] *
+                NodeMatrix[n2 + NODE_MASS]) /
+              distance /
+              distance;
+
+            // Updating nodes' dx and dy
+            NodeMatrix[n1 + NODE_DX] += xDist * factor;
+            NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+            NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+            NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+          }
+        }
+      }
+    }
+  }
+
+  // 3) Gravity
+  //------------
+  g = options.gravity / options.scalingRatio;
+  coefficient = options.scalingRatio;
+  for (n = 0; n < order; n += PPN) {
+    factor = 0;
+
+    // Common to both methods
+    xDist = NodeMatrix[n + NODE_X];
+    yDist = NodeMatrix[n + NODE_Y];
+    distance = Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
+
+    if (options.strongGravityMode) {
+      //-- Strong gravity
+      if (distance > 0) factor = coefficient * NodeMatrix[n + NODE_MASS] * g;
+    } else {
+      //-- Linear Anti-collision Repulsion n
+      if (distance > 0)
+        factor = (coefficient * NodeMatrix[n + NODE_MASS] * g) / distance;
+    }
+
+    // Updating node's dx and dy
+    NodeMatrix[n + NODE_DX] -= xDist * factor;
+    NodeMatrix[n + NODE_DY] -= yDist * factor;
+  }
+
+  // 4) Attraction
+  //---------------
+  coefficient =
+    1 * (options.outboundAttractionDistribution ? outboundAttCompensation : 1);
+
+  // TODO: simplify distance
+  // TODO: coefficient is always used as -c --> optimize?
+  for (e = 0; e < size; e += PPE) {
+    n1 = EdgeMatrix[e + EDGE_SOURCE];
+    n2 = EdgeMatrix[e + EDGE_TARGET];
+    w = EdgeMatrix[e + EDGE_WEIGHT];
+
+    // Edge weight influence
+    ewc = Math.pow(w, options.edgeWeightInfluence);
+
+    // Common measures
+    xDist = NodeMatrix[n1 + NODE_X] - NodeMatrix[n2 + NODE_X];
+    yDist = NodeMatrix[n1 + NODE_Y] - NodeMatrix[n2 + NODE_Y];
+
+    // Applying attraction to nodes
+    if (adjustSizes === true) {
+      distance = Math.sqrt(
+        Math.pow(xDist, 2) +
+          Math.pow(yDist, 2) -
+          NodeMatrix[n1 + NODE_SIZE] -
+          NodeMatrix[n2 + NODE_SIZE]
+      );
+
+      if (options.linLogMode) {
+        if (options.outboundAttractionDistribution) {
+          //-- LinLog Degree Distributed Anti-collision Attraction
+          if (distance > 0) {
+            factor =
+              (-coefficient * ewc * Math.log(1 + distance)) /
+              distance /
+              NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- LinLog Anti-collision Attraction
+          if (distance > 0) {
+            factor = (-coefficient * ewc * Math.log(1 + distance)) / distance;
+          }
+        }
+      } else {
+        if (options.outboundAttractionDistribution) {
+          //-- Linear Degree Distributed Anti-collision Attraction
+          if (distance > 0) {
+            factor = (-coefficient * ewc) / NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- Linear Anti-collision Attraction
+          if (distance > 0) {
+            factor = -coefficient * ewc;
+          }
+        }
+      }
+    } else {
+      distance = Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
+
+      if (options.linLogMode) {
+        if (options.outboundAttractionDistribution) {
+          //-- LinLog Degree Distributed Attraction
+          if (distance > 0) {
+            factor =
+              (-coefficient * ewc * Math.log(1 + distance)) /
+              distance /
+              NodeMatrix[n1 + NODE_MASS];
+          }
+        } else {
+          //-- LinLog Attraction
+          if (distance > 0)
+            factor = (-coefficient * ewc * Math.log(1 + distance)) / distance;
+        }
+      } else {
+        if (options.outboundAttractionDistribution) {
+          //-- Linear Attraction Mass Distributed
+          // NOTE: Distance is set to 1 to override next condition
+          distance = 1;
+          factor = (-coefficient * ewc) / NodeMatrix[n1 + NODE_MASS];
+        } else {
+          //-- Linear Attraction
+          // NOTE: Distance is set to 1 to override next condition
+          distance = 1;
+          factor = -coefficient * ewc;
+        }
+      }
+    }
+
+    // Updating nodes' dx and dy
+    // TODO: if condition or factor = 1?
+    if (distance > 0) {
+      // Updating nodes' dx and dy
+      NodeMatrix[n1 + NODE_DX] += xDist * factor;
+      NodeMatrix[n1 + NODE_DY] += yDist * factor;
+
+      NodeMatrix[n2 + NODE_DX] -= xDist * factor;
+      NodeMatrix[n2 + NODE_DY] -= yDist * factor;
+    }
+  }
+
+  // 5) Apply Forces
+  //-----------------
+  var force, swinging, traction, nodespeed, newX, newY;
+
+  // MATH: sqrt and square distances
+  if (adjustSizes === true) {
+    for (n = 0; n < order; n += PPN) {
+      if (NodeMatrix[n + NODE_FIXED] !== 1) {
+        force = Math.sqrt(
+          Math.pow(NodeMatrix[n + NODE_DX], 2) +
+            Math.pow(NodeMatrix[n + NODE_DY], 2)
+        );
+
+        if (force > MAX_FORCE) {
+          NodeMatrix[n + NODE_DX] =
+            (NodeMatrix[n + NODE_DX] * MAX_FORCE) / force;
+          NodeMatrix[n + NODE_DY] =
+            (NodeMatrix[n + NODE_DY] * MAX_FORCE) / force;
+        }
+
+        swinging =
+          NodeMatrix[n + NODE_MASS] *
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY])
+          );
+
+        traction =
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY])
+          ) / 2;
+
+        nodespeed = (0.1 * Math.log(1 + traction)) / (1 + Math.sqrt(swinging));
+
+        // Updating node's positon
+        newX =
+          NodeMatrix[n + NODE_X] +
+          NodeMatrix[n + NODE_DX] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_X] = newX;
+
+        newY =
+          NodeMatrix[n + NODE_Y] +
+          NodeMatrix[n + NODE_DY] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_Y] = newY;
+      }
+    }
+  } else {
+    for (n = 0; n < order; n += PPN) {
+      if (NodeMatrix[n + NODE_FIXED] !== 1) {
+        swinging =
+          NodeMatrix[n + NODE_MASS] *
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] - NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] - NodeMatrix[n + NODE_DY])
+          );
+
+        traction =
+          Math.sqrt(
+            (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) *
+              (NodeMatrix[n + NODE_OLD_DX] + NodeMatrix[n + NODE_DX]) +
+              (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY]) *
+                (NodeMatrix[n + NODE_OLD_DY] + NodeMatrix[n + NODE_DY])
+          ) / 2;
+
+        nodespeed =
+          (NodeMatrix[n + NODE_CONVERGENCE] * Math.log(1 + traction)) /
+          (1 + Math.sqrt(swinging));
+
+        // Updating node convergence
+        NodeMatrix[n + NODE_CONVERGENCE] = Math.min(
+          1,
+          Math.sqrt(
+            (nodespeed *
+              (Math.pow(NodeMatrix[n + NODE_DX], 2) +
+                Math.pow(NodeMatrix[n + NODE_DY], 2))) /
+              (1 + Math.sqrt(swinging))
+          )
+        );
+
+        // Updating node's positon
+        newX =
+          NodeMatrix[n + NODE_X] +
+          NodeMatrix[n + NODE_DX] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_X] = newX;
+
+        newY =
+          NodeMatrix[n + NODE_Y] +
+          NodeMatrix[n + NODE_DY] * (nodespeed / options.slowDown);
+        NodeMatrix[n + NODE_Y] = newY;
+      }
+    }
+  }
+
+  // We return the information about the layout (no need to return the matrices)
+  return {};
+};
+
+  })();
+
+  var iterate = moduleShim.exports;
+
+  self.addEventListener('message', function (event) {
+    var data = event.data;
+
+    NODES = new Float32Array(data.nodes);
+
+    if (data.edges) EDGES = new Float32Array(data.edges);
+
+    // Running the iteration
+    iterate(data.settings, NODES, EDGES);
+
+    // Sending result to supervisor
+    self.postMessage(
+      {
+        nodes: NODES.buffer
+      },
+      [NODES.buffer]
+    );
+  });
+};
+
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99dfcfa9f6d3f4c6420666058625ae16d0c9c31b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.d.ts
@@ -0,0 +1,20 @@
+import Graph from 'graphology-types';
+import {ForceAtlas2Settings} from './index';
+
+export type FA2LayoutSupervisorParameters = {
+  attributes?: {
+    weight?: string;
+  };
+  settings?: ForceAtlas2Settings;
+  weighted?: boolean;
+  outputReducer?: (key: string, attributes: any) => any;
+};
+
+export default class FA2LayoutSupervisor {
+  constructor(graph: Graph, params?: FA2LayoutSupervisorParameters);
+
+  isRunning(): boolean;
+  start(): void;
+  stop(): void;
+  kill(): void;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.js b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..1cebdb5ccec7d1a61af8cd71e57b6a0b916d0968
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-forceatlas2/worker.js
@@ -0,0 +1,203 @@
+/**
+ * Graphology ForceAtlas2 Layout Supervisor
+ * =========================================
+ *
+ * Supervisor class able to spawn a web worker to run the FA2 layout in a
+ * separate thread not to block UI with heavy synchronous computations.
+ */
+var workerFunction = require('./webworker.js'),
+  isGraph = require('graphology-utils/is-graph'),
+  helpers = require('./helpers.js');
+
+var DEFAULT_SETTINGS = require('./defaults.js');
+
+/**
+ * Class representing a FA2 layout run by a webworker.
+ *
+ * @constructor
+ * @param  {Graph}         graph        - Target graph.
+ * @param  {object|number} params       - Parameters:
+ * @param  {object}          [settings] - Settings.
+ */
+function FA2LayoutSupervisor(graph, params) {
+  params = params || {};
+
+  // Validation
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout-forceatlas2/worker: the given graph is not a valid graphology instance.'
+    );
+
+  var attributes = params.attributes || {};
+  var weightAttribute = params.weighted ? attributes.weight || 'weight' : null;
+
+  // Validating settings
+  var settings = helpers.assign({}, DEFAULT_SETTINGS, params.settings);
+  var validationError = helpers.validateSettings(settings);
+
+  if (validationError)
+    throw new Error(
+      'graphology-layout-forceatlas2/worker: ' + validationError.message
+    );
+
+  // Properties
+  this.worker = null;
+  this.graph = graph;
+  this.settings = settings;
+  this.weightAttribute = weightAttribute;
+  this.matrices = null;
+  this.running = false;
+  this.killed = false;
+  this.outputReducer =
+    typeof params.outputReducer === 'function' ? params.outputReducer : null;
+
+  // Binding listeners
+  this.handleMessage = this.handleMessage.bind(this);
+
+  var respawnFrame = undefined;
+  var self = this;
+
+  this.handleGraphUpdate = function () {
+    if (self.worker) self.worker.terminate();
+
+    if (respawnFrame) clearTimeout(respawnFrame);
+
+    respawnFrame = setTimeout(function () {
+      respawnFrame = undefined;
+      self.spawnWorker();
+    }, 0);
+  };
+
+  graph.on('nodeAdded', this.handleGraphUpdate);
+  graph.on('edgeAdded', this.handleGraphUpdate);
+  graph.on('nodeDropped', this.handleGraphUpdate);
+  graph.on('edgeDropped', this.handleGraphUpdate);
+
+  // Spawning worker
+  this.spawnWorker();
+}
+
+FA2LayoutSupervisor.prototype.isRunning = function () {
+  return this.running;
+};
+
+/**
+ * Internal method used to spawn the web worker.
+ */
+FA2LayoutSupervisor.prototype.spawnWorker = function () {
+  if (this.worker) this.worker.terminate();
+
+  this.worker = helpers.createWorker(workerFunction);
+  this.worker.addEventListener('message', this.handleMessage);
+
+  if (this.running) {
+    this.running = false;
+    this.start();
+  }
+};
+
+/**
+ * Internal method used to handle the worker's messages.
+ *
+ * @param {object} event - Event to handle.
+ */
+FA2LayoutSupervisor.prototype.handleMessage = function (event) {
+  if (!this.running) return;
+
+  var matrix = new Float32Array(event.data.nodes);
+
+  helpers.assignLayoutChanges(this.graph, matrix, this.outputReducer);
+  if (this.outputReducer) helpers.readGraphPositions(this.graph, matrix);
+  this.matrices.nodes = matrix;
+
+  // Looping
+  this.askForIterations();
+};
+
+/**
+ * Internal method used to ask for iterations from the worker.
+ *
+ * @param  {boolean} withEdges - Should we send edges along?
+ * @return {FA2LayoutSupervisor}
+ */
+FA2LayoutSupervisor.prototype.askForIterations = function (withEdges) {
+  var matrices = this.matrices;
+
+  var payload = {
+    settings: this.settings,
+    nodes: matrices.nodes.buffer
+  };
+
+  var buffers = [matrices.nodes.buffer];
+
+  if (withEdges) {
+    payload.edges = matrices.edges.buffer;
+    buffers.push(matrices.edges.buffer);
+  }
+
+  this.worker.postMessage(payload, buffers);
+
+  return this;
+};
+
+/**
+ * Method used to start the layout.
+ *
+ * @return {FA2LayoutSupervisor}
+ */
+FA2LayoutSupervisor.prototype.start = function () {
+  if (this.killed)
+    throw new Error(
+      'graphology-layout-forceatlas2/worker.start: layout was killed.'
+    );
+
+  if (this.running) return this;
+
+  // Building matrices
+  this.matrices = helpers.graphToByteArrays(this.graph, this.weightAttribute);
+
+  this.running = true;
+  this.askForIterations(true);
+
+  return this;
+};
+
+/**
+ * Method used to stop the layout.
+ *
+ * @return {FA2LayoutSupervisor}
+ */
+FA2LayoutSupervisor.prototype.stop = function () {
+  this.running = false;
+
+  return this;
+};
+
+/**
+ * Method used to kill the layout.
+ *
+ * @return {FA2LayoutSupervisor}
+ */
+FA2LayoutSupervisor.prototype.kill = function () {
+  if (this.killed) return this;
+
+  this.running = false;
+  this.killed = true;
+
+  // Clearing memory
+  this.matrices = null;
+
+  // Terminating worker
+  this.worker.terminate();
+
+  // Unbinding listeners
+  this.graph.removeListener('nodeAdded', this.handleGraphUpdate);
+  this.graph.removeListener('edgeAdded', this.handleGraphUpdate);
+  this.graph.removeListener('nodeDropped', this.handleGraphUpdate);
+  this.graph.removeListener('edgeDropped', this.handleGraphUpdate);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = FA2LayoutSupervisor;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/README.md b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0bde0610e1a3ccbaae39756b60ba44c6bd43d95b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/README.md
@@ -0,0 +1,92 @@
+# Graphology Noverlap
+
+JavaScript implementation of the Noverlap anti-collision layout algorithm for [graphology](https://graphology.github.io).
+
+Note that this algorithm is iterative and might not converge easily in some cases.
+
+## Installation
+
+```
+npm install graphology-layout-noverlap
+```
+
+## Usage
+
+- [Pre-requisite](#pre-requisite)
+- [Settings](#settings)
+- [Synchronous layout](#synchronous-layout)
+- [Webworker](#webworker)
+
+### Pre-requisites
+
+Each node's starting position must be set before running the Noverlap anti-collision layout. Two attributes called `x` and `y` must therefore be defined for all the graph nodes.
+
+### Settings
+
+- **gridSize** _?number_ [`20`]: number of grid cells horizontally and vertically subdivising the graph's space. This is used as an optimization scheme. Set it to `1` and you will have `O(n²)` time complexity, which can sometimes perform better with very few nodes.
+- **margin** _?number_ [`5`]: margin to keep between nodes.
+- **expansion** _?number_ [`1.1`]: percentage of current space that nodes could attempt to move outside of.
+- **ratio** _?number_ [`1.0`]: ratio scaling node sizes.
+- **speed** _?number_ [`3`]: dampening factor that will slow down node movements to ease the overall process.
+
+### Synchronous layout
+
+```js
+import noverlap from 'graphology-layout-noverlap';
+
+const positions = noverlap(graph, {maxIterations: 50});
+
+// With settings:
+const positions = noverlap(graph, {
+  maxIterations: 50,
+  settings: {
+    ratio: 2
+  }
+});
+
+// With a custom input reducer
+const positions = noverlap(graph, {
+  inputReducer: (key, attr) => ({
+    x: store[key].x,
+    y: store[key].y,
+    size: attr.size
+  }),
+  outputReducer: (key, pos) => ({x: pos.x * 10, y: pos.y * 10})
+});
+
+// To directly assign the positions to the nodes:
+noverlap.assign(graph);
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _object_: options:
+  - **maxIterations** _?number_ [`500`]: maximum number of iterations to perform before stopping. Note that the algorithm will also stop as soon as converged.
+  - **inputReducer** _?function_: a function reducing each node attributes. This can be useful if the rendered positions/sizes of your graph are stored outside of the graph's data. This is the case when using sigma.js for instance.
+  - **outputReducer** _?function_: a function reducing node positions as computed by the layout algorithm. This can be useful to map back to a previous coordinates system. This is the case when using sigma.js for instance.
+  - **settings** _?object_: the layout's settings (see [#settings](#settings)).
+
+### Webworker
+
+If you need to run the layout's computation in a web worker, the library comes with a utility to do so:
+
+_Example_
+
+```js
+import NoverlapLayout from 'graphology-layout-noverlap/worker';
+
+const layout = new NoverlapLayout(graph, params);
+
+// To start the layout. It will automatically stop when converged
+layout.start();
+
+// To stop the layout
+layout.stop();
+
+// To kill the layout and release attached memory
+layout.kill();
+
+// Assess whether the layout is currently running
+layout.isRunning();
+```
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/defaults.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/defaults.js
new file mode 100644
index 0000000000000000000000000000000000000000..75fd9afd9bd174320d8a1d826b064a798b790b19
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/defaults.js
@@ -0,0 +1,11 @@
+/**
+ * Graphology Noverlap Layout Default Settings
+ * ============================================
+ */
+module.exports = {
+  gridSize: 20,
+  margin: 5,
+  expansion: 1.1,
+  ratio: 1.0,
+  speed: 3
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/helpers.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..98eee491e767cd069d9671341d030cabfff713ae
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/helpers.js
@@ -0,0 +1,150 @@
+/**
+ * Graphology Noverlap Helpers
+ * ============================
+ *
+ * Miscellaneous helper functions.
+ */
+
+/**
+ * Constants.
+ */
+var PPN = 3;
+
+/**
+ * Function used to validate the given settings.
+ *
+ * @param  {object}      settings - Settings to validate.
+ * @return {object|null}
+ */
+exports.validateSettings = function (settings) {
+  if (
+    ('gridSize' in settings && typeof settings.gridSize !== 'number') ||
+    settings.gridSize <= 0
+  )
+    return {message: 'the `gridSize` setting should be a positive number.'};
+
+  if (
+    ('margin' in settings && typeof settings.margin !== 'number') ||
+    settings.margin < 0
+  )
+    return {
+      message: 'the `margin` setting should be 0 or a positive number.'
+    };
+
+  if (
+    ('expansion' in settings && typeof settings.expansion !== 'number') ||
+    settings.expansion <= 0
+  )
+    return {message: 'the `expansion` setting should be a positive number.'};
+
+  if (
+    ('ratio' in settings && typeof settings.ratio !== 'number') ||
+    settings.ratio <= 0
+  )
+    return {message: 'the `ratio` setting should be a positive number.'};
+
+  if (
+    ('speed' in settings && typeof settings.speed !== 'number') ||
+    settings.speed <= 0
+  )
+    return {message: 'the `speed` setting should be a positive number.'};
+
+  return null;
+};
+
+/**
+ * Function generating a flat matrix for the given graph's nodes.
+ *
+ * @param  {Graph}        graph   - Target graph.
+ * @param  {function}     reducer - Node reducer function.
+ * @return {Float32Array}         - The node matrix.
+ */
+exports.graphToByteArray = function (graph, reducer) {
+  var order = graph.order;
+
+  var matrix = new Float32Array(order * PPN);
+
+  var j = 0;
+
+  graph.forEachNode(function (node, attr) {
+    if (typeof reducer === 'function') attr = reducer(node, attr);
+
+    matrix[j] = attr.x;
+    matrix[j + 1] = attr.y;
+    matrix[j + 2] = attr.size || 1;
+    j += PPN;
+  });
+
+  return matrix;
+};
+
+/**
+ * Function applying the layout back to the graph.
+ *
+ * @param {Graph}        graph      - Target graph.
+ * @param {Float32Array} NodeMatrix - Node matrix.
+ * @param {function}     reducer    - Reducing function.
+ */
+exports.assignLayoutChanges = function (graph, NodeMatrix, reducer) {
+  var i = 0;
+
+  graph.forEachNode(function (node) {
+    var pos = {
+      x: NodeMatrix[i],
+      y: NodeMatrix[i + 1]
+    };
+
+    if (typeof reducer === 'function') pos = reducer(node, pos);
+
+    graph.mergeNodeAttributes(node, pos);
+
+    i += PPN;
+  });
+};
+
+/**
+ * Function collecting the layout positions.
+ *
+ * @param  {Graph}        graph      - Target graph.
+ * @param  {Float32Array} NodeMatrix - Node matrix.
+ * @param  {function}     reducer    - Reducing function.
+ * @return {object}                  - Map to node positions.
+ */
+exports.collectLayoutChanges = function (graph, NodeMatrix, reducer) {
+  var positions = {};
+
+  var i = 0;
+
+  graph.forEachNode(function (node) {
+    var pos = {
+      x: NodeMatrix[i],
+      y: NodeMatrix[i + 1]
+    };
+
+    if (typeof reducer === 'function') pos = reducer(node, pos);
+
+    positions[node] = pos;
+
+    i += PPN;
+  });
+
+  return positions;
+};
+
+/**
+ * Function returning a web worker from the given function.
+ *
+ * @param  {function}  fn - Function for the worker.
+ * @return {DOMString}
+ */
+exports.createWorker = function createWorker(fn) {
+  var xURL = window.URL || window.webkitURL;
+  var code = fn.toString();
+  var objectUrl = xURL.createObjectURL(
+    new Blob(['(' + code + ').call(this);'], {type: 'text/javascript'})
+  );
+  var worker = new Worker(objectUrl);
+  xURL.revokeObjectURL(objectUrl);
+
+  return worker;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e12d0d0b887274b03fdb9c9010511ed0518eeb9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.d.ts
@@ -0,0 +1,37 @@
+import Graph from 'graphology-types';
+
+type LayoutMapping = {[key: string]: {x: number; y: number}};
+
+type NoverlapNodeAttributes = {x: number; y: number; size?: number};
+
+export type NoverlapNodeReducer = (
+  key: string,
+  attr: NoverlapNodeAttributes
+) => NoverlapNodeAttributes;
+
+export type NoverlapSettings = {
+  gridSize?: number;
+  margin?: number;
+  expansion?: number;
+  ratio?: number;
+  speed?: number;
+};
+
+export type NoverlapLayoutParameters = {
+  maxIterations?: number;
+  inputReducer?: NoverlapNodeReducer;
+  outputReducer?: NoverlapNodeReducer;
+  settings?: NoverlapSettings;
+};
+
+interface INoverlapLayout {
+  (graph: Graph, maxIterations?: number): LayoutMapping;
+  (graph: Graph, params: NoverlapLayoutParameters): LayoutMapping;
+
+  assign(graph: Graph, maxIterations?: number): void;
+  assign(graph: Graph, params: NoverlapLayoutParameters): void;
+}
+
+declare const noverlap: INoverlapLayout;
+
+export default noverlap;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..08cdd03052461c42256c69d91b36907ea7488396
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/index.js
@@ -0,0 +1,71 @@
+/**
+ * Graphology Noverlap Layout
+ * ===========================
+ *
+ * Library endpoint.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var iterate = require('./iterate.js');
+var helpers = require('./helpers.js');
+
+var DEFAULT_SETTINGS = require('./defaults.js');
+var DEFAULT_MAX_ITERATIONS = 500;
+
+/**
+ * Asbtract function used to run a certain number of iterations.
+ *
+ * @param  {boolean}       assign       - Whether to assign positions.
+ * @param  {Graph}         graph        - Target graph.
+ * @param  {object|number} params       - If number, params.maxIterations, else:
+ * @param  {number}          maxIterations - Maximum number of iterations.
+ * @param  {object}          [settings] - Settings.
+ * @return {object|undefined}
+ */
+function abstractSynchronousLayout(assign, graph, params) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout-noverlap: the given graph is not a valid graphology instance.'
+    );
+
+  if (typeof params === 'number') params = {maxIterations: params};
+  else params = params || {};
+
+  var maxIterations = params.maxIterations || DEFAULT_MAX_ITERATIONS;
+
+  if (typeof maxIterations !== 'number' || maxIterations <= 0)
+    throw new Error(
+      'graphology-layout-force: you should provide a positive number of maximum iterations.'
+    );
+
+  // Validating settings
+  var settings = Object.assign({}, DEFAULT_SETTINGS, params.settings),
+    validationError = helpers.validateSettings(settings);
+
+  if (validationError)
+    throw new Error('graphology-layout-noverlap: ' + validationError.message);
+
+  // Building matrices
+  var matrix = helpers.graphToByteArray(graph, params.inputReducer),
+    converged = false,
+    i;
+
+  // Iterating
+  for (i = 0; i < maxIterations && !converged; i++)
+    converged = iterate(settings, matrix).converged;
+
+  // Applying
+  if (assign) {
+    helpers.assignLayoutChanges(graph, matrix, params.outputReducer);
+    return;
+  }
+
+  return helpers.collectLayoutChanges(graph, matrix, params.outputReducer);
+}
+
+/**
+ * Exporting.
+ */
+var synchronousLayout = abstractSynchronousLayout.bind(null, false);
+synchronousLayout.assign = abstractSynchronousLayout.bind(null, true);
+
+module.exports = synchronousLayout;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/iterate.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/iterate.js
new file mode 100644
index 0000000000000000000000000000000000000000..0eae612c3e7a5c2768ea883a5aeea79a077842b9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/iterate.js
@@ -0,0 +1,177 @@
+/**
+ * Graphology Noverlap Iteration
+ * ==============================
+ *
+ * Function used to perform a single iteration of the algorithm.
+ */
+
+/**
+ * Matrices properties accessors.
+ */
+var NODE_X = 0,
+  NODE_Y = 1,
+  NODE_SIZE = 2;
+
+/**
+ * Constants.
+ */
+var PPN = 3;
+
+/**
+ * Helpers.
+ */
+function hashPair(a, b) {
+  return a + '§' + b;
+}
+
+function jitter() {
+  return 0.01 * (0.5 - Math.random());
+}
+
+/**
+ * Function used to perform a single interation of the algorithm.
+ *
+ * @param  {object}       options    - Layout options.
+ * @param  {Float32Array} NodeMatrix - Node data.
+ * @return {object}                  - Some metadata.
+ */
+module.exports = function iterate(options, NodeMatrix) {
+  // Caching options
+  var margin = options.margin;
+  var ratio = options.ratio;
+  var expansion = options.expansion;
+  var gridSize = options.gridSize; // TODO: decrease grid size when few nodes?
+  var speed = options.speed;
+
+  // Generic iteration variables
+  var i, j, x, y, l, size;
+  var converged = true;
+
+  var length = NodeMatrix.length;
+  var order = (length / PPN) | 0;
+
+  var deltaX = new Float32Array(order);
+  var deltaY = new Float32Array(order);
+
+  // Finding the extents of our space
+  var xMin = Infinity;
+  var yMin = Infinity;
+  var xMax = -Infinity;
+  var yMax = -Infinity;
+
+  for (i = 0; i < length; i += PPN) {
+    x = NodeMatrix[i + NODE_X];
+    y = NodeMatrix[i + NODE_Y];
+    size = NodeMatrix[i + NODE_SIZE] * ratio + margin;
+
+    xMin = Math.min(xMin, x - size);
+    xMax = Math.max(xMax, x + size);
+    yMin = Math.min(yMin, y - size);
+    yMax = Math.max(yMax, y + size);
+  }
+
+  var width = xMax - xMin;
+  var height = yMax - yMin;
+  var xCenter = (xMin + xMax) / 2;
+  var yCenter = (yMin + yMax) / 2;
+
+  xMin = xCenter - (expansion * width) / 2;
+  xMax = xCenter + (expansion * width) / 2;
+  yMin = yCenter - (expansion * height) / 2;
+  yMax = yCenter + (expansion * height) / 2;
+
+  // Building grid
+  var grid = new Array(gridSize * gridSize),
+    gridLength = grid.length,
+    c;
+
+  for (c = 0; c < gridLength; c++) grid[c] = [];
+
+  var nxMin, nxMax, nyMin, nyMax;
+  var xMinBox, xMaxBox, yMinBox, yMaxBox;
+
+  var col, row;
+
+  for (i = 0; i < length; i += PPN) {
+    x = NodeMatrix[i + NODE_X];
+    y = NodeMatrix[i + NODE_Y];
+    size = NodeMatrix[i + NODE_SIZE] * ratio + margin;
+
+    nxMin = x - size;
+    nxMax = x + size;
+    nyMin = y - size;
+    nyMax = y + size;
+
+    xMinBox = Math.floor((gridSize * (nxMin - xMin)) / (xMax - xMin));
+    xMaxBox = Math.floor((gridSize * (nxMax - xMin)) / (xMax - xMin));
+    yMinBox = Math.floor((gridSize * (nyMin - yMin)) / (yMax - yMin));
+    yMaxBox = Math.floor((gridSize * (nyMax - yMin)) / (yMax - yMin));
+
+    for (col = xMinBox; col <= xMaxBox; col++) {
+      for (row = yMinBox; row <= yMaxBox; row++) {
+        grid[col * gridSize + row].push(i);
+      }
+    }
+  }
+
+  // Computing collisions
+  var cell;
+
+  var collisions = new Set();
+
+  var n1, n2, x1, x2, y1, y2, s1, s2, h;
+
+  var xDist, yDist, dist, collision;
+
+  for (c = 0; c < gridLength; c++) {
+    cell = grid[c];
+
+    for (i = 0, l = cell.length; i < l; i++) {
+      n1 = cell[i];
+
+      x1 = NodeMatrix[n1 + NODE_X];
+      y1 = NodeMatrix[n1 + NODE_Y];
+      s1 = NodeMatrix[n1 + NODE_SIZE];
+
+      for (j = i + 1; j < l; j++) {
+        n2 = cell[j];
+        h = hashPair(n1, n2);
+
+        if (gridLength > 1 && collisions.has(h)) continue;
+
+        if (gridLength > 1) collisions.add(h);
+
+        x2 = NodeMatrix[n2 + NODE_X];
+        y2 = NodeMatrix[n2 + NODE_Y];
+        s2 = NodeMatrix[n2 + NODE_SIZE];
+
+        xDist = x2 - x1;
+        yDist = y2 - y1;
+        dist = Math.sqrt(xDist * xDist + yDist * yDist);
+        collision = dist < s1 * ratio + margin + (s2 * ratio + margin);
+
+        if (collision) {
+          converged = false;
+
+          n2 = (n2 / PPN) | 0;
+
+          if (dist > 0) {
+            deltaX[n2] += (xDist / dist) * (1 + s1);
+            deltaY[n2] += (yDist / dist) * (1 + s1);
+          } else {
+            // Nodes are on the exact same spot, we need to jitter a bit
+            deltaX[n2] += width * jitter();
+            deltaY[n2] += height * jitter();
+          }
+        }
+      }
+    }
+  }
+
+  for (i = 0, j = 0; i < length; i += PPN, j++) {
+    NodeMatrix[i + NODE_X] += deltaX[j] * 0.1 * speed;
+    NodeMatrix[i + NODE_Y] += deltaY[j] * 0.1 * speed;
+  }
+
+  return {converged: converged};
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/package.json b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..abbe60c88e6c1dafd6252f0c936c84763c75322b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "graphology-layout-noverlap",
+  "version": "0.4.2",
+  "description": "Noverlap anti-collision layout algorithm for graphology.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "files": [
+    "*.d.ts",
+    "defaults.js",
+    "helpers.js",
+    "index.js",
+    "iterate.js",
+    "worker.js",
+    "webworker.js"
+  ],
+  "scripts": {
+    "bench": "node bench.js",
+    "clean": "rimraf webworker.js",
+    "prepublishOnly": "npm run clean && npm test && npm run template",
+    "template": "node ./scripts/template-webworker.js > webworker.js",
+    "test": "mocha test.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "layout",
+    "force atlas"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.19.0"
+  },
+  "dependencies": {
+    "graphology-utils": "^2.3.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/webworker.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/webworker.js
new file mode 100644
index 0000000000000000000000000000000000000000..870855f91f7220db989ff87569b2e07c02d7bf97
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/webworker.js
@@ -0,0 +1,213 @@
+/**
+ * Graphology Noverlap Layout Webworker
+ * =====================================
+ *
+ * Web worker able to run the layout in a separate thread.
+ */
+module.exports = function worker() {
+  var NODES;
+
+  var moduleShim = {};
+
+  (function () {
+    /**
+ * Graphology Noverlap Iteration
+ * ==============================
+ *
+ * Function used to perform a single iteration of the algorithm.
+ */
+
+/**
+ * Matrices properties accessors.
+ */
+var NODE_X = 0,
+  NODE_Y = 1,
+  NODE_SIZE = 2;
+
+/**
+ * Constants.
+ */
+var PPN = 3;
+
+/**
+ * Helpers.
+ */
+function hashPair(a, b) {
+  return a + '§' + b;
+}
+
+function jitter() {
+  return 0.01 * (0.5 - Math.random());
+}
+
+/**
+ * Function used to perform a single interation of the algorithm.
+ *
+ * @param  {object}       options    - Layout options.
+ * @param  {Float32Array} NodeMatrix - Node data.
+ * @return {object}                  - Some metadata.
+ */
+moduleShim.exports = function iterate(options, NodeMatrix) {
+  // Caching options
+  var margin = options.margin;
+  var ratio = options.ratio;
+  var expansion = options.expansion;
+  var gridSize = options.gridSize; // TODO: decrease grid size when few nodes?
+  var speed = options.speed;
+
+  // Generic iteration variables
+  var i, j, x, y, l, size;
+  var converged = true;
+
+  var length = NodeMatrix.length;
+  var order = (length / PPN) | 0;
+
+  var deltaX = new Float32Array(order);
+  var deltaY = new Float32Array(order);
+
+  // Finding the extents of our space
+  var xMin = Infinity;
+  var yMin = Infinity;
+  var xMax = -Infinity;
+  var yMax = -Infinity;
+
+  for (i = 0; i < length; i += PPN) {
+    x = NodeMatrix[i + NODE_X];
+    y = NodeMatrix[i + NODE_Y];
+    size = NodeMatrix[i + NODE_SIZE] * ratio + margin;
+
+    xMin = Math.min(xMin, x - size);
+    xMax = Math.max(xMax, x + size);
+    yMin = Math.min(yMin, y - size);
+    yMax = Math.max(yMax, y + size);
+  }
+
+  var width = xMax - xMin;
+  var height = yMax - yMin;
+  var xCenter = (xMin + xMax) / 2;
+  var yCenter = (yMin + yMax) / 2;
+
+  xMin = xCenter - (expansion * width) / 2;
+  xMax = xCenter + (expansion * width) / 2;
+  yMin = yCenter - (expansion * height) / 2;
+  yMax = yCenter + (expansion * height) / 2;
+
+  // Building grid
+  var grid = new Array(gridSize * gridSize),
+    gridLength = grid.length,
+    c;
+
+  for (c = 0; c < gridLength; c++) grid[c] = [];
+
+  var nxMin, nxMax, nyMin, nyMax;
+  var xMinBox, xMaxBox, yMinBox, yMaxBox;
+
+  var col, row;
+
+  for (i = 0; i < length; i += PPN) {
+    x = NodeMatrix[i + NODE_X];
+    y = NodeMatrix[i + NODE_Y];
+    size = NodeMatrix[i + NODE_SIZE] * ratio + margin;
+
+    nxMin = x - size;
+    nxMax = x + size;
+    nyMin = y - size;
+    nyMax = y + size;
+
+    xMinBox = Math.floor((gridSize * (nxMin - xMin)) / (xMax - xMin));
+    xMaxBox = Math.floor((gridSize * (nxMax - xMin)) / (xMax - xMin));
+    yMinBox = Math.floor((gridSize * (nyMin - yMin)) / (yMax - yMin));
+    yMaxBox = Math.floor((gridSize * (nyMax - yMin)) / (yMax - yMin));
+
+    for (col = xMinBox; col <= xMaxBox; col++) {
+      for (row = yMinBox; row <= yMaxBox; row++) {
+        grid[col * gridSize + row].push(i);
+      }
+    }
+  }
+
+  // Computing collisions
+  var cell;
+
+  var collisions = new Set();
+
+  var n1, n2, x1, x2, y1, y2, s1, s2, h;
+
+  var xDist, yDist, dist, collision;
+
+  for (c = 0; c < gridLength; c++) {
+    cell = grid[c];
+
+    for (i = 0, l = cell.length; i < l; i++) {
+      n1 = cell[i];
+
+      x1 = NodeMatrix[n1 + NODE_X];
+      y1 = NodeMatrix[n1 + NODE_Y];
+      s1 = NodeMatrix[n1 + NODE_SIZE];
+
+      for (j = i + 1; j < l; j++) {
+        n2 = cell[j];
+        h = hashPair(n1, n2);
+
+        if (gridLength > 1 && collisions.has(h)) continue;
+
+        if (gridLength > 1) collisions.add(h);
+
+        x2 = NodeMatrix[n2 + NODE_X];
+        y2 = NodeMatrix[n2 + NODE_Y];
+        s2 = NodeMatrix[n2 + NODE_SIZE];
+
+        xDist = x2 - x1;
+        yDist = y2 - y1;
+        dist = Math.sqrt(xDist * xDist + yDist * yDist);
+        collision = dist < s1 * ratio + margin + (s2 * ratio + margin);
+
+        if (collision) {
+          converged = false;
+
+          n2 = (n2 / PPN) | 0;
+
+          if (dist > 0) {
+            deltaX[n2] += (xDist / dist) * (1 + s1);
+            deltaY[n2] += (yDist / dist) * (1 + s1);
+          } else {
+            // Nodes are on the exact same spot, we need to jitter a bit
+            deltaX[n2] += width * jitter();
+            deltaY[n2] += height * jitter();
+          }
+        }
+      }
+    }
+  }
+
+  for (i = 0, j = 0; i < length; i += PPN, j++) {
+    NodeMatrix[i + NODE_X] += deltaX[j] * 0.1 * speed;
+    NodeMatrix[i + NODE_Y] += deltaY[j] * 0.1 * speed;
+  }
+
+  return {converged: converged};
+};
+
+  })();
+
+  var iterate = moduleShim.exports;
+
+  self.addEventListener('message', function (event) {
+    var data = event.data;
+
+    NODES = new Float32Array(data.nodes);
+
+    // Running the iteration
+    var result = iterate(data.settings, NODES);
+
+    // Sending result to supervisor
+    self.postMessage(
+      {
+        result: result,
+        nodes: NODES.buffer
+      },
+      [NODES.buffer]
+    );
+  });
+};
+
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a27bd1a9449d0cb2652bbe38e633830ec644635b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.d.ts
@@ -0,0 +1,20 @@
+import Graph from 'graphology-types';
+import {NoverlapSettings, NoverlapNodeReducer} from './index';
+
+export type NoverlapLayoutSupervisorParameters = {
+  inputReducer?: NoverlapNodeReducer;
+  outputReducer?: NoverlapNodeReducer;
+  onConverged?: () => void;
+  settings?: NoverlapSettings;
+};
+
+export default class NoverlapLayoutSupervisor {
+  converged: boolean;
+
+  constructor(graph: Graph, params?: NoverlapLayoutSupervisorParameters);
+
+  isRunning(): boolean;
+  start(): void;
+  stop(): void;
+  kill(): void;
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.js b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..2c3fa6459764dce9bd16dccb7b5ee09ba6f1cc40
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout-noverlap/worker.js
@@ -0,0 +1,203 @@
+/**
+ * Graphology Noverlap Layout Supervisor
+ * ======================================
+ *
+ * Supervisor class able to spawn a web worker to run the Noverlap layout in a
+ * separate thread not to block UI with heavy synchronous computations.
+ */
+var workerFunction = require('./webworker.js'),
+  isGraph = require('graphology-utils/is-graph'),
+  helpers = require('./helpers.js');
+
+var DEFAULT_SETTINGS = require('./defaults.js');
+
+/**
+ * Class representing a Noverlap layout run by a webworker.
+ *
+ * @constructor
+ * @param  {Graph}         graph        - Target graph.
+ * @param  {object|number} params       - Parameters:
+ * @param  {object}          [settings] - Settings.
+ */
+function NoverlapLayoutSupervisor(graph, params) {
+  params = params || {};
+
+  // Validation
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout-noverlap/worker: the given graph is not a valid graphology instance.'
+    );
+
+  // Validating settings
+  var settings = Object.assign({}, DEFAULT_SETTINGS, params.settings),
+    validationError = helpers.validateSettings(settings);
+
+  if (validationError)
+    throw new Error(
+      'graphology-layout-noverlap/worker: ' + validationError.message
+    );
+
+  // Properties
+  this.worker = null;
+  this.graph = graph;
+  this.settings = settings;
+  this.matrices = null;
+  this.running = false;
+  this.killed = false;
+
+  this.inputReducer = params.inputReducer;
+  this.outputReducer = params.outputReducer;
+
+  this.callbacks = {
+    onConverged:
+      typeof params.onConverged === 'function' ? params.onConverged : null
+  };
+
+  // Binding listeners
+  this.handleMessage = this.handleMessage.bind(this);
+
+  var alreadyRespawning = false;
+  var self = this;
+
+  this.handleAddition = function () {
+    if (alreadyRespawning) return;
+
+    alreadyRespawning = true;
+
+    self.spawnWorker();
+    setTimeout(function () {
+      alreadyRespawning = false;
+    }, 0);
+  };
+
+  graph.on('nodeAdded', this.handleAddition);
+  graph.on('edgeAdded', this.handleAddition);
+
+  // Spawning worker
+  this.spawnWorker();
+}
+
+NoverlapLayoutSupervisor.prototype.isRunning = function () {
+  return this.running;
+};
+
+/**
+ * Internal method used to spawn the web worker.
+ */
+NoverlapLayoutSupervisor.prototype.spawnWorker = function () {
+  if (this.worker) this.worker.terminate();
+
+  this.worker = helpers.createWorker(workerFunction);
+  this.worker.addEventListener('message', this.handleMessage);
+
+  if (this.running) {
+    this.running = false;
+    this.start();
+  }
+};
+
+/**
+ * Internal method used to handle the worker's messages.
+ *
+ * @param {object} event - Event to handle.
+ */
+NoverlapLayoutSupervisor.prototype.handleMessage = function (event) {
+  if (!this.running) return;
+
+  var matrix = new Float32Array(event.data.nodes);
+
+  helpers.assignLayoutChanges(this.graph, matrix, this.outputReducer);
+  this.matrices.nodes = matrix;
+
+  if (event.data.result.converged) {
+    if (this.callbacks.onConverged) this.callbacks.onConverged();
+
+    this.stop();
+    return;
+  }
+
+  // Looping
+  this.askForIterations();
+};
+
+/**
+ * Internal method used to ask for iterations from the worker.
+ *
+ * @return {NoverlapLayoutSupervisor}
+ */
+NoverlapLayoutSupervisor.prototype.askForIterations = function () {
+  var matrices = this.matrices;
+
+  var payload = {
+    settings: this.settings,
+    nodes: matrices.nodes.buffer
+  };
+
+  var buffers = [matrices.nodes.buffer];
+
+  this.worker.postMessage(payload, buffers);
+
+  return this;
+};
+
+/**
+ * Method used to start the layout.
+ *
+ * @return {NoverlapLayoutSupervisor}
+ */
+NoverlapLayoutSupervisor.prototype.start = function () {
+  if (this.killed)
+    throw new Error(
+      'graphology-layout-noverlap/worker.start: layout was killed.'
+    );
+
+  if (this.running) return this;
+
+  // Building matrices
+  this.matrices = {
+    nodes: helpers.graphToByteArray(this.graph, this.inputReducer)
+  };
+
+  this.running = true;
+  this.askForIterations();
+
+  return this;
+};
+
+/**
+ * Method used to stop the layout.
+ *
+ * @return {NoverlapLayoutSupervisor}
+ */
+NoverlapLayoutSupervisor.prototype.stop = function () {
+  this.running = false;
+
+  return this;
+};
+
+/**
+ * Method used to kill the layout.
+ *
+ * @return {NoverlapLayoutSupervisor}
+ */
+NoverlapLayoutSupervisor.prototype.kill = function () {
+  if (this.killed) return this;
+
+  this.running = false;
+  this.killed = true;
+
+  // Clearing memory
+  this.matrices = null;
+
+  // Terminating worker
+  this.worker.terminate();
+
+  // Unbinding listeners
+  this.graph.removeListener('nodeAdded', this.handleAddition);
+  this.graph.removeListener('edgeAdded', this.handleAddition);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = NoverlapLayoutSupervisor;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-layout/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/README.md b/libs/shared/graph-layout/node_modules/graphology-layout/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f653382ed09d84ca24adb88bf52dc3b0329973a6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/README.md
@@ -0,0 +1,153 @@
+# Graphology Layout
+
+Collection of basic layout algorithms to be used with [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-layout
+```
+
+## Usage
+
+_Basic_
+
+- [circular](#circular)
+- [random](#random)
+
+_Advanced_
+
+- [circlePack](#circlePack)
+
+_Utilities_
+
+- [rotation](#rotation)
+
+### #.circular
+
+Arranges the node in a circle (or an sphere/hypersphere in higher dimensions).
+
+_Example_
+
+```js
+import {circular} from 'graphology-layout';
+// Alternatively, to load only the relevant code:
+import circular from 'graphology-layout/circular';
+
+const positions = circular(graph);
+
+// With options:
+const positions = circular(graph, {scale: 100});
+
+// To directly assign the positions to the nodes:
+circular.assign(graph);
+
+// To pass custom dimensions
+const positions = random(graph, {dimensions: ['x1', 'x2']});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **dimensions** _?array_ [`['x', 'y']`]: dimensions of the layout. Cannot work with dimensions != 2.
+  - **center** _?number_ [`0.5`]: center of the layout.
+  - **scale** _?number_ [`1`]: scale of the layout.
+
+### #.random
+
+Random layout positioning every node by choosing each coordinates uniformly at random on the interval `[0, 1)`.
+
+_Example_
+
+```js
+import {random} from 'graphology-layout';
+// Alternatively, to load only the relevant code:
+import random from 'graphology-layout/random';
+
+const positions = random(graph);
+
+// With options:
+const positions = random(graph, {rng: customRngFunction});
+
+// To directly assign the positions to the nodes:
+random.assign(graph);
+
+// To pass custom dimensions
+const positions = random(graph, {dimensions: ['x', 'y', 'z']});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **dimensions** _?array_ [`['x', 'y']`]: dimensions of the layout.
+  - **center** _?number_ [`0.5`]: center of the layout.
+  - **rng** _?function_ [`Math.random`]: custom RNG function to use.
+  - **scale** _?number_ [`1`]: scale of the layout.
+
+### #.circlePack
+
+Arranges the nodes as a bubble chart, according to specified attributes.
+
+_Example_
+
+```js
+import {circlepack} from 'graphology-layout';
+// Alternatively, to load only the relevant code:
+import circlepack from 'graphology-layout/circlepack';
+
+const positions = circlepack(graph);
+
+// With options
+const positions = circlepack(graph, {
+  hierarchyAttributes: ['degree', 'community'],
+  rng: customRngFunction
+});
+
+// To directly assign the positions to the nodes:
+circlepack.assign(graph);
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **attributes** _?object_: attributes to map:
+    - **x** _?string_ [`x`]: name of the x position.
+    - **y** _?string_ [`y`]: name of the y position.
+  - **center** _?number_ [`0`]: center of the layout.
+  - **hierarchyAttributes** _?list_ [`[]`]: attributes used to group nodes.
+  - **rng** _?function_ [`Math.random`]: custom RNG function to use.
+  - **scale** _?number_ [`1`]: scale of the layout.
+
+### #.rotation
+
+Rotates the node coordinates of the given graph by the given angle in radians (or in degrees using an option).
+
+Note that this function rotates your graph based on its center. If you want to use zero as the center for your rotation, use the `centeredOnZero` option. This option can also be used as an optimization strategy if you know your graph is already centered on zero to avoid needing to compute the graph's extent.
+
+_Example_
+
+```js
+import {rotation} from 'graphology-layout';
+// Alternatively, to load only the relevant code:
+import rotation from 'graphology-layout/rotation';
+
+const positions = rotation(graph, Math.PI / 2);
+
+// With options:
+const positions = rotation(graph, Math.PI / 2, {centeredOnZero: true});
+
+// To directly assign the positions to the nodes:
+rotation.assign(graph, Math.PI / 2);
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **angle** _number_: rotation angle in radians (or degrees using an option below).
+- **options** _?object_: options:
+  - **dimensions** _?array_ [`['x', 'y']`]: dimensions to use for the rotation. Cannot work with dimensions != 2.
+  - **degrees** _?boolean_ [`false`]: whether the given angle is in degrees.
+  - **centeredOnZero** _?boolean_ [`false`]: whether to rotate the graph around `0`, rather than the graph's center.
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a3cecf6af2e6e57aba74262e03d0335f448571de
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.d.ts
@@ -0,0 +1,25 @@
+import Graph from 'graphology-types';
+
+type RNGFunction = () => number;
+
+export type CirclePackLayoutOptions = {
+  attributes?: {
+    x: string;
+    y: string;
+  };
+  center?: number;
+  hierarchyAttributes?: string[];
+  rng?: RNGFunction;
+  scale?: number;
+};
+
+type LayoutMapping = {[key: string]: {x: number; y: number}};
+
+interface ICirclePackLayout {
+  (graph: Graph, options?: CirclePackLayoutOptions): LayoutMapping;
+  assign(graph: Graph, options?: CirclePackLayoutOptions): void;
+}
+
+declare const circlepack: ICirclePackLayout;
+
+export default circlepack;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.js b/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.js
new file mode 100644
index 0000000000000000000000000000000000000000..26b4415851c8159409d96416a12be6fc5da71be2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/circlepack.js
@@ -0,0 +1,474 @@
+/**
+ * Graphology CirclePack Layout
+ * =============================
+ *
+ * Circlepack layout from d3-hierarchy/gephi.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+var shuffle = require('pandemonium/shuffle-in-place');
+
+/**
+ * Default options.
+ */
+var DEFAULTS = {
+  attributes: {
+    x: 'x',
+    y: 'y'
+  },
+  center: 0,
+  hierarchyAttributes: [],
+  rng: Math.random,
+  scale: 1
+};
+
+/**
+ * Helpers.
+ */
+function CircleWrap(id, x, y, r, circleWrap) {
+  this.wrappedCircle = circleWrap || null; //hacky d3 reference thing
+
+  this.children = {};
+  this.countChildren = 0;
+  this.id = id || null;
+  this.next = null;
+  this.previous = null;
+
+  this.x = x || null;
+  this.y = y || null;
+  if (circleWrap) this.r = 1010101;
+  // for debugging purposes - should not be used in this case
+  else this.r = r || 999;
+}
+
+CircleWrap.prototype.hasChildren = function () {
+  return this.countChildren > 0;
+};
+
+CircleWrap.prototype.addChild = function (id, child) {
+  this.children[id] = child;
+  ++this.countChildren;
+};
+
+CircleWrap.prototype.getChild = function (id) {
+  if (!this.children.hasOwnProperty(id)) {
+    var circleWrap = new CircleWrap();
+    this.children[id] = circleWrap;
+    ++this.countChildren;
+  }
+  return this.children[id];
+};
+
+CircleWrap.prototype.applyPositionToChildren = function () {
+  if (this.hasChildren()) {
+    var root = this; // using 'this' in Object.keys.forEach seems a bad idea
+    for (var key in root.children) {
+      var child = root.children[key];
+      child.x += root.x;
+      child.y += root.y;
+      child.applyPositionToChildren();
+    }
+  }
+};
+
+function setNode(/*Graph*/ graph, /*CircleWrap*/ parentCircle, /*Map*/ posMap) {
+  for (var key in parentCircle.children) {
+    var circle = parentCircle.children[key];
+    if (circle.hasChildren()) {
+      setNode(graph, circle, posMap);
+    } else {
+      posMap[circle.id] = {x: circle.x, y: circle.y};
+    }
+  }
+}
+
+function enclosesNot(/*CircleWrap*/ a, /*CircleWrap*/ b) {
+  var dr = a.r - b.r;
+  var dx = b.x - a.x;
+  var dy = b.y - a.y;
+  return dr < 0 || dr * dr < dx * dx + dy * dy;
+}
+
+function enclosesWeak(/*CircleWrap*/ a, /*CircleWrap*/ b) {
+  var dr = a.r - b.r + 1e-6;
+  var dx = b.x - a.x;
+  var dy = b.y - a.y;
+  return dr > 0 && dr * dr > dx * dx + dy * dy;
+}
+
+function enclosesWeakAll(/*CircleWrap*/ a, /*Array<CircleWrap>*/ B) {
+  for (var i = 0; i < B.length; ++i) {
+    if (!enclosesWeak(a, B[i])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function encloseBasis1(/*CircleWrap*/ a) {
+  return new CircleWrap(null, a.x, a.y, a.r);
+}
+
+function encloseBasis2(/*CircleWrap*/ a, /*CircleWrap*/ b) {
+  var x1 = a.x,
+    y1 = a.y,
+    r1 = a.r,
+    x2 = b.x,
+    y2 = b.y,
+    r2 = b.r,
+    x21 = x2 - x1,
+    y21 = y2 - y1,
+    r21 = r2 - r1,
+    l = Math.sqrt(x21 * x21 + y21 * y21);
+  return new CircleWrap(
+    null,
+    (x1 + x2 + (x21 / l) * r21) / 2,
+    (y1 + y2 + (y21 / l) * r21) / 2,
+    (l + r1 + r2) / 2
+  );
+}
+
+function encloseBasis3(/*CircleWrap*/ a, /*CircleWrap*/ b, /*CircleWrap*/ c) {
+  var x1 = a.x,
+    y1 = a.y,
+    r1 = a.r,
+    x2 = b.x,
+    y2 = b.y,
+    r2 = b.r,
+    x3 = c.x,
+    y3 = c.y,
+    r3 = c.r,
+    a2 = x1 - x2,
+    a3 = x1 - x3,
+    b2 = y1 - y2,
+    b3 = y1 - y3,
+    c2 = r2 - r1,
+    c3 = r3 - r1,
+    d1 = x1 * x1 + y1 * y1 - r1 * r1,
+    d2 = d1 - x2 * x2 - y2 * y2 + r2 * r2,
+    d3 = d1 - x3 * x3 - y3 * y3 + r3 * r3,
+    ab = a3 * b2 - a2 * b3,
+    xa = (b2 * d3 - b3 * d2) / (ab * 2) - x1,
+    xb = (b3 * c2 - b2 * c3) / ab,
+    ya = (a3 * d2 - a2 * d3) / (ab * 2) - y1,
+    yb = (a2 * c3 - a3 * c2) / ab,
+    A = xb * xb + yb * yb - 1,
+    B = 2 * (r1 + xa * xb + ya * yb),
+    C = xa * xa + ya * ya - r1 * r1,
+    r = -(A ? (B + Math.sqrt(B * B - 4 * A * C)) / (2 * A) : C / B);
+  return new CircleWrap(null, x1 + xa + xb * r, y1 + ya + yb * r, r);
+}
+
+function encloseBasis(/*Array<CircleWrap>*/ B) {
+  switch (B.length) {
+    case 1:
+      return encloseBasis1(B[0]);
+    case 2:
+      return encloseBasis2(B[0], B[1]);
+    case 3:
+      return encloseBasis3(B[0], B[1], B[2]);
+    default:
+      throw new Error(
+        'graphology-layout/circlepack: Invalid basis length ' + B.length
+      );
+  }
+}
+
+function extendBasis(/*Array<CircleWrap>*/ B, /*CircleWrap*/ p) {
+  var i, j;
+
+  if (enclosesWeakAll(p, B)) return [p];
+
+  // If we get here then B must have at least one element.
+  for (i = 0; i < B.length; ++i) {
+    if (enclosesNot(p, B[i]) && enclosesWeakAll(encloseBasis2(B[i], p), B)) {
+      return [B[i], p];
+    }
+  }
+
+  // If we get here then B must have at least two elements.
+  for (i = 0; i < B.length - 1; ++i) {
+    for (j = i + 1; j < B.length; ++j) {
+      if (
+        enclosesNot(encloseBasis2(B[i], B[j]), p) &&
+        enclosesNot(encloseBasis2(B[i], p), B[j]) &&
+        enclosesNot(encloseBasis2(B[j], p), B[i]) &&
+        enclosesWeakAll(encloseBasis3(B[i], B[j], p), B)
+      ) {
+        return [B[i], B[j], p];
+      }
+    }
+  }
+
+  // If we get here then something is very wrong.
+  throw new Error('graphology-layout/circlepack: extendBasis failure !');
+}
+
+function score(/*CircleWrap*/ node) {
+  var a = node.wrappedCircle;
+  var b = node.next.wrappedCircle;
+  var ab = a.r + b.r;
+  var dx = (a.x * b.r + b.x * a.r) / ab;
+  var dy = (a.y * b.r + b.y * a.r) / ab;
+  return dx * dx + dy * dy;
+}
+
+function enclose(circles, shuffleFunc) {
+  var i = 0;
+  var circlesLoc = circles.slice();
+
+  var n = circles.length;
+  var B = [];
+  var p;
+  var e;
+  shuffleFunc(circlesLoc);
+  while (i < n) {
+    p = circlesLoc[i];
+    if (e && enclosesWeak(e, p)) {
+      ++i;
+    } else {
+      B = extendBasis(B, p);
+      e = encloseBasis(B);
+      i = 0;
+    }
+  }
+  return e;
+}
+
+function place(/*CircleWrap*/ b, /*CircleWrap*/ a, /*CircleWrap*/ c) {
+  var dx = b.x - a.x,
+    x,
+    a2,
+    dy = b.y - a.y,
+    y,
+    b2,
+    d2 = dx * dx + dy * dy;
+  if (d2) {
+    a2 = a.r + c.r;
+    a2 *= a2;
+    b2 = b.r + c.r;
+    b2 *= b2;
+    if (a2 > b2) {
+      x = (d2 + b2 - a2) / (2 * d2);
+      y = Math.sqrt(Math.max(0, b2 / d2 - x * x));
+      c.x = b.x - x * dx - y * dy;
+      c.y = b.y - x * dy + y * dx;
+    } else {
+      x = (d2 + a2 - b2) / (2 * d2);
+      y = Math.sqrt(Math.max(0, a2 / d2 - x * x));
+      c.x = a.x + x * dx - y * dy;
+      c.y = a.y + x * dy + y * dx;
+    }
+  } else {
+    c.x = a.x + c.r;
+    c.y = a.y;
+  }
+}
+
+function intersects(/*CircleWrap*/ a, /*CircleWrap*/ b) {
+  var dr = a.r + b.r - 1e-6,
+    dx = b.x - a.x,
+    dy = b.y - a.y;
+  return dr > 0 && dr * dr > dx * dx + dy * dy;
+}
+
+function packEnclose(/*Array<CircleWrap>*/ circles, shuffleFunc) {
+  var n = circles.length;
+  if (n === 0) return 0;
+
+  var a, b, c, aa, ca, i, j, k, sj, sk;
+
+  // Place the first circle.
+  a = circles[0];
+  a.x = 0;
+  a.y = 0;
+  if (n <= 1) return a.r;
+
+  // Place the second circle.
+  b = circles[1];
+  a.x = -b.r;
+  b.x = a.r;
+  b.y = 0;
+  if (n <= 2) return a.r + b.r;
+
+  // Place the third circle.
+  c = circles[2];
+  place(b, a, c);
+
+  // Initialize the front-chain using the first three circles a, b and c.
+  a = new CircleWrap(null, null, null, null, a);
+  b = new CircleWrap(null, null, null, null, b);
+  c = new CircleWrap(null, null, null, null, c);
+  a.next = c.previous = b;
+  b.next = a.previous = c;
+  c.next = b.previous = a;
+
+  // Attempt to place each remaining circle…
+  pack: for (i = 3; i < n; ++i) {
+    c = circles[i];
+    place(a.wrappedCircle, b.wrappedCircle, c);
+    c = new CircleWrap(null, null, null, null, c);
+
+    // Find the closest intersecting circle on the front-chain, if any.
+    // “Closeness” is determined by linear distance along the front-chain.
+    // “Ahead” or “behind” is likewise determined by linear distance.
+    j = b.next;
+    k = a.previous;
+    sj = b.wrappedCircle.r;
+    sk = a.wrappedCircle.r;
+    do {
+      if (sj <= sk) {
+        if (intersects(j.wrappedCircle, c.wrappedCircle)) {
+          b = j;
+          a.next = b;
+          b.previous = a;
+          --i;
+          continue pack;
+        }
+        sj += j.wrappedCircle.r;
+        j = j.next;
+      } else {
+        if (intersects(k.wrappedCircle, c.wrappedCircle)) {
+          a = k;
+          a.next = b;
+          b.previous = a;
+          --i;
+          continue pack;
+        }
+        sk += k.wrappedCircle.r;
+        k = k.previous;
+      }
+    } while (j !== k.next);
+
+    // Success! Insert the new circle c between a and b.
+    c.previous = a;
+    c.next = b;
+    a.next = b.previous = b = c;
+
+    // Compute the new closest circle pair to the centroid.
+    aa = score(a);
+    while ((c = c.next) !== b) {
+      if ((ca = score(c)) < aa) {
+        a = c;
+        aa = ca;
+      }
+    }
+    b = a.next;
+  }
+
+  // Compute the enclosing circle of the front chain.
+  a = [b.wrappedCircle];
+  c = b;
+  var safety = 10000;
+  while ((c = c.next) !== b) {
+    if (--safety === 0) {
+      break;
+    }
+    a.push(c.wrappedCircle);
+  }
+  c = enclose(a, shuffleFunc);
+
+  // Translate the circles to put the enclosing circle around the origin.
+  for (i = 0; i < n; ++i) {
+    a = circles[i];
+    a.x -= c.x;
+    a.y -= c.y;
+  }
+  return c.r;
+}
+
+function packHierarchy(/*CircleWrap*/ parentCircle, shuffleFunc) {
+  var r = 0;
+  if (parentCircle.hasChildren()) {
+    //pack the children first because the radius is determined by how the children get packed (recursive)
+    for (var key in parentCircle.children) {
+      var circle = parentCircle.children[key];
+      if (circle.hasChildren()) {
+        circle.r = packHierarchy(circle, shuffleFunc);
+      }
+    }
+    //now that each circle has a radius set by its children, pack the circles at this level
+    r = packEnclose(Object.values(parentCircle.children), shuffleFunc);
+  }
+  return r;
+}
+
+function packHierarchyAndShift(/*CircleWrap*/ parentCircle, shuffleFunc) {
+  packHierarchy(parentCircle, shuffleFunc);
+  for (var key in parentCircle.children) {
+    var circle = parentCircle.children[key];
+    circle.applyPositionToChildren();
+  }
+}
+
+/**
+ * Abstract function running the layout.
+ *
+ * @param  {Graph}    graph                   - Target  graph.
+ * @param  {object}   [options]               - Options:
+ * @param  {object}     [attributes]          - Attributes names to map.
+ * @param  {number}     [center]              - Center of the layout.
+ * @param  {string[]}   [hierarchyAttributes] - List of attributes used for the layout in decreasing order.
+ * @param  {function}   [rng]                 - Custom RNG function to be used.
+ * @param  {number}     [scale]               - Scale of the layout.
+ * @return {object}                           - The positions by node.
+ */
+function genericCirclePackLayout(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout/circlepack: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var posMap = {},
+    positions = {},
+    nodes = graph.nodes(),
+    center = options.center,
+    hierarchyAttributes = options.hierarchyAttributes,
+    shuffleFunc = shuffle.createShuffleInPlace(options.rng),
+    scale = options.scale;
+
+  var container = new CircleWrap();
+
+  graph.forEachNode(function (key, attributes) {
+    var r = attributes.size ? attributes.size : 1;
+    var newCircleWrap = new CircleWrap(key, null, null, r);
+    var parentContainer = container;
+
+    hierarchyAttributes.forEach(function (v) {
+      var attr = attributes[v];
+      parentContainer = parentContainer.getChild(attr);
+    });
+
+    parentContainer.addChild(key, newCircleWrap);
+  });
+  packHierarchyAndShift(container, shuffleFunc);
+  setNode(graph, container, posMap);
+  var l = nodes.length,
+    x,
+    y,
+    i;
+  for (i = 0; i < l; i++) {
+    var node = nodes[i];
+
+    x = center + scale * posMap[node].x;
+    y = center + scale * posMap[node].y;
+
+    positions[node] = {
+      x: x,
+      y: y
+    };
+
+    if (assign) {
+      graph.setNodeAttribute(node, options.attributes.x, x);
+      graph.setNodeAttribute(node, options.attributes.y, y);
+    }
+  }
+  return positions;
+}
+
+var circlePackLayout = genericCirclePackLayout.bind(null, false);
+circlePackLayout.assign = genericCirclePackLayout.bind(null, true);
+
+module.exports = circlePackLayout;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/circular.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/circular.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b4944c4fd38622ca6da9219e3c3b1fa13834370c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/circular.d.ts
@@ -0,0 +1,18 @@
+import Graph from 'graphology-types';
+
+export type CircularLayoutOptions = {
+  dimensions?: string[];
+  center?: number;
+  scale?: number;
+};
+
+type LayoutMapping = {[node: string]: {[dimension: string]: number}};
+
+interface ICircularLayout {
+  (graph: Graph, options?: CircularLayoutOptions): LayoutMapping;
+  assign(graph: Graph, options?: CircularLayoutOptions): void;
+}
+
+declare const circular: ICircularLayout;
+
+export default circular;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/circular.js b/libs/shared/graph-layout/node_modules/graphology-layout/circular.js
new file mode 100644
index 0000000000000000000000000000000000000000..a911c710c9add519e47e13da36f32dd0138d8ac2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/circular.js
@@ -0,0 +1,85 @@
+/**
+ * Graphology Circular Layout
+ * ===========================
+ *
+ * Layout arranging the nodes in a circle.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Default options.
+ */
+var DEFAULTS = {
+  dimensions: ['x', 'y'],
+  center: 0.5,
+  scale: 1
+};
+
+/**
+ * Abstract function running the layout.
+ *
+ * @param  {Graph}    graph          - Target  graph.
+ * @param  {object}   [options]      - Options:
+ * @param  {object}     [attributes] - Attributes names to map.
+ * @param  {number}     [center]     - Center of the layout.
+ * @param  {number}     [scale]      - Scale of the layout.
+ * @return {object}                  - The positions by node.
+ */
+function genericCircularLayout(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout/random: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var dimensions = options.dimensions;
+
+  if (!Array.isArray(dimensions) || dimensions.length !== 2)
+    throw new Error('graphology-layout/random: given dimensions are invalid.');
+
+  var center = options.center;
+  var scale = options.scale;
+  var tau = Math.PI * 2;
+
+  var offset = (center - 0.5) * scale;
+  var l = graph.order;
+
+  var x = dimensions[0];
+  var y = dimensions[1];
+
+  function assignPosition(i, target) {
+    target[x] = scale * Math.cos((i * tau) / l) + offset;
+    target[y] = scale * Math.sin((i * tau) / l) + offset;
+
+    return target;
+  }
+
+  var i = 0;
+
+  if (!assign) {
+    var positions = {};
+
+    graph.forEachNode(function (node) {
+      positions[node] = assignPosition(i++, {});
+    });
+
+    return positions;
+  }
+
+  graph.updateEachNodeAttributes(
+    function (_, attr) {
+      assignPosition(i++, attr);
+      return attr;
+    },
+    {
+      attributes: dimensions
+    }
+  );
+}
+
+var circularLayout = genericCircularLayout.bind(null, false);
+circularLayout.assign = genericCircularLayout.bind(null, true);
+
+module.exports = circularLayout;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5d3b048b6dfbf850b84aaa2006b3bb1e31ddac1d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/index.d.ts
@@ -0,0 +1,4 @@
+export {default as circlepack} from './circlepack';
+export {default as circular} from './circular';
+export {default as random} from './random';
+export {default as rotation} from './rotation';
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/index.js b/libs/shared/graph-layout/node_modules/graphology-layout/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a5f56f5fffafbe3ed55a488f56e14e8188cf37de
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/index.js
@@ -0,0 +1,10 @@
+/**
+ * Graphology Layout
+ * ==================
+ *
+ * Library endpoint.
+ */
+exports.circlepack = require('./circlepack.js');
+exports.circular = require('./circular.js');
+exports.random = require('./random.js');
+exports.rotation = require('./rotation.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/matrices.js b/libs/shared/graph-layout/node_modules/graphology-layout/matrices.js
new file mode 100644
index 0000000000000000000000000000000000000000..14b11df955b71c74f09123f85ffb27dab080abd5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/matrices.js
@@ -0,0 +1,88 @@
+/**
+ * Sigma.js WebGL Matrices Helpers
+ * ================================
+ *
+ * Matrices-related helper functions used by sigma's WebGL renderer.
+ * @module
+ */
+
+exports.identity = function identity() {
+  return Float64Array.of(1, 0, 0, 0, 1, 0, 0, 0, 1);
+};
+
+exports.scale = function scale(m, x, y) {
+  m[0] = x;
+  m[4] = typeof y === 'number' ? y : x;
+
+  return m;
+};
+
+exports.rotate = function rotate(m, r) {
+  var s = Math.sin(r);
+  var c = Math.cos(r);
+
+  m[0] = c;
+  m[1] = s;
+  m[3] = -s;
+  m[4] = c;
+
+  return m;
+};
+
+exports.translate = function translate(m, x, y) {
+  m[6] = x;
+  m[7] = y;
+
+  return m;
+};
+
+exports.multiply = function multiply(a, b) {
+  var a00 = a[0];
+  var a01 = a[1];
+  var a02 = a[2];
+  var a10 = a[3];
+  var a11 = a[4];
+  var a12 = a[5];
+  var a20 = a[6];
+  var a21 = a[7];
+  var a22 = a[8];
+
+  var b00 = b[0];
+  var b01 = b[1];
+  var b02 = b[2];
+  var b10 = b[3];
+  var b11 = b[4];
+  var b12 = b[5];
+  var b20 = b[6];
+  var b21 = b[7];
+  var b22 = b[8];
+
+  a[0] = b00 * a00 + b01 * a10 + b02 * a20;
+  a[1] = b00 * a01 + b01 * a11 + b02 * a21;
+  a[2] = b00 * a02 + b01 * a12 + b02 * a22;
+
+  a[3] = b10 * a00 + b11 * a10 + b12 * a20;
+  a[4] = b10 * a01 + b11 * a11 + b12 * a21;
+  a[5] = b10 * a02 + b11 * a12 + b12 * a22;
+
+  a[6] = b20 * a00 + b21 * a10 + b22 * a20;
+  a[7] = b20 * a01 + b21 * a11 + b22 * a21;
+  a[8] = b20 * a02 + b21 * a12 + b22 * a22;
+
+  return a;
+};
+
+exports.multiplyVec2 = function multiplyVec2(a, b) {
+  var a00 = a[0];
+  var a01 = a[1];
+  var a10 = a[3];
+  var a11 = a[4];
+  var a20 = a[6];
+  var a21 = a[7];
+
+  var b0 = b.x;
+  var b1 = b.y;
+
+  b.x = b0 * a00 + b1 * a10 + a20;
+  b.y = b0 * a01 + b1 * a11 + a21;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/package.json b/libs/shared/graph-layout/node_modules/graphology-layout/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..336dec88714df0f64d9d3fcbfb6004f60837d189
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/package.json
@@ -0,0 +1,49 @@
+{
+  "name": "graphology-layout",
+  "version": "0.5.0",
+  "description": "Collection of basic layout algorithms for graphology.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "files": [
+    "*.d.ts",
+    "circlepack.js",
+    "circular.js",
+    "index.js",
+    "matrices.js",
+    "random.js",
+    "rotation.js",
+    "utils.js"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha --recursive test"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "circlepack",
+    "circular",
+    "graph",
+    "graphology",
+    "layout",
+    "random"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.19.0"
+  },
+  "dependencies": {
+    "graphology-utils": "^2.3.0",
+    "pandemonium": "^1.5.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/random.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/random.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..57ee1bd0f4328ffd4c455bfc42624a1e11c1f865
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/random.d.ts
@@ -0,0 +1,21 @@
+import Graph from 'graphology-types';
+
+type RNGFunction = () => number;
+
+export type RandomLayoutOptions = {
+  dimensions?: string[];
+  center?: number;
+  rng?: RNGFunction;
+  scale?: number;
+};
+
+type LayoutMapping = {[node: string]: {[dimension: string]: number}};
+
+interface IRandomLayout {
+  (graph: Graph, options?: RandomLayoutOptions): LayoutMapping;
+  assign(graph: Graph, options?: RandomLayoutOptions): void;
+}
+
+declare const random: IRandomLayout;
+
+export default random;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/random.js b/libs/shared/graph-layout/node_modules/graphology-layout/random.js
new file mode 100644
index 0000000000000000000000000000000000000000..1274db9d5f8371baacc657be7f30f71a7d7bbf68
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/random.js
@@ -0,0 +1,83 @@
+/**
+ * Graphology Random Layout
+ * =========================
+ *
+ * Simple layout giving uniform random positions to the nodes.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Default options.
+ */
+var DEFAULTS = {
+  dimensions: ['x', 'y'],
+  center: 0.5,
+  rng: Math.random,
+  scale: 1
+};
+
+/**
+ * Abstract function running the layout.
+ *
+ * @param  {Graph}    graph          - Target  graph.
+ * @param  {object}   [options]      - Options:
+ * @param  {array}      [dimensions] - List of dimensions of the layout.
+ * @param  {number}     [center]     - Center of the layout.
+ * @param  {function}   [rng]        - Custom RNG function to be used.
+ * @param  {number}     [scale]      - Scale of the layout.
+ * @return {object}                  - The positions by node.
+ */
+function genericRandomLayout(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout/random: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var dimensions = options.dimensions;
+
+  if (!Array.isArray(dimensions) || dimensions.length < 1)
+    throw new Error('graphology-layout/random: given dimensions are invalid.');
+
+  var d = dimensions.length;
+  var center = options.center;
+  var rng = options.rng;
+  var scale = options.scale;
+
+  var offset = (center - 0.5) * scale;
+
+  function assignPosition(target) {
+    for (var i = 0; i < d; i++) {
+      target[dimensions[i]] = rng() * scale + offset;
+    }
+
+    return target;
+  }
+
+  if (!assign) {
+    var positions = {};
+
+    graph.forEachNode(function (node) {
+      positions[node] = assignPosition({});
+    });
+
+    return positions;
+  }
+
+  graph.updateEachNodeAttributes(
+    function (_, attr) {
+      assignPosition(attr);
+      return attr;
+    },
+    {
+      attributes: dimensions
+    }
+  );
+}
+
+var randomLayout = genericRandomLayout.bind(null, false);
+randomLayout.assign = genericRandomLayout.bind(null, true);
+
+module.exports = randomLayout;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/rotation.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/rotation.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80c66c679e7ae6c74636019eaf8e510799e758b8
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/rotation.d.ts
@@ -0,0 +1,18 @@
+import Graph from 'graphology-types';
+
+export type RotationOptions = {
+  dimensions?: string[];
+  centeredOnZero?: boolean;
+  degrees?: boolean;
+};
+
+type LayoutMapping = {[node: string]: {[dimension: string]: number}};
+
+interface IRotation {
+  (graph: Graph, angle: number, options?: RotationOptions): LayoutMapping;
+  assign(graph: Graph, angle: number, options?: RotationOptions): void;
+}
+
+declare const rotation: IRotation;
+
+export default rotation;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/rotation.js b/libs/shared/graph-layout/node_modules/graphology-layout/rotation.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5be8fef1f4fe8ebff0dcf063c8cd283f82d4a34
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/rotation.js
@@ -0,0 +1,121 @@
+/**
+ * Graphology Rotation Layout Helper
+ * ==================================
+ *
+ * Function rotating the coordinates of the graph.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Constants.
+ */
+var RAD_CONVERSION = Math.PI / 180;
+
+/**
+ * Default options.
+ */
+var DEFAULTS = {
+  dimensions: ['x', 'y'],
+  centeredOnZero: false,
+  degrees: false
+};
+
+/**
+ * Abstract function for rotating a graph's coordinates.
+ *
+ * @param  {Graph}    graph          - Target  graph.
+ * @param  {number}   angle          - Rotation angle.
+ * @param  {object}   [options]      - Options.
+ * @return {object}                  - The positions by node.
+ */
+function genericRotation(assign, graph, angle, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-layout/rotation: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  if (options.degrees) angle *= RAD_CONVERSION;
+
+  var dimensions = options.dimensions;
+
+  if (!Array.isArray(dimensions) || dimensions.length !== 2)
+    throw new Error('graphology-layout/random: given dimensions are invalid.');
+
+  // Handling null graph
+  if (graph.order === 0) {
+    if (assign) return;
+
+    return {};
+  }
+
+  var xd = dimensions[0];
+  var yd = dimensions[1];
+
+  var xCenter = 0;
+  var yCenter = 0;
+
+  if (!options.centeredOnZero) {
+    // Finding bounds of the graph
+    var xMin = Infinity;
+    var xMax = -Infinity;
+    var yMin = Infinity;
+    var yMax = -Infinity;
+
+    graph.forEachNode(function (node, attr) {
+      var x = attr[xd];
+      var y = attr[yd];
+
+      if (x < xMin) xMin = x;
+      if (x > xMax) xMax = x;
+      if (y < yMin) yMin = y;
+      if (y > yMax) yMax = y;
+    });
+
+    xCenter = (xMin + xMax) / 2;
+    yCenter = (yMin + yMax) / 2;
+  }
+
+  var cos = Math.cos(angle);
+  var sin = Math.sin(angle);
+
+  function assignPosition(target) {
+    var x = target[xd];
+    var y = target[yd];
+
+    target[xd] = xCenter + (x - xCenter) * cos - (y - yCenter) * sin;
+    target[yd] = yCenter + (x - xCenter) * sin + (y - yCenter) * cos;
+
+    return target;
+  }
+
+  if (!assign) {
+    var positions = {};
+
+    graph.forEachNode(function (node, attr) {
+      var o = {};
+      o[xd] = attr[xd];
+      o[yd] = attr[yd];
+      positions[node] = assignPosition(o);
+    });
+
+    return positions;
+  }
+
+  graph.updateEachNodeAttributes(
+    function (_, attr) {
+      assignPosition(attr);
+      return attr;
+    },
+    {
+      attributes: dimensions
+    }
+  );
+}
+
+var rotation = genericRotation.bind(null, false);
+rotation.assign = genericRotation.bind(null, true);
+
+module.exports = rotation;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/utils.d.ts b/libs/shared/graph-layout/node_modules/graphology-layout/utils.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6f987f118eab0a296b3c51d31eb7b589d84b07b1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/utils.d.ts
@@ -0,0 +1,37 @@
+type Extent = [min: number, max: number];
+
+export type GraphExtent = {
+  x: Extent;
+  y: Extent;
+};
+
+export interface Coordinates {
+  x: number;
+  y: number;
+}
+
+export interface Dimensions {
+  width: number;
+  height: number;
+}
+
+export interface Camera extends Coordinates {
+  angle: number;
+  ratio: number;
+}
+
+type ConversionFunctionOptions = {
+  camera?: Camera;
+  padding?: number;
+};
+
+export interface CoordinateConversionFunction {
+  (coordinates: Coordinates): Coordinates;
+  assign(coordinates: Coordinates): Coordinates;
+}
+
+export function createGraphToViewportConversionFunction(
+  graphExtent: GraphExtent,
+  viewportDimensions: Dimensions,
+  options?: ConversionFunctionOptions
+): CoordinateConversionFunction;
diff --git a/libs/shared/graph-layout/node_modules/graphology-layout/utils.js b/libs/shared/graph-layout/node_modules/graphology-layout/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..97b30fa2bec5026993c99539e5713e8f0b1328b6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-layout/utils.js
@@ -0,0 +1,149 @@
+/**
+ * Graphology Layout Utilities
+ * ============================
+ *
+ * Miscellaneous utility functions used by the library.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var matrices = require('./matrices.js');
+
+var identity = matrices.identity;
+var multiply = matrices.multiply;
+var translate = matrices.translate;
+var scale = matrices.scale;
+var rotate = matrices.rotate;
+var multiplyVec2 = matrices.multiplyVec2;
+
+/**
+ * Function taken from sigma and returning a correction factor to suit the
+ * difference between the graph and the viewport's aspect ratio.
+ */
+function getAspectRatioCorrection(
+  graphWidth,
+  graphHeight,
+  viewportWidth,
+  viewportHeight
+) {
+  var viewportRatio = viewportHeight / viewportWidth;
+  var graphRatio = graphHeight / graphWidth;
+
+  // If the stage and the graphs are in different directions (such as the graph being wider that tall while the stage
+  // is taller than wide), we can stop here to have indeed nodes touching opposite sides:
+  if (
+    (viewportRatio < 1 && graphRatio > 1) ||
+    (viewportRatio > 1 && graphRatio < 1)
+  ) {
+    return 1;
+  }
+
+  // Else, we need to fit the graph inside the stage:
+  // 1. If the graph is "squarer" (ie. with a ratio closer to 1), we need to make the largest sides touch;
+  // 2. If the stage is "squarer", we need to make the smallest sides touch.
+  return Math.min(
+    Math.max(graphRatio, 1 / graphRatio),
+    Math.max(1 / viewportRatio, viewportRatio)
+  );
+}
+
+/**
+ * Factory for a function converting from an arbitrary graph space to a
+ * viewport one (like an HTML5 canvas, for instance).
+ */
+var DEFAULT_CAMERA = {
+  x: 0.5,
+  y: 0.5,
+  angle: 0,
+  ratio: 1
+};
+
+var CONVERSION_FUNCTION_DEFAULTS = {
+  camera: DEFAULT_CAMERA,
+  padding: 0
+};
+
+function createGraphToViewportConversionFunction(
+  graphExtent,
+  viewportDimensions,
+  options
+) {
+  // Resolving options
+  options = resolveDefaults(options, CONVERSION_FUNCTION_DEFAULTS);
+
+  var camera = options.camera;
+
+  // Computing graph dimensions
+  var maxGX = graphExtent.x[1];
+  var maxGY = graphExtent.y[1];
+  var minGX = graphExtent.x[0];
+  var minGY = graphExtent.y[0];
+
+  var graphWidth = maxGX - minGX;
+  var graphHeight = maxGY - minGY;
+
+  var viewportWidth = viewportDimensions.width;
+  var viewportHeight = viewportDimensions.height;
+
+  // Precomputing values
+  var graphRatio = Math.max(graphWidth, graphHeight) || 1;
+
+  var gdx = (maxGX + minGX) / 2;
+  var gdy = (maxGY + minGY) / 2;
+
+  var smallest = Math.min(viewportWidth, viewportHeight);
+  smallest -= 2 * options.padding;
+
+  var correction = getAspectRatioCorrection(
+    graphWidth,
+    graphHeight,
+    viewportWidth,
+    viewportHeight
+  );
+
+  var matrix = identity();
+
+  // Realigning with canvas coordinates
+  multiply(matrix, scale(identity(), viewportWidth / 2, viewportHeight / 2));
+  multiply(matrix, translate(identity(), 1, 1));
+  multiply(matrix, scale(identity(), 1, -1));
+
+  // Applying camera and transforming space
+  multiply(
+    matrix,
+    scale(
+      identity(),
+      2 * (smallest / viewportWidth) * correction,
+      2 * (smallest / viewportHeight) * correction
+    )
+  );
+  multiply(matrix, rotate(identity(), -camera.angle));
+  multiply(matrix, scale(identity(), 1 / camera.ratio));
+  multiply(matrix, translate(identity(), -camera.x, -camera.y));
+
+  // Normalizing graph space to squished square
+  multiply(matrix, translate(identity(), 0.5, 0.5));
+  multiply(matrix, scale(identity(), 1 / graphRatio));
+  multiply(matrix, translate(identity(), -gdx, -gdy));
+
+  // Assignation function
+  var assign = function (pos) {
+    // Applying matrix transformation
+    multiplyVec2(matrix, pos);
+
+    return pos;
+  };
+
+  // Immutable variant
+  var graphToViewport = function (pos) {
+    return assign({x: pos.x, y: pos.y});
+  };
+
+  graphToViewport.assign = assign;
+
+  return graphToViewport;
+}
+
+/**
+ * Exports.
+ */
+exports.createGraphToViewportConversionFunction =
+  createGraphToViewportConversionFunction;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-metrics/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df12ee58e0ff4fa313b5bc24e694918ccc1b363a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/README.md b/libs/shared/graph-layout/node_modules/graphology-metrics/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c970344d10a241c84b01520291bf18c378f44cbc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/README.md
@@ -0,0 +1,547 @@
+# Graphology metrics
+
+Miscellaneous metrics to be used with [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-metrics
+```
+
+## Usage
+
+_Graph metrics_
+
+- [Density](#density)
+- [Diameter](#diameter)
+- [Extent](#extent)
+- [Modularity](#modularity)
+- [Simple size](#simple-size)
+- [Weighted size](#weighted-size)
+
+_Node metrics_
+
+- [Eccentricity](#eccentricity)
+- [Weighted degree](#weighted-degree)
+
+_Edge metrics_
+
+- [Disparity](#disparity)
+- [Simmelian strength](#simmelian-strength)
+
+_Centrality_
+
+- [Betweenness centrality](#betweenness-centrality)
+- [Closeness centrality](#closeness-centrality)
+- [Degree centrality](#degree-centrality)
+- [Eigenvector centrality](#eigenvector-centrality)
+- [HITS](#hits)
+- [Pagerank](#pagerank)
+
+_Layout quality metrics_
+
+- [Edge Uniformity](#edge-uniformity)
+- [Neighborhood Preservation](#neighborhood-preservation)
+- [Stress](#stress)
+
+## Graph metrics
+
+### Density
+
+Computes the density of the given graph. Note that multi variants can exceed `0`, as it is also the case when considering self loops.
+
+```js
+import {density} from 'graphology-metrics/graph/density';
+
+// Passing a graph instance
+const d = density(graph);
+
+// Passing the graph's order & size
+const d = density(order, size);
+
+// Or to force the kind of density being computed
+import {
+  mixedDensity,
+  directedDensity,
+  undirectedDensity,
+  multiMixedDensity,
+  multiDirectedDensity,
+  multiUndirectedDensity
+} from 'graphology-metric/graph/density';
+
+const d = undirectedDensity(mixedGraph);
+
+// If you need to chose the kind of density dynamically
+import {abstractDensity} from 'graphology-metric/graph/density';
+
+abstractDensity('directed', true, 10, 24);
+```
+
+_Arguments_
+
+Either:
+
+- **graph** _Graph_: target graph.
+
+Or:
+
+- **order** _number_: number of nodes in the graph.
+- **size** _number_: number of edges in the graph.
+
+_Abstract version arguments_
+
+Either:
+
+- **type** _string_: type of density to compute (`directed`, `undirected` or `mixed`).
+- **multi** _boolean_: whether to compute density for the multi of simple case.
+- **graph** _Graph_: target graph.
+
+Or:
+
+- **type** _string_: type of density to compute (`directed`, `undirected` or `mixed`).
+- **multi** _boolean_: whether to compute density for the multi of simple case.
+- **order** _number_: number of nodes in the graph.
+- **size** _number_: number of edges in the graph.
+
+### Diameter
+
+Computes the diameter, i.e the maximum eccentricity of any node of the given graph.
+
+```js
+import diameter from 'graphology-metrics/graph/diameter';
+
+const graph = new Graph();
+graph.addNode('1');
+graph.addNode('2');
+graph.addNode('3');
+graph.addUndirectedEdge(1, 2);
+graph.addUndirectedEdge(2, 3);
+
+diameter(graph);
+>>> 2
+
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+
+### Extent
+
+Computes the extent - min, max - of a node or edge's attribute.
+
+```js
+import {nodeExtent, edgeExtent} from 'graphology-metrics/graph';
+// Alternatively, to load only the relevant code:
+import {nodeExtent, edgeExtent} from 'graphology-metrics/graph/extent';
+
+// Retrieving a single node attribute's extent
+nodeExtent(graph, 'size');
+>>> [1, 34]
+
+// Retrieving multiple node attributes' extents
+nodeExtent(graph, ['x', 'y']);
+>>> {x: [-4, 3], y: [-34, 56]}
+
+// The same for edges
+edgeExtent(graph, 'weight');
+>>> [0, 5.7]
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **attributes** _string|array_: single attribute names or array of attribute names.
+
+### Modularity
+
+Computes the modularity, given the graph and a node partition. It works on both directed & undirected networks and will return the relevant modularity.
+
+```js
+import modularity from 'graphology-metrics/graph/modularity';
+
+// Simplest way
+const Q = modularity(graph);
+
+// Custom node partition
+const Q = modularity(graph, {
+  getNodeCommunity(node, attr) {
+    return attr.customPartition;
+  }
+});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **getNodeCommunity** _?string\|function_ [`community`]: name of the node community attribute or getter function.
+  - **getEdgeWeight** _?string\|function_ [`weight`]: name of the edges' weight attribute or getter function.
+  - **resolution** _?number_: resolution parameter (`γ`).
+
+### Simple size
+
+Computes the simple size of a given graph, i.e. its number of edges if we consider the graph simple, even if it has multiple edges between pairs of nodes.
+
+```js
+import {simpleSize} from 'graphology-metrics';
+// Alternatively, to load only the relevant code:
+import simpleSize from 'graphology-metrics/graph/simple-size';
+
+const graph = new MultiGraph();
+graph.mergeEdge(1, 2);
+graph.mergeEdge(1, 2);
+graph.mergeEdge(4, 3);
+graph.mergeUndirectedEdge(5, 6);
+
+simpleSize(graph);
+>>> 3
+```
+
+### Weighted size
+
+Computes the weighted size, i.e. the sum of the graph's edges' weight, of the given graph.
+
+```js
+import weightedSize from 'graphology-metrics/graph/weighted-size';
+
+const graph = new Graph();
+graph.mergeEdge(1, 2, {weight: 3});
+graph.mergeEdge(1, 2, {weight: 1});
+
+// Simplest way
+weightedSize(graph);
+>>> 4
+
+// With custom weight attribute
+weightedSize(graph, 'myWeightAttribute');
+>>> 4
+
+// With custom getter
+weightedSize(graph, (_, attr) => attr.importance);
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **getEdgeWeight** _?string\|function_ [`weight`]: name of the weight attribute or getter function.
+
+## Node metrics
+
+### Weighted degree
+
+Computes the weighted degree of nodes. The weighted degree of a node is the sum of its edges' weights.
+
+```js
+import {
+  weightedDegree,
+  weightedInDegree,
+  weightedOutDegree,
+  weightedInboundDegree,
+  weightedOutboundDegree,
+  weightedUndirectedDegree,
+  weightedDirectedDegree
+} from 'graphology-metrics/node/weighted-degree';
+
+// To compute weighted degree of a node
+weightedDegree(graph, 'A');
+
+// To use a custom weight
+weightedDegree(graph, 'A', function (_, attr) {
+  return attr.importance;
+});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **node** _any_: desired node.
+- **getEdgeWeight** _?string\|function_: name of the edge weight attribute or getter function.
+
+### Eccentricity
+
+Computes the eccentricity which is the maximum of the shortest paths between the given node and any other node.
+
+```js
+import eccentricity from 'graphology-metrics/node/eccentricity';
+
+graph.addNode('1');
+graph.addNode('2');
+graph.addNode('3');
+graph.addNode('4');
+graph.addUndirectedEdge(1, 2);
+graph.addUndirectedEdge(2, 3);
+graph.addUndirectedEdge(3, 1);
+graph.addUndirectedEdge(3, 4);
+
+eccentricity(graph, 3) >> 1;
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **node** _any_: desired node.
+
+## Edge metrics
+
+### Disparity
+
+Function computing a score for each edge which is necessary to apply a "disparity filter" as described in the following paper:
+
+> Serrano, M. Ángeles, Marián Boguná, and Alessandro Vespignani. "Extracting the multiscale backbone of complex weighted networks." Proceedings of the national academy of sciences 106.16 (2009): 6483-6488.
+
+Note that this metric requires a weighted graph or will return a useless result.
+
+Beware, the results must be interpreted thusly: a lower score means a more relevant edge, as is intuited in the paper's formulae. This means you can prune edges that have a score greater than a given threshold, as a statistical test. Some other implementations might differ in that they offer the opposite intuition (i.e. greater score = more relevant edge).
+
+```js
+import disparity from 'graphology-metrics/edge/disparity';
+
+// To compute strength for every edge:
+const disparities = disparity(graph);
+
+// To directly map the result onto edge attributes (`disparity`):
+disparity.assign(graph);
+
+// Using custom weights
+disparity.assign(graph, {getEdgeWeight: (_, attr) => attr.importance});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **edgeDisparityAttribute** _?string_ [`disparity`]: Name of the disparity attribute to assign.
+  - **getEdgeWeight** _?string\|function_ [`weight`]: Name of the edge weight attribute or getter function.
+
+### Simmelian strength
+
+Function returning the simmelian strength, i.e. the number of triangles an edge is part of, of all the edges in the given graph.
+
+```js
+import simmelianStrength from 'graphology-metrics/edge/simmelian-strength';
+
+// To compute strength for every edge:
+const strengths = simmelianStrength(graph);
+
+// To directly map the result onto edge attributes (`simmelianStrength`):
+simmelianStrength.assign(graph);
+```
+
+## Centrality
+
+### Betweenness centrality
+
+Computes the betweenness centrality for every node.
+
+```js
+import betweennessCentrality from 'graphology-metrics/centrality/betweenness';
+
+// To compute centrality for every node:
+const centralities = betweennessCentrality(graph);
+
+// To directly map the result onto nodes' attributes (`betweennessCentrality`):
+betweennessCentrality.assign(graph);
+
+// To directly map the result onto a custom attribute:
+betweennessCentrality.assign(graph, {nodeCentralityAttribute: 'myCentrality'});
+
+// To ignore weights
+const centralities = betweennessCentrality(graph, {getEdgeWeight: null});
+
+// To use a getter function for weights
+const centralities = betweennessCentrality(graph, {
+  getEdgeWeight: (_, attr) => attr.importance
+});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **nodeCentralityAttribute** _?string_ [`betweennessCentrality`]: Name of the centrality attribute to assign.
+  - **getEdgeWeight** _?string\|function_ [`weight`]: Name of the edge weight attribute or getter function.
+  - **normalized** _?boolean_ [`true`]: should the result be normalized?
+
+### Closeness centrality
+
+Computes the closeness centrality of a graph's nodes.
+
+```js
+import closenessCentrality from 'graphology-metrics/centrality/closeness';
+
+// To compute the eigenvector centrality and return the score per node:
+const scores = closenessCentrality(graph);
+
+// To directly map the result to nodes' attributes:
+closenessCentrality.assign(graph);
+
+// Note that you can also pass options to customize the algorithm:
+const p = closenessCentrality(graph, {wassermanFaust: true});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **nodeCentralityAttribute** _?string_ [`closenessCentrality`]: name of the node attribute that will be assigned the closeness centrality.
+  - **wassermanFaust** _?boolean_ [`false`]: whether to use Wasserman & Faust's normalization scheme.
+
+### Degree centrality
+
+Computes the degree centrality for every node.
+
+```js
+import {
+  degreeCentrality,
+  inDegreeCentrality,
+  outDegreeCentrality
+} from 'graphology-metrics/centrality/degree';
+
+// To compute degree centrality for every node:
+const centralities = degreeCentrality(graph);
+
+// To directly map the result onto nodes' attributes (`degreeCentrality`):
+degreeCentrality.assign(graph);
+
+// To directly map the result onto a custom attribute:
+degreeCentrality.assign(graph, {nodeCentralityAttribute: 'myCentrality'});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **nodeCentralityAttribute** _?string_ [`degreeCentrality`]: name of the centrality attribute to assign.
+
+### Eigenvector centrality
+
+Computes the eigenvector centrality of a graph's nodes.
+
+```js
+import eigenvectorCentrality from 'graphology-metrics/centrality/eigenvector';
+
+// To compute the eigenvector centrality and return the score per node:
+const scores = eigenvectorCentrality(graph);
+
+// To directly map the result to nodes' attributes:
+eigenvectorCentrality.assign(graph);
+
+// Note that you can also pass options to customize the algorithm:
+const p = eigenvectorCentrality(graph, {tolerance: 1e-3});
+
+// To ignore your graph's weights
+eigenvectorCentrality.assign(graph, {getEdgeWeight: null});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **nodeCentralityAttribute** _?string_ [`eigenvectorCentrality`]: name of the node attribute that will be assigned the eigenvector centrality.
+  - **getEdgeWeight** _?string\|function_ [`weight`]: name of the edges' weight attribute or getter function.
+  - **maxIterations** _?number_ [`100`]: maximum number of iterations to perform.
+  - **tolerance** _?number_ [`1.e-6`]: convergence error tolerance.
+
+### HITS
+
+Computes the hub/authority metrics for each node using the HITS algorithm.
+
+```js
+import hits from 'graphology-metrics/centrality/hits';
+
+// To compute and return the result as 'hubs' & 'authorities':
+const {hubs, authorities} = hits(graph);
+
+// To directly map the result to nodes' attributes:
+hits.assign(graph);
+
+// Note that you can also pass options to customize the algorithm:
+const {hubs, authorities} = hits(graph, {normalize: false});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **getEdgeWeight** _?string\|function_ [`weight`]: name of the edges' weight attribute or getter function.
+  - **nodeHubAttribute** _?string_ [`hub`]: name of the node attribute holding hub information.
+  - **nodeAuthorityAttribute** _?string_ [`authority`]: name of the node attribute holding authority information.
+  - **maxIterations** _?number_ [`100`]: maximum number of iterations to perform.
+  - **normalize** _?boolean_ [`true`]: should the result be normalized by the sum of values.
+  - **tolerance** _?number_ [`1.e-8`]: convergence error tolerance.
+
+### Pagerank
+
+Computes the pagerank metrics for each node.
+
+```js
+import pagerank from 'graphology-metrics/centrality/pagerank';
+
+// To compute pagerank and return the score per node:
+const scores = pagerank(graph);
+
+// To directly map the result to nodes' attributes:
+pagerank.assign(graph);
+
+// Note that you can also pass options to customize the algorithm:
+const p = pagerank(graph, {alpha: 0.9});
+
+// To ignore your graph's weights
+pagerank.assign(graph, {getEdgeWeight: null});
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **options** _?object_: options:
+  - **nodePagerankAttribute** _?string_ [`pagerank`]: name of the node attribute that will be assigned the pagerank score.
+  - **getEdgeWeight** _?string\|function_ [`weight`]: name of the edges' weight attribute or getter function.
+  - **alpha** _?number_ [`0.85`]: damping parameter of the algorithm.
+  - **maxIterations** _?number_ [`100`]: maximum number of iterations to perform.
+  - **tolerance** _?number_ [`1.e-6`]: convergence error tolerance.
+
+## Layout quality metrics
+
+### Edge Uniformity
+
+Computes the edge uniformity layout quality metric from the given graph having `x` and `y` positions attached to its nodes. Edge uniformity is the normalized standard deviation of edge length of the graph. Lower values should be synonym of better layout according to this particular metric.
+
+Runs in `O(E)`.
+
+```js
+import edgeUniformity from 'graphology-metrics/layout-quality/edge-uniformity';
+
+edgeUniformity(graph);
+>>> ~1.132
+```
+
+### Neighborhood preservation
+
+Computes the "neighborhood preservation" layout quality metric from the given graph having `x` and `y` positions attached to its nodes. Neighborhood preservation is the average proportion of node neighborhood being the same both in the graph's topology and its 2d layout space. The metric is therefore comprised between `0` and `1`, `1` being the best, meaning that every node keeps its neighborhood perfectly intact within the layout space.
+
+Runs in approximately `O(N * log(N))`.
+
+```js
+import neighborhoodPreservation from 'graphology-metrics/layout-quality/neighborhood-preservation';
+
+neighborhoodPreservation(graph);
+// >>> 0.456
+```
+
+### Stress
+
+Computes the "stress" layout quality metric from the given graph having `x` and `y` positions attached to its nodes. Stress is the sum of normalized delta between node topology distances and their layout space distances. Lower values should be synonym of better layout according to this particular metric.
+
+Note that this metric does not work very well when the graph has multiple connected components.
+
+Note also that this metric traverses any given graph as an undirected one.
+
+Runs in `O(N^2)`.
+
+```js
+import stress from 'graphology-metrics/layout-quality/stress';
+
+stress(graph);
+// >>> ~24510.2914
+```
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1e680fe66dfc3cb124fdd3ce80f659c3468ccba
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.d.ts
@@ -0,0 +1,34 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+
+type BetweennessCentralityMapping = {[node: string]: number};
+
+type BetweennessCentralityOptions<EdgeAttributes extends Attributes> = {
+  nodeCentralityAttribute?: string;
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+    | null;
+  normalized?: boolean;
+};
+
+interface IBetweennessCentrality {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: BetweennessCentralityOptions<EdgeAttributes>
+  ): BetweennessCentralityMapping;
+  assign<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: BetweennessCentralityOptions<EdgeAttributes>
+  ): void;
+}
+
+declare const betweennessCentrality: IBetweennessCentrality;
+
+export default betweennessCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.js
new file mode 100644
index 0000000000000000000000000000000000000000..119fad62296ddf0a5deae9d47303e9d69570b564
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/betweenness.js
@@ -0,0 +1,104 @@
+/**
+ * Graphology Betweenness Centrality
+ * ==================================
+ *
+ * Function computing betweenness centrality.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var lib = require('graphology-shortest-path/indexed-brandes');
+var resolveDefaults = require('graphology-utils/defaults');
+
+var createUnweightedIndexedBrandes = lib.createUnweightedIndexedBrandes;
+var createDijkstraIndexedBrandes = lib.createDijkstraIndexedBrandes;
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  nodeCentralityAttribute: 'betweennessCentrality',
+  getEdgeWeight: 'weight',
+  normalized: true
+};
+
+/**
+ * Abstract function computing beetweenness centrality for the given graph.
+ *
+ * @param  {boolean} assign                      - Assign the results to node attributes?
+ * @param  {Graph}   graph                       - Target graph.
+ * @param  {object}  [options]                   - Options:
+ * @param  {object}    [nodeCentralityAttribute] - Name of the attribute to assign.
+ * @param  {string}    [getEdgeWeight]           - Name of the weight attribute or getter function.
+ * @param  {boolean}   [normalized]              - Should the centrality be normalized?
+ * @param  {object}
+ */
+function abstractBetweennessCentrality(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-centrality/beetweenness-centrality: the given graph is not a valid graphology instance.'
+    );
+
+  // Solving options
+  options = resolveDefaults(options, DEFAULTS);
+
+  var outputName = options.nodeCentralityAttribute;
+  var normalized = options.normalized;
+
+  var brandes = options.getEdgeWeight
+    ? createDijkstraIndexedBrandes(graph, options.getEdgeWeight)
+    : createUnweightedIndexedBrandes(graph);
+
+  var N = graph.order;
+
+  var result, S, P, sigma, coefficient, i, j, m, v, w;
+
+  var delta = new Float64Array(N);
+  var centralities = new Float64Array(N);
+
+  // Iterating over each node
+  for (i = 0; i < N; i++) {
+    result = brandes(i);
+
+    S = result[0];
+    P = result[1];
+    sigma = result[2];
+
+    // Accumulating
+    j = S.size;
+
+    while (j--) delta[S.items[S.size - j]] = 0;
+
+    while (S.size !== 0) {
+      w = S.pop();
+      coefficient = (1 + delta[w]) / sigma[w];
+
+      for (j = 0, m = P[w].length; j < m; j++) {
+        v = P[w][j];
+        delta[v] += sigma[v] * coefficient;
+      }
+
+      if (w !== i) centralities[w] += delta[w];
+    }
+  }
+
+  // Rescaling
+  var scale = null;
+
+  if (normalized) scale = N <= 2 ? null : 1 / ((N - 1) * (N - 2));
+  else scale = graph.type === 'undirected' ? 0.5 : null;
+
+  if (scale !== null) {
+    for (i = 0; i < N; i++) centralities[i] *= scale;
+  }
+
+  if (assign) return brandes.index.assign(outputName, centralities);
+
+  return brandes.index.collect(centralities);
+}
+
+/**
+ * Exporting.
+ */
+var betweennessCentrality = abstractBetweennessCentrality.bind(null, false);
+betweennessCentrality.assign = abstractBetweennessCentrality.bind(null, true);
+
+module.exports = betweennessCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9b134af57b27ea325cb6b6ffe10783b6bfcdaa32
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.d.ts
@@ -0,0 +1,20 @@
+import Graph from 'graphology-types';
+
+type ClosenessCentralityOptions = {
+  nodeCentralityAttribute?: string;
+  wassermanFaust?: boolean;
+};
+
+type ClosenessCentralityMapping = {[node: string]: number};
+
+interface ClosenessCentrality {
+  (
+    graph: Graph,
+    options?: ClosenessCentralityOptions
+  ): ClosenessCentralityMapping;
+  assign(graph: Graph, options?: ClosenessCentralityOptions): void;
+}
+
+declare const closenessCentrality: ClosenessCentrality;
+
+export default closenessCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.js
new file mode 100644
index 0000000000000000000000000000000000000000..62849e49eaa979c79452895137e8fbd8150ea7b8
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/closeness.js
@@ -0,0 +1,149 @@
+/**
+ * Graphology Closeness Centrality
+ * ================================
+ *
+ * JavaScript implementation of the closeness centrality
+ *
+ * [References]:
+ * https://en.wikipedia.org/wiki/Closeness_centrality
+ *
+ * Linton C. Freeman: Centrality in networks: I.
+ * Conceptual clarification. Social Networks 1:215-239, 1979.
+ * https://doi.org/10.1016/0378-8733(78)90021-7
+ *
+ * pg. 201 of Wasserman, S. and Faust, K.,
+ * Social Network Analysis: Methods and Applications, 1994,
+ * Cambridge University Press.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var resolveDefaults = require('graphology-utils/defaults');
+var FixedDeque = require('mnemonist/fixed-deque');
+var SparseSet = require('mnemonist/sparse-set');
+var NeighborhoodIndex =
+  require('graphology-indices/neighborhood').NeighborhoodIndex;
+
+// TODO: can be computed for a single node
+// TODO: weighted
+// TODO: abstract the single source indexed shortest path in lib
+// TODO: what about self loops?
+// TODO: refactor a BFSQueue working on integer ranges in graphology-indices?
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  nodeCentralityAttribute: 'closenessCentrality',
+  wassermanFaust: false
+};
+
+/**
+ * Helpers.
+ */
+function IndexedBFS(graph) {
+  this.index = new NeighborhoodIndex(graph, 'inbound');
+  this.queue = new FixedDeque(Array, graph.order);
+  this.seen = new SparseSet(graph.order);
+}
+
+IndexedBFS.prototype.fromNode = function (i) {
+  var index = this.index;
+  var queue = this.queue;
+  var seen = this.seen;
+
+  seen.clear();
+  queue.clear();
+
+  seen.add(i);
+  queue.push([i, 0]);
+
+  var item, n, d, j, l, neighbor;
+
+  var total = 0;
+  var count = 0;
+
+  while (queue.size !== 0) {
+    item = queue.shift();
+    n = item[0];
+    d = item[1];
+
+    if (d !== 0) {
+      total += d;
+      count += 1;
+    }
+
+    l = index.starts[n + 1];
+
+    for (j = index.starts[n]; j < l; j++) {
+      neighbor = index.neighborhood[j];
+
+      if (seen.has(neighbor)) continue;
+
+      seen.add(neighbor);
+      queue.push([neighbor, d + 1]);
+    }
+  }
+
+  return [count, total];
+};
+
+/**
+ * Abstract function computing the closeness centrality of a graph's nodes.
+ *
+ * @param  {boolean}  assign        - Should we assign the result to nodes.
+ * @param  {Graph}    graph         - Target graph.
+ * @param  {?object}  option        - Options:
+ * @param  {?string}   nodeCentralityAttribute - Name of the centrality attribute to assign.
+ * @param  {?boolean}  wassermanFaust - Whether to compute the Wasserman & Faust
+ *                                      variant of the metric.
+ * @return {object|undefined}
+ */
+function abstractClosenessCentrality(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/centrality/closeness: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var wassermanFaust = options.wassermanFaust;
+
+  var bfs = new IndexedBFS(graph);
+
+  var N = graph.order;
+
+  var i, result, count, total, closeness;
+
+  var mapping = new Float64Array(N);
+
+  for (i = 0; i < N; i++) {
+    result = bfs.fromNode(i);
+    count = result[0];
+    total = result[1];
+
+    closeness = 0;
+
+    if (total > 0 && N > 1) {
+      closeness = count / total;
+
+      if (wassermanFaust) {
+        closeness *= count / (N - 1);
+      }
+    }
+
+    mapping[i] = closeness;
+  }
+
+  if (assign) {
+    return bfs.index.assign(options.nodeCentralityAttribute, mapping);
+  }
+
+  return bfs.index.collect(mapping);
+}
+
+/**
+ * Exporting.
+ */
+var closenessCentrality = abstractClosenessCentrality.bind(null, false);
+closenessCentrality.assign = abstractClosenessCentrality.bind(null, true);
+
+module.exports = closenessCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1cca2ba1502f3a7c573925b268288257a69e3c7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.d.ts
@@ -0,0 +1,18 @@
+import Graph from 'graphology-types';
+
+type DegreeCentralityOptions = {
+  nodeCentralityAttribute?: string;
+};
+
+type DegreeCentralityMapping = {[node: string]: number};
+
+interface IDegreeCentralityBase {
+  (graph: Graph, options?: DegreeCentralityOptions): DegreeCentralityMapping;
+  assign(graph: Graph, options?: DegreeCentralityOptions): void;
+}
+
+declare const degreeCentrality: IDegreeCentralityBase;
+declare const inDegreeCentrality: IDegreeCentralityBase;
+declare const outDegreeCentrality: IDegreeCentralityBase;
+
+export {degreeCentrality, inDegreeCentrality, outDegreeCentrality};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca032275ee28c321eef93fc43c5bf0b85d15b476
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/degree.js
@@ -0,0 +1,98 @@
+/**
+ * Graphology Degree Centrality
+ * =============================
+ *
+ * Function computing degree centrality.
+ */
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Asbtract function to perform any kind of degree centrality.
+ *
+ * Intuitively, the degree centrality of a node is the fraction of nodes it
+ * is connected to.
+ *
+ * @param  {boolean} assign           - Whether to assign the result to the nodes.
+ * @param  {string}  method           - Method of the graph to get the degree.
+ * @param  {Graph}   graph            - A graphology instance.
+ * @param  {object}  [options]        - Options:
+ * @param  {string}    [nodeCentralityAttribute] - Name of the attribute to assign.
+ * @return {object|void}
+ */
+function abstractDegreeCentrality(assign, method, graph, options) {
+  var name = method + 'Centrality';
+
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-centrality/' +
+        name +
+        ': the given graph is not a valid graphology instance.'
+    );
+
+  if (method !== 'degree' && graph.type === 'undirected')
+    throw new Error(
+      'graphology-centrality/' +
+        name +
+        ': cannot compute ' +
+        method +
+        ' centrality on an undirected graph.'
+    );
+
+  // Solving options
+  options = options || {};
+
+  var centralityAttribute = options.nodeCentralityAttribute || name;
+
+  var ratio = graph.order - 1;
+  var getDegree = graph[method].bind(graph);
+
+  if (assign) {
+    graph.updateEachNodeAttributes(
+      function (node, attr) {
+        attr[centralityAttribute] = getDegree(node) / ratio;
+        return attr;
+      },
+      {attributes: [centralityAttribute]}
+    );
+
+    return;
+  }
+
+  var centralities = {};
+
+  graph.forEachNode(function (node) {
+    centralities[node] = getDegree(node) / ratio;
+  });
+
+  return centralities;
+}
+
+/**
+ * Building various functions to export.
+ */
+var degreeCentrality = abstractDegreeCentrality.bind(null, false, 'degree');
+var inDegreeCentrality = abstractDegreeCentrality.bind(null, false, 'inDegree');
+var outDegreeCentrality = abstractDegreeCentrality.bind(
+  null,
+  false,
+  'outDegree'
+);
+
+degreeCentrality.assign = abstractDegreeCentrality.bind(null, true, 'degree');
+inDegreeCentrality.assign = abstractDegreeCentrality.bind(
+  null,
+  true,
+  'inDegree'
+);
+outDegreeCentrality.assign = abstractDegreeCentrality.bind(
+  null,
+  true,
+  'outDegree'
+);
+
+/**
+ * Exporting.
+ */
+exports.degreeCentrality = degreeCentrality;
+exports.inDegreeCentrality = inDegreeCentrality;
+exports.outDegreeCentrality = outDegreeCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc19ee3961caa20cf9596658f14b3b4a5e52a8ea
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.d.ts
@@ -0,0 +1,37 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+
+type EigenvectorCentralityOptions<
+  EdgeAttributes extends Attributes = Attributes
+> = {
+  nodeCentralityAttribute?: string;
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+    | null;
+  maxIterations?: number;
+  tolerance?: number;
+};
+
+type EigenvectorCentralityMapping = {[node: string]: number};
+
+interface EigenvectorCentrality {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: EigenvectorCentralityOptions<EdgeAttributes>
+  ): EigenvectorCentralityMapping;
+  assign<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: EigenvectorCentralityOptions<EdgeAttributes>
+  ): void;
+}
+
+declare const eigenvectorCentrality: EigenvectorCentrality;
+
+export default eigenvectorCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b59227dce2f3f3ae80f30e83167734496b7f3bd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/eigenvector.js
@@ -0,0 +1,150 @@
+/**
+ * Graphology Eigenvector Centrality
+ * ==================================
+ *
+ * JavaScript implementation of the eigenvector centrality.
+ *
+ * [References]:
+ * https://en.wikipedia.org/wiki/Eigenvector_centrality
+ *
+ * Phillip Bonacich. "Power and Centrality: A Family of Measures."
+ * American Journal of Sociology, 92(5):1170–1182, 1986
+ * http://www.leonidzhukov.net/hse/2014/socialnetworks/papers/Bonacich-Centrality.pdf
+ *
+ * Mark E. J. Newman.
+ * Networks: An Introduct *
+ * Oxford University Press, USA, 2010, pp. 169.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var resolveDefaults = require('graphology-utils/defaults');
+var WeightedNeighborhoodIndex =
+  require('graphology-indices/neighborhood').WeightedNeighborhoodIndex;
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  nodeCentralityAttribute: 'eigenvectorCentrality',
+  getEdgeWeight: 'weight',
+  maxIterations: 100,
+  tolerance: 1e-6
+};
+
+/**
+ * Helpers.
+ */
+function safeVariadicHypot(x) {
+  var max = 0;
+  var s = 0;
+
+  for (var i = 0, l = x.length; i < l; i++) {
+    var n = Math.abs(x[i]);
+
+    if (n > max) {
+      s *= (max / n) * (max / n);
+      max = n;
+    }
+    s += n === 0 && max === 0 ? 0 : (n / max) * (n / max);
+  }
+
+  // NOTE: In case of numerical error we'll assume the norm is 1 in our case!
+  return max === Infinity ? 1 : max * Math.sqrt(s);
+}
+
+/**
+ * Abstract function computing the eigenvector centrality of a graph's nodes.
+ *
+ * @param  {boolean}  assign        - Should we assign the result to nodes.
+ * @param  {Graph}    graph         - Target graph.
+ * @param  {?object}  option        - Options:
+ * @param  {?string}    nodeCentralityAttribute - Name of the centrality attribute to assign.
+ * @param  {?string}    getEdgeWeight - Name of the weight algorithm or getter function.
+ * @param  {?number}    maxIterations - Maximum number of iterations to perform.
+ * @param  {?number}    tolerance     - Error tolerance when checking for convergence.
+ * @return {object|undefined}
+ */
+function abstractEigenvectorCentrality(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/centrality/eigenvector: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var maxIterations = options.maxIterations;
+  var tolerance = options.tolerance;
+
+  var N = graph.order;
+
+  var index = new WeightedNeighborhoodIndex(graph, options.getEdgeWeight);
+
+  var i, j, l, w;
+
+  var x = new Float64Array(graph.order);
+
+  // Initializing
+  for (i = 0; i < N; i++) {
+    x[i] = 1 / N;
+  }
+
+  // Power iterations
+  var iteration = 0;
+  var error = 0;
+  var neighbor, xLast, norm;
+  var converged = false;
+
+  while (iteration < maxIterations) {
+    xLast = x;
+    x = new Float64Array(xLast);
+
+    for (i = 0; i < N; i++) {
+      l = index.starts[i + 1];
+
+      for (j = index.starts[i]; j < l; j++) {
+        neighbor = index.neighborhood[j];
+        w = index.weights[j];
+        x[neighbor] += xLast[i] * w;
+      }
+    }
+
+    norm = safeVariadicHypot(x);
+
+    for (i = 0; i < N; i++) {
+      x[i] /= norm;
+    }
+
+    // Checking convergence
+    error = 0;
+
+    for (i = 0; i < N; i++) {
+      error += Math.abs(x[i] - xLast[i]);
+    }
+
+    if (error < N * tolerance) {
+      converged = true;
+      break;
+    }
+
+    iteration++;
+  }
+
+  if (!converged)
+    throw Error(
+      'graphology-metrics/centrality/eigenvector: failed to converge.'
+    );
+
+  if (assign) {
+    index.assign(options.nodeCentralityAttribute, x);
+    return;
+  }
+
+  return index.collect(x);
+}
+
+/**
+ * Exporting.
+ */
+var eigenvectorCentrality = abstractEigenvectorCentrality.bind(null, false);
+eigenvectorCentrality.assign = abstractEigenvectorCentrality.bind(null, true);
+
+module.exports = eigenvectorCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..101eb8079a0955a7eb36f82515c0468e97ca9bd9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.d.ts
@@ -0,0 +1,42 @@
+import Graph, {Attributes, EdgeMapper} from 'graphology-types';
+
+type HITSOptions<
+  NodeAttributes extends Attributes,
+  EdgeAttributes extends Attributes
+> = {
+  nodeAuthorityAttribute?: string;
+  nodeHubAttribute?: string;
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | EdgeMapper<number, NodeAttributes, EdgeAttributes>
+    | null;
+  maxIterations?: number;
+  normalize?: boolean;
+  tolerance?: number;
+};
+
+type HITSResult = {
+  authorities: {[node: string]: number};
+  hubs: {[node: string]: number};
+};
+
+interface HITS {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: HITSOptions<NodeAttributes, EdgeAttributes>
+  ): HITSResult;
+  assign<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph,
+    options?: HITSOptions<NodeAttributes, EdgeAttributes>
+  ): void;
+}
+
+declare const hits: HITS;
+
+export default hits;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.js
new file mode 100644
index 0000000000000000000000000000000000000000..48765448f7af68a08e8c50a7187e0d3591e8c311
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/hits.js
@@ -0,0 +1,203 @@
+/**
+ * Graphology HITS Algorithm
+ * ==========================
+ *
+ * Implementation of the HITS algorithm for the graphology specs.
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+
+// TODO: optimize using NeighborhoodIndex
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  nodeAuthorityAttribute: 'authority',
+  nodeHubAttribute: 'hub',
+  getEdgeWeight: 'weight',
+  maxIterations: 100,
+  normalize: true,
+  tolerance: 1e-8
+};
+
+/**
+ * Function returning an object with the given keys set to the given value.
+ *
+ * @param  {array}  keys  - Keys to set.
+ * @param  {number} value - Value to set.
+ * @return {object}       - The created object.
+ */
+function dict(keys, value) {
+  var o = Object.create(null);
+
+  var i, l;
+
+  for (i = 0, l = keys.length; i < l; i++) o[keys[i]] = value;
+
+  return o;
+}
+
+/**
+ * Function returning the sum of an object's values.
+ *
+ * @param  {object} o - Target object.
+ * @return {number}   - The sum.
+ */
+function sum(o) {
+  var nb = 0;
+
+  for (var k in o) nb += o[k];
+
+  return nb;
+}
+
+/**
+ * HITS function taking a Graph instance & some options and returning a map
+ * of nodes to their hubs & authorities.
+ *
+ * @param  {boolean} assign    - Should we assign the results as node attributes?
+ * @param  {Graph}   graph     - A Graph instance.
+ * @param  {object}  [options] - Options:
+ * @param  {number}    [maxIterations] - Maximum number of iterations to perform.
+ * @param  {boolean}   [normalize]     - Whether to normalize the results by the
+ *                                       sum of all values.
+ * @param  {number}    [tolerance]     - Error tolerance used to check
+ *                                       convergence in power method iteration.
+ */
+function hits(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-hits: the given graph is not a valid graphology instance.'
+    );
+
+  if (graph.multi)
+    throw new Error(
+      'graphology-hits: the HITS algorithm does not work with MultiGraphs.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var getEdgeWeight = createEdgeWeightGetter(options.getEdgeWeight).fromEntry;
+
+  // Variables
+  var order = graph.order;
+  var nodes = graph.nodes();
+  var edges;
+  var hubs = dict(nodes, 1 / order);
+  var weights = {};
+  var converged = false;
+  var lastHubs;
+  var authorities;
+
+  // Iteration variables
+  var node, neighbor, edge, iteration, maxAuthority, maxHub, error, S, i, j, m;
+
+  // Indexing weights
+  graph.forEachEdge(function (e, a, s, t, sa, ta, u) {
+    weights[e] = getEdgeWeight(e, a, s, t, sa, ta, u);
+  });
+
+  // Performing iterations
+  for (iteration = 0; iteration < options.maxIterations; iteration++) {
+    lastHubs = hubs;
+    hubs = dict(nodes, 0);
+    authorities = dict(nodes, 0);
+    maxHub = 0;
+    maxAuthority = 0;
+
+    // Iterating over nodes to update authorities
+    for (i = 0; i < order; i++) {
+      node = nodes[i];
+      edges = graph.outboundEdges(node);
+
+      // Iterating over neighbors
+      for (j = 0, m = edges.length; j < m; j++) {
+        edge = edges[j];
+        neighbor = graph.opposite(node, edge);
+
+        authorities[neighbor] += lastHubs[node] * weights[edge];
+
+        if (authorities[neighbor] > maxAuthority)
+          maxAuthority = authorities[neighbor];
+      }
+    }
+
+    // Iterating over nodes to update hubs
+    for (i = 0; i < order; i++) {
+      node = nodes[i];
+      edges = graph.outboundEdges(node);
+
+      for (j = 0, m = edges.length; j < m; j++) {
+        edge = edges[j];
+        neighbor = graph.opposite(node, edge);
+
+        hubs[node] += authorities[neighbor] * weights[edge];
+
+        if (hubs[neighbor] > maxHub) maxHub = hubs[neighbor];
+      }
+    }
+
+    // Normalizing
+    S = 1 / maxHub;
+
+    for (node in hubs) hubs[node] *= S;
+
+    S = 1 / maxAuthority;
+
+    for (node in authorities) authorities[node] *= S;
+
+    // Checking convergence
+    error = 0;
+
+    for (node in hubs) error += Math.abs(hubs[node] - lastHubs[node]);
+
+    if (error < options.tolerance) {
+      converged = true;
+      break;
+    }
+  }
+
+  if (!converged)
+    throw Error('graphology-metrics/centrality/hits: failed to converge.');
+
+  // Should we normalize the result?
+  if (options.normalize) {
+    S = 1 / sum(authorities);
+
+    for (node in authorities) authorities[node] *= S;
+
+    S = 1 / sum(hubs);
+
+    for (node in hubs) hubs[node] *= S;
+  }
+
+  // Should we assign the results to the graph?
+  if (assign) {
+    graph.updateEachNodeAttributes(
+      function (n, attr) {
+        attr[options.nodeAuthorityAttribute] = authorities[n];
+        attr[options.nodeHubAttribute] = hubs[n];
+
+        return attr;
+      },
+      {
+        attributes: [options.nodeAuthorityAttribute, options.nodeHubAttribute]
+      }
+    );
+
+    return;
+  }
+
+  return {hubs: hubs, authorities: authorities};
+}
+
+/**
+ * Exporting.
+ */
+var main = hits.bind(null, false);
+main.assign = hits.bind(null, true);
+
+module.exports = main;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f657016b41d7d7fdf72675d56c1a332dafa1e5d3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.d.ts
@@ -0,0 +1,10 @@
+export {default as betweenness} from './betweenness';
+export {default as closeness} from './closeness';
+export {
+  degreeCentrality as degree,
+  inDegreeCentrality as inDegree,
+  outDegreeCentrality as outDegree
+} from './degree';
+export {default as eigenvector} from './eigenvector';
+export {default as hits} from './hits';
+export {default as pagerank} from './pagerank';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e277e6f2235494b8c499e71c0bbed3c81943fe1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/index.js
@@ -0,0 +1,17 @@
+/**
+ * Graphology Metrics Centrality
+ * ==============================
+ *
+ * Sub module endpoint.
+ */
+var degree = require('./degree.js');
+
+exports.betweenness = require('./betweenness.js');
+exports.closeness = require('./closeness.js');
+exports.eigenvector = require('./eigenvector.js');
+exports.hits = require('./hits.js');
+exports.pagerank = require('./pagerank.js');
+
+exports.degree = degree.degreeCentrality;
+exports.inDegree = degree.inDegreeCentrality;
+exports.outDegree = degree.outDegreeCentrality;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..870daae87264ad772b2125780168ae1d3d717edc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.d.ts
@@ -0,0 +1,36 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+
+type PagerankOptions<EdgeAttributes extends Attributes> = {
+  nodePagerankAttribute?: string;
+  getEdgeWeight:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+    | null;
+  alpha?: number;
+  maxIterations?: number;
+  tolerance?: number;
+};
+
+type PagerankMapping = {[node: string]: number};
+
+interface Pagerank {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: PagerankOptions<EdgeAttributes>
+  ): PagerankMapping;
+  assign<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: PagerankOptions<EdgeAttributes>
+  ): void;
+}
+
+declare const pagerank: Pagerank;
+
+export default pagerank;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.js b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c5b688dbf931f304cfcbda7edc181414b8d7466
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/centrality/pagerank.js
@@ -0,0 +1,141 @@
+/**
+ * Graphology Pagerank
+ * ====================
+ *
+ * JavaScript implementation of the pagerank algorithm for graphology.
+ *
+ * [Reference]:
+ * Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry,
+ * The PageRank citation ranking: Bringing order to the Web. 1999
+ */
+var isGraph = require('graphology-utils/is-graph');
+var resolveDefaults = require('graphology-utils/defaults');
+var WeightedNeighborhoodIndex =
+  require('graphology-indices/neighborhood').WeightedNeighborhoodIndex;
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  nodePagerankAttribute: 'pagerank',
+  getEdgeWeight: 'weight',
+  alpha: 0.85,
+  maxIterations: 100,
+  tolerance: 1e-6
+};
+
+/**
+ * Abstract function applying the pagerank algorithm to the given graph.
+ *
+ * @param  {boolean}  assign        - Should we assign the result to nodes.
+ * @param  {Graph}    graph         - Target graph.
+ * @param  {?object}  option        - Options:
+ * @param  {?object}    attributes  - Custom attribute names:
+ * @param  {?string}      pagerank  - Name of the pagerank attribute to assign.
+ * @param  {?string}      weight    - Name of the weight algorithm.
+ * @param  {?number}  alpha         - Damping parameter.
+ * @param  {?number}  maxIterations - Maximum number of iterations to perform.
+ * @param  {?number}  tolerance     - Error tolerance when checking for convergence.
+ * @param  {?boolean} weighted      - Should we use the graph's weights.
+ * @return {object|undefined}
+ */
+function abstractPagerank(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/centrality/pagerank: the given graph is not a valid graphology instance.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var alpha = options.alpha;
+  var maxIterations = options.maxIterations;
+  var tolerance = options.tolerance;
+
+  var pagerankAttribute = options.nodePagerankAttribute;
+
+  var N = graph.order;
+  var p = 1 / N;
+
+  var index = new WeightedNeighborhoodIndex(graph, options.getEdgeWeight);
+
+  var i, j, l, d;
+
+  var x = new Float64Array(graph.order);
+
+  // Normalizing edge weights & indexing dangling nodes
+  var normalizedEdgeWeights = new Float64Array(index.weights.length);
+  var danglingNodes = [];
+
+  for (i = 0; i < N; i++) {
+    x[i] = p;
+    l = index.starts[i + 1];
+    d = index.outDegrees[i];
+
+    if (d === 0) danglingNodes.push(i);
+
+    for (j = index.starts[i]; j < l; j++) {
+      normalizedEdgeWeights[j] = index.weights[j] / d;
+    }
+  }
+
+  // Power iterations
+  var iteration = 0;
+  var error = 0;
+  var dangleSum, neighbor, xLast;
+  var converged = false;
+
+  while (iteration < maxIterations) {
+    xLast = x;
+    x = new Float64Array(graph.order); // TODO: it should be possible to swap two arrays to avoid allocations (bench)
+
+    dangleSum = 0;
+
+    for (i = 0, l = danglingNodes.length; i < l; i++)
+      dangleSum += xLast[danglingNodes[i]];
+
+    dangleSum *= alpha;
+
+    for (i = 0; i < N; i++) {
+      l = index.starts[i + 1];
+
+      for (j = index.starts[i]; j < l; j++) {
+        neighbor = index.neighborhood[j];
+        x[neighbor] += alpha * xLast[i] * normalizedEdgeWeights[j];
+      }
+
+      x[i] += dangleSum * p + (1 - alpha) * p;
+    }
+
+    // Checking convergence
+    error = 0;
+
+    for (i = 0; i < N; i++) {
+      error += Math.abs(x[i] - xLast[i]);
+    }
+
+    if (error < N * tolerance) {
+      converged = true;
+      break;
+    }
+
+    iteration++;
+  }
+
+  if (!converged)
+    throw Error('graphology-metrics/centrality/pagerank: failed to converge.');
+
+  if (assign) {
+    index.assign(pagerankAttribute, x);
+    return;
+  }
+
+  return index.collect(x);
+}
+
+/**
+ * Exporting.
+ */
+var pagerank = abstractPagerank.bind(null, false);
+pagerank.assign = abstractPagerank.bind(null, true);
+
+module.exports = pagerank;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..154d7bb1a7114541723293cdce5d98a44357ef70
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.d.ts
@@ -0,0 +1,35 @@
+import Graph, {Attributes, EdgeMapper} from 'graphology-types';
+
+export type DisparityOptions<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> = {
+  edgeDisparityAttribute?: string;
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | EdgeMapper<number, NodeAttributes, EdgeAttributes>;
+};
+
+export type DisparityMapping = {[edge: string]: number};
+
+interface IDisparity {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: DisparityOptions<NodeAttributes, EdgeAttributes>
+  ): DisparityMapping;
+
+  assign<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: DisparityOptions<NodeAttributes, EdgeAttributes>
+  ): void;
+}
+
+declare const disparity: IDisparity;
+
+export default disparity;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.js b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.js
new file mode 100644
index 0000000000000000000000000000000000000000..b50b08b1a6ec1284cbd8eb2c3af2d60bb463fb76
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/disparity.js
@@ -0,0 +1,158 @@
+/**
+ * Graphology Edge Disparity
+ * ==========================
+ *
+ * Function computing the disparity score of each edge in the given graph. This
+ * score is typically used to extract the multiscale backbone of a weighted
+ * graph.
+ *
+ * The formula from the paper (relying on integral calculus) can be simplified
+ * to become:
+ *
+ *   disparity(u, v) = min(
+ *     (1 - normalizedWeight(u, v)) ^ (degree(u) - 1)),
+ *     (1 - normalizedWeight(v, u)) ^ (degree(v) - 1))
+ *   )
+ *
+ *   where normalizedWeight(u, v) = weight(u, v) / weightedDegree(u)
+ *   where weightedDegree(u) = sum(weight(u, v) for v in neighbors(u))
+ *
+ * This score can sometimes be found reversed likewise:
+ *
+ *   disparity(u, v) = max(
+ *     1 - (1 - normalizedWeight(u, v)) ^ (degree(u) - 1)),
+ *     1 - (1 - normalizedWeight(v, u)) ^ (degree(v) - 1))
+ *   )
+ *
+ * so that higher score means better edges. I chose to keep the metric close
+ * to the paper to keep the statistical test angle. This means that, in my
+ * implementation at least, a low score for an edge means a high relevance and
+ * increases its chances to be kept in the backbone.
+ *
+ * Note that this algorithm has no proper definition for directed graphs and
+ * is only useful if edges have varying weights. This said, it could be
+ * possible to compute the disparity score only based on edge direction, if
+ * we drop the min part.
+ *
+ * [Article]:
+ * Serrano, M. Ángeles, Marián Boguná, and Alessandro Vespignani. "Extracting
+ * the multiscale backbone of complex weighted networks." Proceedings of the
+ * national academy of sciences 106.16 (2009): 6483-6488.
+ *
+ * [Reference]:
+ * https://www.pnas.org/content/pnas/106/16/6483.full.pdf
+ * https://en.wikipedia.org/wiki/Disparity_filter_algorithm_of_weighted_network
+ */
+var isGraph = require('graphology-utils/is-graph');
+var inferType = require('graphology-utils/infer-type');
+var resolveDefaults = require('graphology-utils/defaults');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  edgeDisparityAttribute: 'disparity',
+  getEdgeWeight: 'weight'
+};
+
+// TODO: test without weight, to see what happens
+
+function abstractDisparity(assign, graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/edge/disparity: the given graph is not a valid graphology instance.'
+    );
+
+  if (graph.multi || inferType(graph) === 'mixed')
+    throw new Error(
+      'graphology-metrics/edge/disparity: not defined for multi nor mixed graphs.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  var getEdgeWeight = createEdgeWeightGetter(options.getEdgeWeight).fromEntry;
+
+  // Computing node weighted degrees
+  var weightedDegrees = {};
+
+  graph.forEachNode(function (node) {
+    weightedDegrees[node] = 0;
+  });
+
+  graph.forEachAssymetricAdjacencyEntry(function (
+    source,
+    target,
+    sa,
+    ta,
+    edge,
+    attr,
+    undirected
+  ) {
+    var weight = getEdgeWeight(edge, attr, source, target, sa, ta, undirected);
+
+    weightedDegrees[source] += weight;
+    weightedDegrees[target] += weight;
+  });
+
+  // Computing edge disparity
+  var previous, previousDegree, previousWeightedDegree;
+
+  var disparities = {};
+
+  graph.forEachAssymetricAdjacencyEntry(function (
+    source,
+    target,
+    sa,
+    ta,
+    edge,
+    attr,
+    undirected
+  ) {
+    var weight = getEdgeWeight(edge, attr, source, target, sa, ta, undirected);
+
+    if (previous !== source) {
+      previous = source;
+      previousDegree = graph.degree(source);
+      previousWeightedDegree = weightedDegrees[source];
+    }
+
+    var targetDegree = graph.degree(target);
+    var targetWeightedDegree = weightedDegrees[target];
+
+    var normalizedWeightPerSource = weight / previousWeightedDegree;
+    var normalizedWeightPerTarget = weight / targetWeightedDegree;
+
+    var sourceScore = Math.pow(
+      1 - normalizedWeightPerSource,
+      previousDegree - 1
+    );
+
+    var targetScore = Math.pow(1 - normalizedWeightPerTarget, targetDegree - 1);
+
+    disparities[edge] = Math.min(sourceScore, targetScore);
+  });
+
+  if (assign) {
+    graph.updateEachEdgeAttributes(
+      function (edge, attr) {
+        attr[options.edgeDisparityAttribute] = disparities[edge];
+        return attr;
+      },
+      {attributes: [options.edgeDisparityAttribute]}
+    );
+
+    return;
+  }
+
+  return disparities;
+}
+
+var disparity = abstractDisparity.bind(null, false);
+disparity.assign = abstractDisparity.bind(null, true);
+
+/**
+ * Exporting.
+ */
+module.exports = disparity;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6aa20a63d233a54316eb35e647df954abadec93e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.d.ts
@@ -0,0 +1,2 @@
+export {default as disparity} from './disparity';
+export {default as simmelianStrength} from './simmelian-strength';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..70062a35ef76ebfbe4c8198dd2e7a694e71b732e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/index.js
@@ -0,0 +1,2 @@
+exports.disparity = require('./disparity.js');
+exports.simmelianStrength = require('./simmelian-strength.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..898a01719d107091a98c4b3986e80f7c466f275b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.d.ts
@@ -0,0 +1,12 @@
+import Graph from 'graphology-types';
+
+type SimmelianStrengthMapping = {[edge: string]: number};
+
+interface ISimmelianStrength {
+  (graph: Graph): SimmelianStrengthMapping;
+  assign(graph: Graph): void;
+}
+
+declare const simmelianStrength: ISimmelianStrength;
+
+export default simmelianStrength;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.js b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.js
new file mode 100644
index 0000000000000000000000000000000000000000..7bd65ab60c6c450847d2b3d6b7b2d69d4cc6f082
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/edge/simmelian-strength.js
@@ -0,0 +1,53 @@
+/**
+ * Graphology Simmelian Strength
+ * ==============================
+ *
+ * Function computing the Simmelian strength, i.e. the number of triangles in
+ * which an edge stands, for each edge in a given graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var intersectionSize = require('mnemonist/set').intersectionSize;
+
+function abstractSimmelianStrength(assign, graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/simmelian-strength: given graph is not a valid graphology instance.'
+    );
+
+  // Indexing neighborhoods
+  var neighborhoods = {};
+
+  graph.forEachNode(function (node) {
+    neighborhoods[node] = new Set(graph.neighbors(node));
+  });
+
+  if (!assign) {
+    var strengths = {};
+
+    graph.forEachEdge(function (edge, _, source, target) {
+      strengths[edge] = intersectionSize(
+        neighborhoods[source],
+        neighborhoods[target]
+      );
+    });
+
+    return strengths;
+  }
+
+  graph.updateEachEdgeAttributes(
+    function (_, attr, source, target) {
+      attr.simmelianStrength = intersectionSize(
+        neighborhoods[source],
+        neighborhoods[target]
+      );
+
+      return attr;
+    },
+    {attributes: ['simmelianStrength']}
+  );
+}
+
+var simmelianStrength = abstractSimmelianStrength.bind(null, false);
+simmelianStrength.assign = abstractSimmelianStrength.bind(null, true);
+
+module.exports = simmelianStrength;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb356ca7d1ac9ded8ddce0293b66f7734474c6c6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.d.ts
@@ -0,0 +1,34 @@
+import Graph, {GraphType} from 'graphology-types';
+
+export function abstractDensity(
+  type: GraphType,
+  multi: boolean,
+  graph: Graph
+): number;
+export function abstractDensity(
+  type: GraphType,
+  multi: boolean,
+  order: number,
+  size: number
+): number;
+
+export function density(graph: Graph): number;
+export function density(order: number, size: number): number;
+
+export function directedDensity(graph: Graph): number;
+export function directedDensity(order: number, size: number): number;
+
+export function undirectedDensity(graph: Graph): number;
+export function undirectedDensity(order: number, size: number): number;
+
+export function mixedDensity(graph: Graph): number;
+export function mixedDensity(order: number, size: number): number;
+
+export function multiDirectedDensity(graph: Graph): number;
+export function multiDirectedDensity(order: number, size: number): number;
+
+export function multiUndirectedDensity(graph: Graph): number;
+export function multiUndirectedDensity(order: number, size: number): number;
+
+export function multiMixedDensity(graph: Graph): number;
+export function multiMixedDensity(order: number, size: number): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.js
new file mode 100644
index 0000000000000000000000000000000000000000..b751abaa96b8b8c31ccdf3ba81518b8f1581f943
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/density.js
@@ -0,0 +1,118 @@
+/**
+ * Graphology Density
+ * ===================
+ *
+ * Functions used to compute the density of the given graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var simpleSize = require('./simple-size.js');
+
+/**
+ * Returns the undirected density.
+ *
+ * @param  {number} order - Number of nodes in the graph.
+ * @param  {number} size  - Number of edges in the graph.
+ * @return {number}
+ */
+function undirectedDensity(order, size) {
+  return (2 * size) / (order * (order - 1));
+}
+
+/**
+ * Returns the directed density.
+ *
+ * @param  {number} order - Number of nodes in the graph.
+ * @param  {number} size  - Number of edges in the graph.
+ * @return {number}
+ */
+function directedDensity(order, size) {
+  return size / (order * (order - 1));
+}
+
+/**
+ * Returns the mixed density.
+ *
+ * @param  {number} order - Number of nodes in the graph.
+ * @param  {number} size  - Number of edges in the graph.
+ * @return {number}
+ */
+function mixedDensity(order, size) {
+  var d = order * (order - 1);
+
+  return size / (d + d / 2);
+}
+
+/**
+ * Returns the density for the given parameters.
+ *
+ * Arity 3:
+ * @param  {boolean} type  - Type of density.
+ * @param  {boolean} multi - Compute multi density?
+ * @param  {Graph}   graph - Target graph.
+ *
+ * Arity 4:
+ * @param  {boolean} type  - Type of density.
+ * @param  {boolean} multi - Compute multi density?
+ * @param  {number}  order - Number of nodes in the graph.
+ * @param  {number}  size  - Number of edges in the graph.
+ *
+ * @return {number}
+ */
+function abstractDensity(type, multi, graph) {
+  var order, size;
+
+  // Retrieving order & size
+  if (arguments.length > 3) {
+    order = graph;
+    size = arguments[3];
+
+    if (typeof order !== 'number' || order < 0)
+      throw new Error(
+        'graphology-metrics/density: given order is not a valid number.'
+      );
+
+    if (typeof size !== 'number' || size < 0)
+      throw new Error(
+        'graphology-metrics/density: given size is not a valid number.'
+      );
+  } else {
+    if (!isGraph(graph))
+      throw new Error(
+        'graphology-metrics/density: given graph is not a valid graphology instance.'
+      );
+
+    order = graph.order;
+    size = graph.size;
+
+    if (graph.multi && multi === false) size = simpleSize(graph);
+  }
+
+  // When the graph has only one node, its density is 0
+  if (order < 2) return 0;
+
+  // Guessing type & multi
+  if (type === null) type = graph.type;
+  if (multi === null) multi = graph.multi;
+
+  // Getting the correct function
+  var fn;
+
+  if (type === 'undirected') fn = undirectedDensity;
+  else if (type === 'directed') fn = directedDensity;
+  else fn = mixedDensity;
+
+  // Applying the function
+  return fn(order, size);
+}
+
+/**
+ * Exporting.
+ */
+exports.abstractDensity = abstractDensity;
+exports.density = abstractDensity.bind(null, null, null);
+exports.directedDensity = abstractDensity.bind(null, 'directed', false);
+exports.undirectedDensity = abstractDensity.bind(null, 'undirected', false);
+exports.mixedDensity = abstractDensity.bind(null, 'mixed', false);
+exports.multiDirectedDensity = abstractDensity.bind(null, 'directed', true);
+exports.multiUndirectedDensity = abstractDensity.bind(null, 'undirected', true);
+exports.multiMixedDensity = abstractDensity.bind(null, 'mixed', true);
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33e3b20bb4d32f2cac9ecfbb34acc019010dde6e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function diameter(graph: Graph): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f7f3abc075c4d089123c99222d1013fd1e25a57
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/diameter.js
@@ -0,0 +1,30 @@
+/**
+ * Graphology Diameter
+ * ========================
+ *
+ * Functions used to compute the diameter of the given graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var eccentricity = require('../node/eccentricity.js');
+
+module.exports = function diameter(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/diameter: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.size === 0) return Infinity;
+
+  var max = -Infinity;
+
+  graph.someNode(function (node) {
+    var e = eccentricity(graph, node);
+
+    if (e > max) max = e;
+
+    // If the graph is not connected, its diameter is infinite
+    return max === Infinity;
+  });
+
+  return max;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9d41a9ea167fd46d448f429e2ea666334579f684
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.d.ts
@@ -0,0 +1,30 @@
+import Graph, {Attributes} from 'graphology-types';
+
+export type Extent = [number, number];
+export type ExtentMapping = {[name: string]: Extent};
+
+export function nodeExtent<NodeAttributes extends Attributes = Attributes>(
+  graph: Graph<NodeAttributes>,
+  attribute: keyof NodeAttributes
+): Extent;
+
+export function nodeExtent<NodeAttributes extends Attributes = Attributes>(
+  graph: Graph<NodeAttributes>,
+  attributes: Array<keyof NodeAttributes>
+): ExtentMapping;
+
+export function edgeExtent<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  attribute: keyof EdgeAttributes
+): Extent;
+
+export function edgeExtent<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  attributes: Array<keyof EdgeAttributes>
+): ExtentMapping;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.js
new file mode 100644
index 0000000000000000000000000000000000000000..0065d26f153f6f5a5334f1b178646f19bf18af99
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/extent.js
@@ -0,0 +1,91 @@
+/**
+ * Graphology Extent
+ * ==================
+ *
+ * Simple function returning the extent of selected attributes of the graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Function returning the extent of the selected node attributes.
+ *
+ * @param  {Graph}        graph     - Target graph.
+ * @param  {string|array} attribute - Single or multiple attributes.
+ * @return {array|object}
+ */
+function nodeExtent(graph, attribute) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/extent: the given graph is not a valid graphology instance.'
+    );
+
+  var attributes = [].concat(attribute);
+
+  var value, key, a;
+
+  var results = {};
+
+  for (a = 0; a < attributes.length; a++) {
+    key = attributes[a];
+
+    results[key] = [Infinity, -Infinity];
+  }
+
+  graph.forEachNode(function (node, data) {
+    for (a = 0; a < attributes.length; a++) {
+      key = attributes[a];
+      value = data[key];
+
+      if (value < results[key][0]) results[key][0] = value;
+
+      if (value > results[key][1]) results[key][1] = value;
+    }
+  });
+
+  return typeof attribute === 'string' ? results[attribute] : results;
+}
+
+/**
+ * Function returning the extent of the selected edge attributes.
+ *
+ * @param  {Graph}        graph     - Target graph.
+ * @param  {string|array} attribute - Single or multiple attributes.
+ * @return {array|object}
+ */
+function edgeExtent(graph, attribute) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/extent: the given graph is not a valid graphology instance.'
+    );
+
+  var attributes = [].concat(attribute);
+
+  var value, key, a;
+
+  var results = {};
+
+  for (a = 0; a < attributes.length; a++) {
+    key = attributes[a];
+
+    results[key] = [Infinity, -Infinity];
+  }
+
+  graph.forEachEdge(function (edge, data) {
+    for (a = 0; a < attributes.length; a++) {
+      key = attributes[a];
+      value = data[key];
+
+      if (value < results[key][0]) results[key][0] = value;
+
+      if (value > results[key][1]) results[key][1] = value;
+    }
+  });
+
+  return typeof attribute === 'string' ? results[attribute] : results;
+}
+
+/**
+ * Exporting.
+ */
+exports.nodeExtent = nodeExtent;
+exports.edgeExtent = edgeExtent;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80d2ec5e4be266956b443b42b8e9c3423b53e7ab
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.d.ts
@@ -0,0 +1,15 @@
+export {
+  abstractDensity,
+  density,
+  undirectedDensity,
+  directedDensity,
+  mixedDensity,
+  multiUndirectedDensity,
+  multiDirectedDensity,
+  multiMixedDensity
+} from './density';
+export {default as diameter} from './diameter';
+export {nodeExtent, edgeExtent} from './extent';
+export {default as modularity} from './modularity';
+export {default as simpleSize} from './simple-size';
+export {default as weightedSize} from './weighted-size';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..74232769765714fd410d762e2068758b3c0cfac3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/index.js
@@ -0,0 +1,19 @@
+var density = require('./density.js');
+var extent = require('./extent.js');
+
+exports.diameter = require('./diameter.js');
+exports.modularity = require('./modularity.js');
+exports.simpleSize = require('./simple-size.js');
+exports.weightedSize = require('./weighted-size.js');
+
+exports.abstractDensity = density.abstractDensity;
+exports.density = density.density;
+exports.directedDensity = density.directedDensity;
+exports.undirectedDensity = density.undirectedDensity;
+exports.mixedDensity = density.mixedDensity;
+exports.multiDirectedDensity = density.multiDirectedDensity;
+exports.multiUndirectedDensity = density.multiUndirectedDensity;
+exports.multiMixedDensity = density.multiMixedDensity;
+
+exports.nodeExtent = extent.nodeExtent;
+exports.edgeExtent = extent.edgeExtent;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44ff73aa8ea67db73f900da6339960d6d63bddcc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.d.ts
@@ -0,0 +1,60 @@
+import Graph, {Attributes, NodeMapper, EdgeMapper} from 'graphology-types';
+
+type ModularityOptions<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> = {
+  getNodeCommunity?:
+    | keyof NodeAttributes
+    | NodeMapper<string | number, NodeAttributes>;
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | EdgeMapper<number, NodeAttributes, EdgeAttributes>
+    | null;
+  resolution?: number;
+};
+
+interface IModularity {
+  <
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: ModularityOptions<NodeAttributes, EdgeAttributes>
+  ): number;
+
+  dense<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: ModularityOptions<NodeAttributes, EdgeAttributes>
+  ): number;
+  sparse<
+    NodeAttributes extends Attributes = Attributes,
+    EdgeAttributes extends Attributes = Attributes
+  >(
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    options?: ModularityOptions<NodeAttributes, EdgeAttributes>
+  ): number;
+
+  undirectedDelta(
+    M: number,
+    communityTotalWeight: number,
+    nodeDegree: number,
+    nodeCommunityDegree: number
+  ): number;
+
+  directedDelta(
+    M: number,
+    communityTotalInWeight: number,
+    communityTotalOutWeight: number,
+    nodeInDegree: number,
+    nodeOutDegree: number,
+    nodeCommunityDegree: number
+  ): number;
+}
+
+declare const modularity: IModularity;
+
+export default modularity;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.js
new file mode 100644
index 0000000000000000000000000000000000000000..164af9ea5d5f00eb94b9b0b891b6a6d01d8b20b3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/modularity.js
@@ -0,0 +1,581 @@
+/**
+ * Graphology Modularity
+ * ======================
+ *
+ * Implementation of network modularity for graphology.
+ *
+ * Modularity is a bit of a tricky problem because there are a wide array
+ * of different definitions and implementations. The current implementation
+ * try to stay true to Newman's original definition and consider both the
+ * undirected & directed case as well as the weighted one. The current
+ * implementation should also be aligned with Louvain algorithm's definition
+ * of the metric.
+ *
+ * Regarding the directed version, one has to understand that the undirected
+ * version's is basically considering the graph as a directed one where all
+ * edges would be mutual.
+ *
+ * There is one exception to this, though: self loops. To conform with density's
+ * definition, as used in modularity's one, and to keep true to the matrix
+ * formulation of modularity, one has to note that self-loops only count once
+ * in both the undirected and directed cases. This means that a k-clique with
+ * one node having a self-loop will not have the same modularity in the
+ * undirected and mutual case. Indeed, in both cases the modularity of a
+ * k-clique with one loop and minus one internal edge should be equal.
+ *
+ * This also means that, as with the naive density formula regarding loops,
+ * one should increment M when considering a loop. Also, to remain coherent
+ * in this regard, degree should not be multiplied by two because of the loop
+ * else it will have too much importance regarding the considered proportions.
+ *
+ * Hence, here are the retained formulas:
+ *
+ * For dense weighted undirected network:
+ * --------------------------------------
+ *
+ * Q = 1/2m * [ ∑ij[Aij - (di.dj / 2m)] * ∂(ci, cj) ]
+ *
+ * where:
+ *  - i & j being a pair of nodes
+ *  - m is the sum of edge weights
+ *  - Aij being the weight of the ij edge (or 0 if absent)
+ *  - di being the weighted degree of node i
+ *  - ci being the community to which belongs node i
+ *  - ∂ is Kronecker's delta function (1 if x = y else 0)
+ *
+ * For dense weighted directed network:
+ * ------------------------------------
+ *
+ * Qd = 1/m * [ ∑ij[Aij - (dini.doutj / m)] * ∂(ci, cj) ]
+ *
+ * where:
+ *  - dini is the in degree of node i
+ *  - douti is the out degree of node i
+ *
+ * For sparse weighted undirected network:
+ * ---------------------------------------
+ *
+ * Q = ∑c[ (∑cinternal / 2m) - (∑ctotal / 2m)² ]
+ *
+ * where:
+ *  - c is a community
+ *  - ∑cinternal is the total weight of a community internal edges
+ *  - ∑ctotal is the total weight of edges connected to a community
+ *
+ * For sparse weighted directed network:
+ * -------------------------------------
+ *
+ * Qd = ∑c[ (∑cinternal / m) - (∑cintotal * ∑couttotal / m²) ]
+ *
+ * where:
+ *  - ∑cintotal is the total weight of edges pointing towards a community
+ *  - ∑couttotal is the total weight of edges going from a community
+ *
+ * Note that dense version run in O(N²) while sparse version runs in O(V). So
+ * the dense version is mostly here to guarantee the validity of the sparse one.
+ * As such it is not used as default.
+ *
+ * For undirected delta computation:
+ * ---------------------------------
+ *
+ * ∆Q = (dic / 2m) - ((∑ctotal * di) / 2m²)
+ *
+ * where:
+ *  - dic is the degree of the node in community c
+ *
+ * For directed delta computation:
+ * -------------------------------
+ *
+ * ∆Qd = (dic / m) - (((douti * ∑cintotal) + (dini * ∑couttotal)) / m²)
+ *
+ * Gephi's version of undirected delta computation:
+ * ------------------------------------------------
+ *
+ * ∆Qgephi = dic - (di * Ztot) / 2m
+ *
+ * Note that the above formula is erroneous and should really be:
+ *
+ * ∆Qgephi = dic - (di * Ztot) / m
+ *
+ * because then: ∆Qgephi = ∆Q * 2m
+ *
+ * It is used because it is faster to compute. Since Gephi's error is only by
+ * a constant factor, it does not make the result incorrect.
+ *
+ * [Latex]
+ *
+ * Sparse undirected
+ * Q = \sum_{c} \bigg{[} \frac{\sum\nolimits_{c\,in}}{2m} - \left(\frac{\sum\nolimits_{c\,tot}}{2m}\right )^2 \bigg{]}
+ *
+ * Sparse directed
+ * Q_d = \sum_{c} \bigg{[} \frac{\sum\nolimits_{c\,in}}{m} - \frac{\sum_{c\,tot}^{in}\sum_{c\,tot}^{out}}{m^2} \bigg{]}
+ *
+ * [Articles]
+ * M. E. J. Newman, « Modularity and community structure in networks »,
+ * Proc. Natl. Acad. Sci. USA, vol. 103, no 23, 2006, p. 8577–8582
+ * https://dx.doi.org/10.1073%2Fpnas.0601602103
+ *
+ * Newman, M. E. J. « Community detection in networks: Modularity optimization
+ * and maximum likelihood are equivalent ». Physical Review E, vol. 94, no 5,
+ * novembre 2016, p. 052315. arXiv.org, doi:10.1103/PhysRevE.94.052315.
+ * https://arxiv.org/pdf/1606.02319.pdf
+ *
+ * Blondel, Vincent D., et al. « Fast unfolding of communities in large
+ * networks ». Journal of Statistical Mechanics: Theory and Experiment,
+ * vol. 2008, no 10, octobre 2008, p. P10008. DOI.org (Crossref),
+ * doi:10.1088/1742-5468/2008/10/P10008.
+ * https://arxiv.org/pdf/0803.0476.pdf
+ *
+ * Nicolas Dugué, Anthony Perez. Directed Louvain: maximizing modularity in
+ * directed networks. [Research Report] Université d’Orléans. 2015. hal-01231784
+ * https://hal.archives-ouvertes.fr/hal-01231784
+ *
+ * R. Lambiotte, J.-C. Delvenne and M. Barahona. Laplacian Dynamics and
+ * Multiscale Modular Structure in Networks,
+ * doi:10.1109/TNSE.2015.2391998.
+ * https://arxiv.org/abs/0812.1770
+ *
+ * [Links]:
+ * https://math.stackexchange.com/questions/2637469/where-does-the-second-formula-of-modularity-comes-from-in-the-louvain-paper-the
+ * https://www.quora.com/How-is-the-formula-for-Louvain-modularity-change-derived
+ * https://github.com/gephi/gephi/blob/master/modules/StatisticsPlugin/src/main/java/org/gephi/statistics/plugin/Modularity.java
+ * https://github.com/igraph/igraph/blob/eca5e809aab1aa5d4eca1e381389bcde9cf10490/src/community.c#L906
+ */
+var resolveDefaults = require('graphology-utils/defaults');
+var isGraph = require('graphology-utils/is-graph');
+var inferType = require('graphology-utils/infer-type');
+var getters = require('graphology-utils/getters');
+
+var DEFAULTS = {
+  getNodeCommunity: 'community',
+  getEdgeWeight: 'weight',
+  resolution: 1
+};
+
+function collectForUndirectedDense(graph, options) {
+  var communities = new Array(graph.order);
+  var weightedDegrees = new Float64Array(graph.order);
+  var weights = {};
+  var M = 0;
+
+  var getEdgeWeight = getters.createEdgeWeightGetter(
+    options.getEdgeWeight
+  ).fromEntry;
+  var getNodeCommunity = getters.createNodeValueGetter(
+    options.getNodeCommunity
+  ).fromEntry;
+
+  // Collecting communities
+  var i = 0;
+  var ids = {};
+  graph.forEachNode(function (node, attr) {
+    ids[node] = i;
+    communities[i++] = getNodeCommunity(node, attr);
+  });
+
+  // Collecting weights
+  graph.forEachUndirectedEdge(function (edge, attr, source, target, sa, ta, u) {
+    var weight = getEdgeWeight(edge, attr, source, target, sa, ta, u);
+
+    M += weight;
+    weights[edge] = weight;
+
+    weightedDegrees[ids[source]] += weight;
+
+    // NOTE: we double degree only if we don't have a loop
+    if (source !== target) weightedDegrees[ids[target]] += weight;
+  });
+
+  return {
+    weights: weights,
+    communities: communities,
+    weightedDegrees: weightedDegrees,
+    M: M
+  };
+}
+
+function collectForDirectedDense(graph, options) {
+  var communities = new Array(graph.order);
+  var weightedInDegrees = new Float64Array(graph.order);
+  var weightedOutDegrees = new Float64Array(graph.order);
+  var weights = {};
+  var M = 0;
+
+  var getEdgeWeight = getters.createEdgeWeightGetter(
+    options.getEdgeWeight
+  ).fromEntry;
+  var getNodeCommunity = getters.createNodeValueGetter(
+    options.getNodeCommunity
+  ).fromEntry;
+
+  // Collecting communities
+  var i = 0;
+  var ids = {};
+  graph.forEachNode(function (node, attr) {
+    ids[node] = i;
+    communities[i++] = getNodeCommunity(node, attr);
+  });
+
+  // Collecting weights
+  graph.forEachDirectedEdge(function (edge, attr, source, target, sa, ta, u) {
+    var weight = getEdgeWeight(edge, attr, source, target, sa, ta, u);
+
+    M += weight;
+    weights[edge] = weight;
+
+    weightedOutDegrees[ids[source]] += weight;
+    weightedInDegrees[ids[target]] += weight;
+  });
+
+  return {
+    weights: weights,
+    communities: communities,
+    weightedInDegrees: weightedInDegrees,
+    weightedOutDegrees: weightedOutDegrees,
+    M: M
+  };
+}
+
+function undirectedDenseModularity(graph, options) {
+  var resolution = options.resolution;
+
+  var result = collectForUndirectedDense(graph, options);
+
+  var communities = result.communities;
+  var weightedDegrees = result.weightedDegrees;
+
+  var M = result.M;
+
+  var nodes = graph.nodes();
+
+  var i, j, l, Aij, didj, e;
+
+  var S = 0;
+
+  var M2 = M * 2;
+
+  for (i = 0, l = graph.order; i < l; i++) {
+    // NOTE: it is important to parse the whole matrix here, diagonal and
+    // lower part included. A lot of implementation differ here because
+    // they process only a part of the matrix
+    for (j = 0; j < l; j++) {
+      // NOTE: Kronecker's delta
+      // NOTE: we could go from O(n^2) to O(avg.C^2)
+      if (communities[i] !== communities[j]) continue;
+
+      e = graph.undirectedEdge(nodes[i], nodes[j]);
+
+      Aij = result.weights[e] || 0;
+      didj = weightedDegrees[i] * weightedDegrees[j];
+
+      // We add twice if we have a self loop
+      if (i === j && typeof e !== 'undefined')
+        S += (Aij - (didj / M2) * resolution) * 2;
+      else S += Aij - (didj / M2) * resolution;
+    }
+  }
+
+  return S / M2;
+}
+
+function directedDenseModularity(graph, options) {
+  var resolution = options.resolution;
+
+  var result = collectForDirectedDense(graph, options);
+
+  var communities = result.communities;
+  var weightedInDegrees = result.weightedInDegrees;
+  var weightedOutDegrees = result.weightedOutDegrees;
+
+  var M = result.M;
+
+  var nodes = graph.nodes();
+
+  var i, j, l, Aij, didj, e;
+
+  var S = 0;
+
+  for (i = 0, l = graph.order; i < l; i++) {
+    // NOTE: it is important to parse the whole matrix here, diagonal and
+    // lower part included. A lot of implementation differ here because
+    // they process only a part of the matrix
+    for (j = 0; j < l; j++) {
+      // NOTE: Kronecker's delta
+      // NOTE: we could go from O(n^2) to O(avg.C^2)
+      if (communities[i] !== communities[j]) continue;
+
+      e = graph.directedEdge(nodes[i], nodes[j]);
+
+      Aij = result.weights[e] || 0;
+      didj = weightedInDegrees[i] * weightedOutDegrees[j];
+
+      // Here we multiply by two to simulate iteration through lower part
+      S += Aij - (didj / M) * resolution;
+    }
+  }
+
+  return S / M;
+}
+
+function collectCommunitesForUndirected(graph, options) {
+  var communities = {};
+  var totalWeights = {};
+  var internalWeights = {};
+
+  var getNodeCommunity = getters.createNodeValueGetter(
+    options.getNodeCommunity
+  ).fromEntry;
+
+  graph.forEachNode(function (node, attr) {
+    var community = getNodeCommunity(node, attr);
+    communities[node] = community;
+
+    if (typeof community === 'undefined')
+      throw new Error(
+        'graphology-metrics/modularity: the "' +
+          node +
+          '" node is not in the partition.'
+      );
+
+    totalWeights[community] = 0;
+    internalWeights[community] = 0;
+  });
+
+  return {
+    communities: communities,
+    totalWeights: totalWeights,
+    internalWeights: internalWeights
+  };
+}
+
+function collectCommunitesForDirected(graph, options) {
+  var communities = {};
+  var totalInWeights = {};
+  var totalOutWeights = {};
+  var internalWeights = {};
+
+  var getNodeCommunity = getters.createNodeValueGetter(
+    options.getNodeCommunity
+  ).fromEntry;
+
+  graph.forEachNode(function (node, attr) {
+    var community = getNodeCommunity(node, attr);
+    communities[node] = community;
+
+    if (typeof community === 'undefined')
+      throw new Error(
+        'graphology-metrics/modularity: the "' +
+          node +
+          '" node is not in the partition.'
+      );
+
+    totalInWeights[community] = 0;
+    totalOutWeights[community] = 0;
+    internalWeights[community] = 0;
+  });
+
+  return {
+    communities: communities,
+    totalInWeights: totalInWeights,
+    totalOutWeights: totalOutWeights,
+    internalWeights: internalWeights
+  };
+}
+
+function undirectedSparseModularity(graph, options) {
+  var resolution = options.resolution;
+
+  var result = collectCommunitesForUndirected(graph, options);
+
+  var M = 0;
+
+  var totalWeights = result.totalWeights;
+  var internalWeights = result.internalWeights;
+  var communities = result.communities;
+
+  var getEdgeWeight = getters.createEdgeWeightGetter(
+    options.getEdgeWeight
+  ).fromEntry;
+
+  graph.forEachUndirectedEdge(function (
+    edge,
+    edgeAttr,
+    source,
+    target,
+    sa,
+    ta,
+    u
+  ) {
+    var weight = getEdgeWeight(edge, edgeAttr, source, target, sa, ta, u);
+
+    M += weight;
+
+    var sourceCommunity = communities[source];
+    var targetCommunity = communities[target];
+
+    totalWeights[sourceCommunity] += weight;
+    totalWeights[targetCommunity] += weight;
+
+    if (sourceCommunity !== targetCommunity) return;
+
+    internalWeights[sourceCommunity] += weight * 2;
+  });
+
+  var Q = 0;
+  var M2 = M * 2;
+
+  for (var C in internalWeights)
+    Q +=
+      internalWeights[C] / M2 - Math.pow(totalWeights[C] / M2, 2) * resolution;
+
+  return Q;
+}
+
+function directedSparseModularity(graph, options) {
+  var resolution = options.resolution;
+
+  var result = collectCommunitesForDirected(graph, options);
+
+  var M = 0;
+
+  var totalInWeights = result.totalInWeights;
+  var totalOutWeights = result.totalOutWeights;
+  var internalWeights = result.internalWeights;
+  var communities = result.communities;
+
+  var getEdgeWeight = getters.createEdgeWeightGetter(
+    options.getEdgeWeight
+  ).fromEntry;
+
+  graph.forEachDirectedEdge(function (
+    edge,
+    edgeAttr,
+    source,
+    target,
+    sa,
+    ta,
+    u
+  ) {
+    var weight = getEdgeWeight(edge, edgeAttr, source, target, sa, ta, u);
+
+    M += weight;
+
+    var sourceCommunity = communities[source];
+    var targetCommunity = communities[target];
+
+    totalOutWeights[sourceCommunity] += weight;
+    totalInWeights[targetCommunity] += weight;
+
+    if (sourceCommunity !== targetCommunity) return;
+
+    internalWeights[sourceCommunity] += weight;
+  });
+
+  var Q = 0;
+
+  for (var C in internalWeights)
+    Q +=
+      internalWeights[C] / M -
+      ((totalInWeights[C] * totalOutWeights[C]) / Math.pow(M, 2)) * resolution;
+
+  return Q;
+}
+
+// NOTE: the formula is a bit unclear here but nodeCommunityDegree should be
+// given as the edges count * 2
+function undirectedModularityDelta(
+  M,
+  communityTotalWeight,
+  nodeDegree,
+  nodeCommunityDegree
+) {
+  return (
+    nodeCommunityDegree / (2 * M) -
+    (communityTotalWeight * nodeDegree) / (2 * (M * M))
+  );
+}
+
+function directedModularityDelta(
+  M,
+  communityTotalInWeight,
+  communityTotalOutWeight,
+  nodeInDegree,
+  nodeOutDegree,
+  nodeCommunityDegree
+) {
+  return (
+    nodeCommunityDegree / M -
+    (nodeOutDegree * communityTotalInWeight +
+      nodeInDegree * communityTotalOutWeight) /
+      (M * M)
+  );
+}
+
+function denseModularity(graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/modularity: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.size === 0)
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of an empty graph.'
+    );
+
+  if (graph.multi)
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of a multi graph. Cast it to a simple one beforehand.'
+    );
+
+  var trueType = inferType(graph);
+
+  if (trueType === 'mixed')
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of a mixed graph.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  if (trueType === 'directed') return directedDenseModularity(graph, options);
+
+  return undirectedDenseModularity(graph, options);
+}
+
+function sparseModularity(graph, options) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/modularity: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.size === 0)
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of an empty graph.'
+    );
+
+  if (graph.multi)
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of a multi graph. Cast it to a simple one beforehand.'
+    );
+
+  var trueType = inferType(graph);
+
+  if (trueType === 'mixed')
+    throw new Error(
+      'graphology-metrics/modularity: cannot compute modularity of a mixed graph.'
+    );
+
+  options = resolveDefaults(options, DEFAULTS);
+
+  if (trueType === 'directed') return directedSparseModularity(graph, options);
+
+  return undirectedSparseModularity(graph, options);
+}
+
+var modularity = sparseModularity;
+
+modularity.sparse = sparseModularity;
+modularity.dense = denseModularity;
+modularity.undirectedDelta = undirectedModularityDelta;
+modularity.directedDelta = directedModularityDelta;
+
+module.exports = modularity;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83d89a20c5b60b09fdb1d99170f93a57031270ee
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function simpleSize(graph: Graph): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.js
new file mode 100644
index 0000000000000000000000000000000000000000..adcbef0bcb1f12bf78e2d8668cc79558f4d8e7ee
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/simple-size.js
@@ -0,0 +1,45 @@
+/**
+ * Graphology Simple Size
+ * =======================
+ *
+ * Function returning the simple size of a graph, i.e. the size it would have
+ * if it we assume it is a simple graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Simple size function.
+ *
+ * @param  {Graph}  graph - Target graph.
+ * @return {number}
+ */
+module.exports = function simpleSize(graph) {
+  // Handling errors
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/simple-size: the given graph is not a valid graphology instance.'
+    );
+
+  if (!graph.multi) return graph.size;
+
+  var u = 0;
+  var d = 0;
+
+  function accumulateUndirected() {
+    u++;
+  }
+
+  function accumulateDirected() {
+    d++;
+  }
+
+  graph.forEachNode(function (node) {
+    if (graph.type !== 'directed')
+      graph.forEachUndirectedNeighbor(node, accumulateUndirected);
+
+    if (graph.type !== 'undirected')
+      graph.forEachOutNeighbor(node, accumulateDirected);
+  });
+
+  return u / 2 + d;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8c2c30b6df2ceb82b140a171e4c13635923fcdf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.d.ts
@@ -0,0 +1,9 @@
+import Graph, {Attributes, EdgeMapper} from 'graphology-types';
+
+export default function weightedSize<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  getEdgeWeight?: string | EdgeMapper<NodeAttributes, EdgeAttributes>
+): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.js b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.js
new file mode 100644
index 0000000000000000000000000000000000000000..64f36af2c987a975d6be505ab0532ff168e1f68d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/graph/weighted-size.js
@@ -0,0 +1,41 @@
+/**
+ * Graphology Weighted Size
+ * =========================
+ *
+ * Function returning the sum of the graph's edges' weights.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+
+/**
+ * Defaults.
+ */
+var DEFAULT_WEIGHT_ATTRIBUTE = 'weight';
+
+/**
+ * Weighted size function.
+ *
+ * @param  {Graph}  graph                    - Target graph.
+ * @param  {string|function} [getEdgeWeight] - Name of the weight attribute or getter function.
+ * @return {number}
+ */
+module.exports = function weightedSize(graph, getEdgeWeight) {
+  // Handling errors
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/weighted-size: the given graph is not a valid graphology instance.'
+    );
+
+  getEdgeWeight = createEdgeWeightGetter(
+    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
+  ).fromEntry;
+
+  var size = 0;
+
+  graph.forEachEdge(function (e, a, s, t, sa, ta, u) {
+    size += getEdgeWeight(e, a, s, t, sa, ta, u);
+  });
+
+  return size;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..26040278a08ffbde1bc94bce261930ee6bde7efc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/index.d.ts
@@ -0,0 +1,5 @@
+export * as centrality from './centrality';
+export * as edge from './edge';
+export * as graph from './graph';
+export * as layoutQuality from './layout-quality';
+export * as node from './node';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce37fc9eeaf42cc249cd67aa0462f62b67560621
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/index.js
@@ -0,0 +1,11 @@
+/**
+ * Graphology Metrics
+ * ===================
+ *
+ * Library endpoint.
+ */
+exports.centrality = require('./centrality');
+exports.edge = require('./edge');
+exports.graph = require('./graph');
+exports.layoutQuality = require('./layout-quality');
+exports.node = require('./node');
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae69dc48a88b1c76f4c0e295e424bccb9d435111
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function edgeUniformity(graph: Graph): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.js b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.js
new file mode 100644
index 0000000000000000000000000000000000000000..275835324ba0de469a37c72a90a3814250b02c7d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/edge-uniformity.js
@@ -0,0 +1,57 @@
+/**
+ * Graphology Layout Quality - Edge Uniformity
+ * ============================================
+ *
+ * Function computing the layout quality metric named "edge uniformity".
+ * It is basically the normalized standard deviation of edge length.
+ *
+ * [Article]:
+ * Rahman, Md Khaledur, et al. « BatchLayout: A Batch-Parallel Force-Directed
+ * Graph Layout Algorithm in Shared Memory ».
+ * http://arxiv.org/abs/2002.08233.
+ */
+var isGraph = require('graphology-utils/is-graph');
+
+function euclideanDistance(a, b) {
+  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
+}
+
+module.exports = function edgeUniformity(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/layout-quality/edge-uniformity: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.size === 0) return 0;
+
+  var sum = 0,
+    i = 0,
+    l;
+
+  var lengths = new Float64Array(graph.size);
+
+  graph.forEachEdge(function (
+    edge,
+    attr,
+    source,
+    target,
+    sourceAttr,
+    targetAttr
+  ) {
+    var edgeLength = euclideanDistance(sourceAttr, targetAttr);
+
+    lengths[i++] = edgeLength;
+    sum += edgeLength;
+  });
+
+  var avg = sum / graph.size;
+
+  var stdev = 0;
+
+  for (i = 0, l = graph.size; i < l; i++)
+    stdev += Math.pow(avg - lengths[i], 2);
+
+  var metric = stdev / (graph.size * Math.pow(avg, 2));
+
+  return Math.sqrt(metric);
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0dc34389d72b50e86d0da6442281314ae6338852
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.d.ts
@@ -0,0 +1,3 @@
+export {default as edgeUniformity} from './edge-uniformity';
+export {default as neighborhoodPreservation} from './neighborhood-preservation';
+export {default as stress} from './stress';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e94c9618329b93a8bc4aee7a1ee32556c302d37a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/index.js
@@ -0,0 +1,3 @@
+exports.edgeUniformity = require('./edge-uniformity.js');
+exports.neighborhoodPreservation = require('./neighborhood-preservation.js');
+exports.stress = require('./stress.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d305642e9ce8269de49607e57e1db9f37e87cd1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function neighborhoodPreservation(graph: Graph): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.js b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.js
new file mode 100644
index 0000000000000000000000000000000000000000..7f1f2aa2b48cc182d9c4e60aac4d997c3490cfad
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/neighborhood-preservation.js
@@ -0,0 +1,62 @@
+/**
+ * Graphology Layout Quality - Neighborhood Preservation
+ * ======================================================
+ *
+ * Function computing the layout quality metric named "neighborhood preservation".
+ * It is basically the average of overlap coefficient between node neighbors in
+ * the graph and equivalent k-nn in the 2d layout space.
+ *
+ * [Article]:
+ * Rahman, Md Khaledur, et al. « BatchLayout: A Batch-Parallel Force-Directed
+ * Graph Layout Algorithm in Shared Memory ».
+ * http://arxiv.org/abs/2002.08233.
+ */
+var isGraph = require('graphology-utils/is-graph'),
+  KDTree = require('mnemonist/kd-tree'),
+  intersectionSize = require('mnemonist/set').intersectionSize;
+
+module.exports = function neighborhoodPreservation(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/layout-quality/neighborhood-preservation: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.order === 0)
+    throw new Error(
+      'graphology-metrics/layout-quality/neighborhood-preservation: cannot compute stress for a null graph.'
+    );
+
+  if (graph.size === 0) return 0;
+
+  var axes = [new Float64Array(graph.order), new Float64Array(graph.order)],
+    i = 0;
+
+  graph.forEachNode(function (node, attr) {
+    axes[0][i] = attr.x;
+    axes[1][i++] = attr.y;
+  });
+
+  var tree = KDTree.fromAxes(axes, graph.nodes());
+
+  var sum = 0;
+
+  graph.forEachNode(function (node, attr) {
+    var neighbors = new Set(graph.neighbors(node));
+
+    // If node has no neighbors or is connected to every other node
+    if (neighbors.size === 0 || neighbors.size === graph.order - 1) {
+      sum += 1;
+      return;
+    }
+
+    var knn = tree.kNearestNeighbors(neighbors.size + 1, [attr.x, attr.y]);
+    knn = new Set(knn.slice(1));
+
+    var I = intersectionSize(neighbors, knn);
+
+    // Computing overlap coefficient
+    sum += I / knn.size;
+  });
+
+  return sum / graph.order;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e1b6bc13210040166e82bc03d29beb423798ea6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function stress(graph: Graph): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.js b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.js
new file mode 100644
index 0000000000000000000000000000000000000000..85e206640f9385128545a8580468582ab0a205ac
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/layout-quality/stress.js
@@ -0,0 +1,74 @@
+/**
+ * Graphology Layout Quality - Stress
+ * ===================================
+ *
+ * Function computing the layout quality metric named "stress".
+ * It is basically the sum of normalized deltas between graph topology distances
+ * and 2d space distances of the layout.
+ *
+ * [Article]:
+ * Rahman, Md Khaledur, et al. « BatchLayout: A Batch-Parallel Force-Directed
+ * Graph Layout Algorithm in Shared Memory ».
+ * http://arxiv.org/abs/2002.08233.
+ */
+var isGraph = require('graphology-utils/is-graph'),
+  undirectedSingleSourceLength =
+    require('graphology-shortest-path/unweighted').undirectedSingleSourceLength;
+
+function euclideanDistance(a, b) {
+  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
+}
+
+module.exports = function stress(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/layout-quality/stress: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.order === 0)
+    throw new Error(
+      'graphology-metrics/layout-quality/stress: cannot compute stress for a null graph.'
+    );
+
+  var nodes = new Array(graph.order),
+    entries = new Array(graph.order),
+    i = 0;
+
+  // We choose an arbitrary large distance for when two nodes cannot be
+  // connected because they belong to different connected components
+  // and because we cannot deal with Infinity in our computations
+  // This is what most papers recommend anyway
+  var maxDistance = graph.order * 4;
+
+  graph.forEachNode(function (node, attr) {
+    nodes[i] = node;
+    entries[i++] = attr;
+  });
+
+  var j, l, p1, p2, shortestPaths, dij, wij, cicj;
+
+  var sum = 0;
+
+  for (i = 0, l = graph.order; i < l; i++) {
+    shortestPaths = undirectedSingleSourceLength(graph, nodes[i]);
+
+    p1 = entries[i];
+
+    for (j = i + 1; j < l; j++) {
+      p2 = entries[j];
+
+      // NOTE: dij should be 0 since we don't consider self-loops
+      dij = shortestPaths[nodes[j]];
+
+      // Target is in another castle
+      if (typeof dij === 'undefined') dij = maxDistance;
+
+      cicj = euclideanDistance(p1, p2);
+      wij = 1 / (dij * dij);
+
+      sum += wij * Math.pow(cicj - dij, 2);
+    }
+  }
+
+  return sum;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d3d3b5bf7a3ab9d6f2661cefe8e7c22fa46d77c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function eccentricity(graph: Graph, node: unknown): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.js b/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa1e17cf71cba722ecb05523f053169780655858
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/eccentricity.js
@@ -0,0 +1,39 @@
+/**
+ * Graphology Eccentricity
+ * ========================
+ *
+ * Functions used to compute the eccentricity of each node of a given graph.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var singleSourceLength =
+  require('graphology-shortest-path/unweighted').singleSourceLength;
+
+module.exports = function eccentricity(graph, node) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/eccentricity: given graph is not a valid graphology instance.'
+    );
+
+  if (graph.size === 0) return Infinity;
+
+  var ecc = -Infinity;
+
+  var lengths = singleSourceLength(graph, node);
+
+  var otherNode;
+
+  var pathLength,
+    l = 0;
+
+  for (otherNode in lengths) {
+    pathLength = lengths[otherNode];
+
+    if (pathLength > ecc) ecc = pathLength;
+
+    l++;
+  }
+
+  if (l < graph.order) return Infinity;
+
+  return ecc;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4172de3f45e04af24350be237d4b049c57062bd8
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.d.ts
@@ -0,0 +1,2 @@
+export {default as eccentricity} from './eccentricity';
+export * from './weighted-degree';
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.js b/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d473c13aa01a8e2c48b927c16641f566b95d6d8e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/index.js
@@ -0,0 +1,11 @@
+var wd = require('./weighted-degree.js');
+
+exports.eccentricity = require('./eccentricity.js');
+
+exports.weightedDegree = wd.weightedDegree;
+exports.weightedInDegree = wd.weightedInDegree;
+exports.weightedOutDegree = wd.weightedOutDegree;
+exports.weightedInboundDegree = wd.weightedInboundDegree;
+exports.weightedOutboundDegree = wd.weightedOutboundDegree;
+exports.weightedUndirectedDegree = wd.weightedUndirectedDegree;
+exports.weightedDirectedDegree = wd.weightedDirectedDegree;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.d.ts b/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8276002faf77bba06ef5688db6c9f8a5429cb4b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.d.ts
@@ -0,0 +1,69 @@
+import Graph, {Attributes, EdgeMapper} from 'graphology-types';
+
+type EdgeWeightGetter<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> = keyof EdgeAttributes | EdgeMapper<number, NodeAttributes, EdgeAttributes>;
+
+export function weightedDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedInDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedOutDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedInboundDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedOutboundDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedUndirectedDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
+
+export function weightedDirectedDegree<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  node: unknown,
+  getEdgeWeight?: EdgeWeightGetter<NodeAttributes, EdgeAttributes>
+): number;
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.js b/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.js
new file mode 100644
index 0000000000000000000000000000000000000000..d87f0bdee32b1b650bc4f8f36da1fb7f0ab0073a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/node/weighted-degree.js
@@ -0,0 +1,89 @@
+/**
+ * Graphology Weighted Degree
+ * ===========================
+ *
+ * Function computing the weighted degree of nodes. The weighted degree is the
+ * sum of a node's edges' weights.
+ */
+var isGraph = require('graphology-utils/is-graph');
+
+/**
+ * Defaults.
+ */
+var DEFAULT_WEIGHT_ATTRIBUTE = 'weight';
+
+/**
+ * Asbtract function to perform any kind of weighted degree.
+ *
+ * @param  {string}          name          - Name of the implemented function.
+ * @param  {string}          method        - Method of the graph to get the edges.
+ * @param  {Graph}           graph         - A graphology instance.
+ * @param  {string}          node          - Target node.
+ * @param  {string|function} getEdgeWeight - Name of edge weight attribute or getter function.
+ *
+ * @return {number}
+ */
+function abstractWeightedDegree(name, method, graph, node, getEdgeWeight) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-metrics/' +
+        name +
+        ': the given graph is not a valid graphology instance.'
+    );
+
+  getEdgeWeight = getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE;
+
+  var d = 0;
+
+  graph[method](node, function (e, a, s, t, sa, ta, u) {
+    var w =
+      typeof getEdgeWeight === 'function'
+        ? getEdgeWeight(e, a, s, t, sa, ta, u)
+        : a[getEdgeWeight];
+
+    if (typeof w !== 'number' || isNaN(w)) w = 1;
+
+    d += w;
+  });
+
+  return d;
+}
+
+/**
+ * Exports.
+ */
+exports.weightedDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedDegree',
+  'forEachEdge'
+);
+exports.weightedInDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedInDegree',
+  'forEachInEdge'
+);
+exports.weightedOutDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedOutDegree',
+  'forEachOutEdge'
+);
+exports.weightedInboundDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedInboundDegree',
+  'forEachInboundEdge'
+);
+exports.weightedOutboundDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedOutboundDegree',
+  'forEachOutboundEdge'
+);
+exports.weightedUndirectedDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedUndirectedDegree',
+  'forEachUndirectedEdge'
+);
+exports.weightedDirectedDegree = abstractWeightedDegree.bind(
+  null,
+  'weightedDirectedDegree',
+  'forEachDirectedEdge'
+);
diff --git a/libs/shared/graph-layout/node_modules/graphology-metrics/package.json b/libs/shared/graph-layout/node_modules/graphology-metrics/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8fa42af58134574af7f661f19ed5784031ad1a6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-metrics/package.json
@@ -0,0 +1,62 @@
+{
+  "name": "graphology-metrics",
+  "version": "2.1.0",
+  "description": "Miscellaneous graph metrics for graphology.",
+  "main": "index.js",
+  "files": [
+    "index.js",
+    "*.d.ts",
+    "centrality",
+    "edge",
+    "graph",
+    "layout-quality",
+    "node"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha --recursive test"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "betweenness",
+    "centrality",
+    "density",
+    "diameter",
+    "eccentricity",
+    "extent",
+    "graph",
+    "graphology",
+    "metrics",
+    "modularity"
+  ],
+  "contributors": [
+    {
+      "name": "Guillaume Plique",
+      "url": "http://github.com/Yomguithereal"
+    },
+    {
+      "name": "Tanguy Lucci",
+      "url": "https://github.com/luccitan"
+    },
+    {
+      "name": "Pauline Breteau",
+      "url": "https://github.com/paubre"
+    }
+  ],
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.20.0"
+  },
+  "dependencies": {
+    "graphology-shortest-path": "^2.0.0",
+    "graphology-utils": "^2.4.4",
+    "mnemonist": "^0.39.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-shortest-path/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df12ee58e0ff4fa313b5bc24e694918ccc1b363a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/README.md b/libs/shared/graph-layout/node_modules/graphology-shortest-path/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..89c296bb253ff5da35e11d8edad7c10651809738
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/README.md
@@ -0,0 +1,174 @@
+# Graphology Shortest Path
+
+Shortest path functions for [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-shortest-path
+```
+
+## Usage
+
+- [Unweighted](#unweighted)
+  - [bidirectional](#bidirectional)
+  - [singleSource](#singlesource)
+  - [singleSourceLength](#singlesourcelength)
+  - [undirectedSingleSourceLength](#undirectedsinglesourcelength)
+- [Dijkstra](#dijkstra)
+  - [bidirectional](#dijkstra-bidirectional)
+  - [singleSource](#dijkstra-singlesource)
+- [Utilities](#utilities)
+  - [edgePathFromNodePath](#edgepathfromnodepath)
+
+## Unweighted
+
+### bidirectional
+
+Returns the shortest path in the graph between source & target or `null` if such a path does not exist.
+
+```js
+import {bidirectional} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import {bidirectional} from 'graphology-shortest-path/unweighted';
+
+// Returning the shortest path between source & target
+const path = bidirectional(graph, source, target);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+- **target** _any_: target node.
+
+### singleSource
+
+Return a map of every shortest path between the given source & all the nodes of the graph.
+
+```js
+import {singleSource} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import {singleSource} from 'graphology-shortest-path/unweighted';
+
+// Returning every shortest path between source & every node of the graph
+const paths = singleSource(graph, source);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+
+### singleSourceLength
+
+Return a map of every shortest path length between the given source & all the nodes of the graph.
+
+```js
+import {singleSourceLength} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import {singleSourceLength} from 'graphology-shortest-path/unweighted';
+
+// Returning every shortest path between source & every node of the graph
+const paths = singleSourceLength(graph, source);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+
+### undirectedSingleSourceLength
+
+Return a map of every shortest path length between the given source & all the nodes of the graph. This is basically the same as [singleSourceLength](#singlesourcelength) except that it will consider any given graph as undirected when traversing.
+
+```js
+import {undirectedSingleSourceLength} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import {undirectedSingleSourceLength} from 'graphology-shortest-path/unweighted';
+
+// Returning every shortest path between source & every node of the graph
+const paths = undirectedSingleSourceLength(graph, source);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+
+## Dijkstra
+
+<h3 id="dijkstra-bidirectional">bidirectional</h3>
+
+Returns the shortest path in the weighted graph between source & target or `null` if such a path does not exist.
+
+```js
+import {dijkstra} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import dijkstra from 'graphology-shortest-path/dijkstra';
+
+// Returning the shortest path between source & target
+const path = dijkstra.bidirectional(graph, source, target);
+
+// If you store edges' weight in custom attribute
+const path = dijkstra.bidirectional(graph, source, target, 'customWeight');
+
+// Using a custom weight getter function
+const path = dijkstra.bidirectional(
+  graph,
+  source,
+  target,
+  (_, attr) => attr.importance
+);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+- **target** _any_: target node.
+- **getEdgeWeight** _?string\|function_ [`weight`]: name of the weight attribute or getter function.
+
+<h3 id="dijkstra-singlesource">singleSource</h3>
+
+Return a map of every shortest path between the given source & all the nodes of the weighted graph.
+
+```js
+import {dijkstra} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import dijkstra from 'graphology-shortest-path/dijkstra';
+
+// Returning every shortest path between source & every node of the graph
+const paths = dijkstra.singleSource(graph, source);
+
+// If you store edges' weight in custom attribute
+const paths = dijkstra.singleSource(graph, source, 'customWeight');
+
+// Using a custom weight getter function
+const path = dijkstra.singleSource(graph, source, (_, attr) => attr.importance);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **source** _any_: source node.
+- **getEdgeWeight** _?string\|function_ [`weight`]: name of the weight attribute or getter function.
+
+## Utilities
+
+### edgePathFromNodePath
+
+Helper function that can convert a node path to an edge path.
+
+```js
+import {edgePathFromNodePath} from 'graphology-shortest-path';
+// Alternatively, if you want to load only the relevant code
+import {edgePathFromNodePath} from 'graphology-shortest-path/utils';
+
+const edgePath = edgePathFromNodePath(graph, nodePath);
+```
+
+_Arguments_
+
+- **graph** _Graph_: a `graphology` instance.
+- **edgePath** _Array_: edge path to convert.
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.d.ts b/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0d00fda5f7c676cb36fa74ca630e3fe915f0fa9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.d.ts
@@ -0,0 +1,44 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+
+type SingleSourceDijkstraResult = {[key: string]: string[]};
+type BidirectionalDijstraResult = string[];
+type BrandesResult = [
+  Array<string>,
+  {[key: string]: Array<string>},
+  {[key: string]: number}
+];
+
+export function bidirectional<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  source: unknown,
+  target: unknown,
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+): BidirectionalDijstraResult;
+
+export function singleSource<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  source: unknown,
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+): SingleSourceDijkstraResult;
+
+export function brandes<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  source: unknown,
+  getEdgeWeight?:
+    | keyof EdgeAttributes
+    | MinimalEdgeMapper<number, EdgeAttributes>
+): BrandesResult;
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.js b/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.js
new file mode 100644
index 0000000000000000000000000000000000000000..71d471b411a8823ee2853c3e4596323f7ec5a841
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/dijkstra.js
@@ -0,0 +1,381 @@
+/**
+ * Graphology Dijkstra Shortest Path
+ * ==================================
+ *
+ * Graphology implementation of Dijkstra shortest path for weighted graphs.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var createEdgeWeightGetter =
+  require('graphology-utils/getters').createEdgeWeightGetter;
+var Heap = require('mnemonist/heap');
+
+/**
+ * Defaults & helpers.
+ */
+var DEFAULT_WEIGHT_ATTRIBUTE = 'weight';
+
+function DIJKSTRA_HEAP_COMPARATOR(a, b) {
+  if (a[0] > b[0]) return 1;
+  if (a[0] < b[0]) return -1;
+
+  if (a[1] > b[1]) return 1;
+  if (a[1] < b[1]) return -1;
+
+  if (a[2] > b[2]) return 1;
+  if (a[2] < b[2]) return -1;
+
+  return 0;
+}
+
+function BRANDES_DIJKSTRA_HEAP_COMPARATOR(a, b) {
+  if (a[0] > b[0]) return 1;
+  if (a[0] < b[0]) return -1;
+
+  if (a[1] > b[1]) return 1;
+  if (a[1] < b[1]) return -1;
+
+  if (a[2] > b[2]) return 1;
+  if (a[2] < b[2]) return -1;
+
+  if (a[3] > b[3]) return 1;
+  if (a[3] < b[3]) return -1;
+
+  return 0;
+}
+
+/**
+ * Bidirectional Dijkstra shortest path between source & target node abstract.
+ *
+ * Note that this implementation was basically copied from networkx.
+ *
+ * @param  {Graph}  graph         - The graphology instance.
+ * @param  {string} source        - Source node.
+ * @param  {string} target        - Target node.
+ * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
+ * @param  {array}                - The found path if any and its cost.
+ */
+function abstractBidirectionalDijkstra(graph, source, target, getEdgeWeight) {
+  source = '' + source;
+  target = '' + target;
+
+  // Sanity checks
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-shortest-path/dijkstra: invalid graphology instance.'
+    );
+
+  if (source && !graph.hasNode(source))
+    throw new Error(
+      'graphology-shortest-path/dijkstra: the "' +
+        source +
+        '" source node does not exist in the given graph.'
+    );
+
+  if (target && !graph.hasNode(target))
+    throw new Error(
+      'graphology-shortest-path/dijkstra: the "' +
+        target +
+        '" target node does not exist in the given graph.'
+    );
+
+  getEdgeWeight = createEdgeWeightGetter(
+    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
+  ).fromMinimalEntry;
+
+  if (source === target) return [0, [source]];
+
+  var distances = [{}, {}],
+    paths = [{}, {}],
+    fringe = [
+      new Heap(DIJKSTRA_HEAP_COMPARATOR),
+      new Heap(DIJKSTRA_HEAP_COMPARATOR)
+    ],
+    seen = [{}, {}];
+
+  paths[0][source] = [source];
+  paths[1][target] = [target];
+
+  seen[0][source] = 0;
+  seen[1][target] = 0;
+
+  var finalPath = [],
+    finalDistance = Infinity;
+
+  var count = 0,
+    dir = 1,
+    item,
+    edges,
+    cost,
+    d,
+    v,
+    u,
+    e,
+    i,
+    l;
+
+  fringe[0].push([0, count++, source]);
+  fringe[1].push([0, count++, target]);
+
+  while (fringe[0].size && fringe[1].size) {
+    // Swapping direction
+    dir = 1 - dir;
+
+    item = fringe[dir].pop();
+    d = item[0];
+    v = item[2];
+
+    if (v in distances[dir]) continue;
+
+    distances[dir][v] = d;
+
+    // Shortest path is found?
+    if (v in distances[1 - dir]) return [finalDistance, finalPath];
+
+    edges = dir === 1 ? graph.inboundEdges(v) : graph.outboundEdges(v);
+
+    for (i = 0, l = edges.length; i < l; i++) {
+      e = edges[i];
+      u = graph.opposite(v, e);
+      cost = distances[dir][v] + getEdgeWeight(e, graph.getEdgeAttributes(e));
+
+      if (u in distances[dir] && cost < distances[dir][u]) {
+        throw Error(
+          'graphology-shortest-path/dijkstra: contradictory paths found. Do some of your edges have a negative weight?'
+        );
+      } else if (!(u in seen[dir]) || cost < seen[dir][u]) {
+        seen[dir][u] = cost;
+        fringe[dir].push([cost, count++, u]);
+        paths[dir][u] = paths[dir][v].concat(u);
+
+        if (u in seen[0] && u in seen[1]) {
+          d = seen[0][u] + seen[1][u];
+
+          if (finalPath.length === 0 || finalDistance > d) {
+            finalDistance = d;
+            finalPath = paths[0][u].concat(paths[1][u].slice(0, -1).reverse());
+          }
+        }
+      }
+    }
+  }
+
+  // No path was found
+  return [Infinity, null];
+}
+
+/**
+ * Multisource Dijkstra shortest path abstract function. This function is the
+ * basis of the algorithm that every other will use.
+ *
+ * Note that this implementation was basically copied from networkx.
+ * TODO: it might be more performant to use a dedicated objet for the heap's
+ * items.
+ *
+ * @param  {Graph}  graph         - The graphology instance.
+ * @param  {array}  sources       - A list of sources.
+ * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
+ * @param  {number} cutoff        - Maximum depth of the search.
+ * @param  {string} target        - Optional target to reach.
+ * @param  {object} paths         - Optional paths object to maintain.
+ * @return {object}               - Returns the paths.
+ */
+function abstractDijkstraMultisource(
+  graph,
+  sources,
+  getEdgeWeight,
+  cutoff,
+  target,
+  paths
+) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-shortest-path/dijkstra: invalid graphology instance.'
+    );
+
+  if (target && !graph.hasNode(target))
+    throw new Error(
+      'graphology-shortest-path/dijkstra: the "' +
+        target +
+        '" target node does not exist in the given graph.'
+    );
+
+  getEdgeWeight = createEdgeWeightGetter(
+    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
+  ).fromMinimalEntry;
+
+  var distances = {},
+    seen = {},
+    fringe = new Heap(DIJKSTRA_HEAP_COMPARATOR);
+
+  var count = 0,
+    edges,
+    item,
+    cost,
+    v,
+    u,
+    e,
+    d,
+    i,
+    j,
+    l,
+    m;
+
+  for (i = 0, l = sources.length; i < l; i++) {
+    v = sources[i];
+    seen[v] = 0;
+    fringe.push([0, count++, v]);
+
+    if (paths) paths[v] = [v];
+  }
+
+  while (fringe.size) {
+    item = fringe.pop();
+    d = item[0];
+    v = item[2];
+
+    if (v in distances) continue;
+
+    distances[v] = d;
+
+    if (v === target) break;
+
+    edges = graph.outboundEdges(v);
+
+    for (j = 0, m = edges.length; j < m; j++) {
+      e = edges[j];
+      u = graph.opposite(v, e);
+      cost = getEdgeWeight(e, graph.getEdgeAttributes(e)) + distances[v];
+
+      if (cutoff && cost > cutoff) continue;
+
+      if (u in distances && cost < distances[u]) {
+        throw Error(
+          'graphology-shortest-path/dijkstra: contradictory paths found. Do some of your edges have a negative weight?'
+        );
+      } else if (!(u in seen) || cost < seen[u]) {
+        seen[u] = cost;
+        fringe.push([cost, count++, u]);
+
+        if (paths) paths[u] = paths[v].concat(u);
+      }
+    }
+  }
+
+  return distances;
+}
+
+/**
+ * Single source Dijkstra shortest path between given node & other nodes in
+ * the graph.
+ *
+ * @param  {Graph}  graph         - The graphology instance.
+ * @param  {string} source        - Source node.
+ * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
+ * @return {object}               - An object of found paths.
+ */
+function singleSourceDijkstra(graph, source, getEdgeWeight) {
+  var paths = {};
+
+  abstractDijkstraMultisource(graph, [source], getEdgeWeight, 0, null, paths);
+
+  return paths;
+}
+
+function bidirectionalDijkstra(graph, source, target, getEdgeWeight) {
+  return abstractBidirectionalDijkstra(graph, source, target, getEdgeWeight)[1];
+}
+
+/**
+ * Function using Ulrik Brandes' method to map single source shortest paths
+ * from selected node.
+ *
+ * [Reference]:
+ * Ulrik Brandes: A Faster Algorithm for Betweenness Centrality.
+ * Journal of Mathematical Sociology 25(2):163-177, 2001.
+ *
+ * @param  {Graph}  graph         - Target graph.
+ * @param  {any}    source        - Source node.
+ * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
+ * @return {array}                - [Stack, Paths, Sigma]
+ */
+function brandes(graph, source, getEdgeWeight) {
+  source = '' + source;
+
+  getEdgeWeight = createEdgeWeightGetter(
+    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
+  ).fromMinimalEntry;
+
+  var S = [],
+    P = {},
+    sigma = {};
+
+  var nodes = graph.nodes(),
+    edges,
+    item,
+    pred,
+    dist,
+    cost,
+    v,
+    w,
+    e,
+    i,
+    l;
+
+  for (i = 0, l = nodes.length; i < l; i++) {
+    v = nodes[i];
+    P[v] = [];
+    sigma[v] = 0;
+  }
+
+  var D = {};
+
+  sigma[source] = 1;
+
+  var seen = {};
+  seen[source] = 0;
+
+  var count = 0;
+
+  var Q = new Heap(BRANDES_DIJKSTRA_HEAP_COMPARATOR);
+  Q.push([0, count++, source, source]);
+
+  while (Q.size) {
+    item = Q.pop();
+    dist = item[0];
+    pred = item[2];
+    v = item[3];
+
+    if (v in D) continue;
+
+    sigma[v] += sigma[pred];
+    S.push(v);
+    D[v] = dist;
+
+    edges = graph.outboundEdges(v);
+
+    for (i = 0, l = edges.length; i < l; i++) {
+      e = edges[i];
+      w = graph.opposite(v, e);
+      cost = dist + getEdgeWeight(e, graph.getEdgeAttributes(e));
+
+      if (!(w in D) && (!(w in seen) || cost < seen[w])) {
+        seen[w] = cost;
+        Q.push([cost, count++, v, w]);
+        sigma[w] = 0;
+        P[w] = [v];
+      } else if (cost === seen[w]) {
+        sigma[w] += sigma[v];
+        P[w].push(v);
+      }
+    }
+  }
+
+  return [S, P, sigma];
+}
+
+/**
+ * Exporting.
+ */
+exports.bidirectional = bidirectionalDijkstra;
+exports.singleSource = singleSourceDijkstra;
+exports.brandes = brandes;
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..977b8a865bdfa4b24190725332900ca12a65edce
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.d.ts
@@ -0,0 +1,11 @@
+export * as dijkstra from './dijkstra';
+export * as unweighted from './unweighted';
+
+export {
+  singleSource,
+  singleSourceLength,
+  bidirectional,
+  brandes
+} from './unweighted';
+
+export {edgePathFromNodePath} from './utils';
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.js b/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..17849aec19e1f870521881ac9b59a959a94af301
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/index.js
@@ -0,0 +1,19 @@
+/**
+ * Graphology Shortest Path
+ * =========================
+ *
+ * Library endpoint.
+ */
+var unweighted = require('./unweighted.js');
+var utils = require('./utils.js');
+
+exports.unweighted = unweighted;
+exports.dijkstra = require('./dijkstra.js');
+
+exports.bidirectional = unweighted.bidirectional;
+exports.singleSource = unweighted.singleSource;
+exports.singleSourceLength = unweighted.singleSourceLength;
+exports.undirectedSingleSourceLength = unweighted.undirectedSingleSourceLength;
+exports.brandes = unweighted.brandes;
+
+exports.edgePathFromNodePath = utils.edgePathFromNodePath;
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.d.ts b/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5abbc45a63338d6d4bc9aa2e930bdc5e602299f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.d.ts
@@ -0,0 +1,38 @@
+import Graph, {Attributes} from 'graphology-types';
+import {MinimalEdgeMapper} from 'graphology-utils/getters';
+import FixedStack from 'mnemonist/fixed-stack';
+import {
+  NeighborhoodIndex,
+  WeightedNeighborhoodIndex
+} from 'graphology-indices/neighborhood';
+
+type IndexedBrandesResult = [
+  FixedStack<number>,
+  Array<Array<number>>,
+  Uint32Array
+];
+
+type IndexedBrandesFunction = (sourceIndex: number) => IndexedBrandesResult;
+
+interface ICreateUnweightedIndexedBrandes {
+  (graph: Graph): IndexedBrandesFunction;
+  index: NeighborhoodIndex;
+}
+
+interface ICreateDijkstraIndexedBrandes<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> {
+  (
+    graph: Graph<NodeAttributes, EdgeAttributes>,
+    getEdgeWeight?:
+      | keyof EdgeAttributes
+      | MinimalEdgeMapper<number, EdgeAttributes>
+  ): IndexedBrandesFunction;
+  index: WeightedNeighborhoodIndex;
+}
+
+declare const createUnweightedIndexedBrandes: ICreateUnweightedIndexedBrandes;
+declare const createDijkstraIndexedBrandes: ICreateDijkstraIndexedBrandes;
+
+export {createUnweightedIndexedBrandes, createDijkstraIndexedBrandes};
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.js b/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.js
new file mode 100644
index 0000000000000000000000000000000000000000..3cac718e9d1645245e120c20d74f52a11847fc92
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/indexed-brandes.js
@@ -0,0 +1,195 @@
+/**
+ * Graphology Indexed Brandes Routine
+ * ===================================
+ *
+ * Indexed version of the famous Brandes routine aiming at computing
+ * betweenness centrality efficiently.
+ */
+var FixedDeque = require('mnemonist/fixed-deque');
+var FixedStack = require('mnemonist/fixed-stack');
+var Heap = require('mnemonist/heap');
+var typed = require('mnemonist/utils/typed-arrays');
+var neighborhoodIndices = require('graphology-indices/neighborhood');
+
+var NeighborhoodIndex = neighborhoodIndices.NeighborhoodIndex;
+var WeightedNeighborhoodIndex = neighborhoodIndices.WeightedNeighborhoodIndex;
+
+/**
+ * Indexed unweighted Brandes routine.
+ *
+ * [Reference]:
+ * Ulrik Brandes: A Faster Algorithm for Betweenness Centrality.
+ * Journal of Mathematical Sociology 25(2):163-177, 2001.
+ *
+ * @param  {Graph}    graph - The graphology instance.
+ * @return {function}
+ */
+exports.createUnweightedIndexedBrandes =
+  function createUnweightedIndexedBrandes(graph) {
+    var neighborhoodIndex = new NeighborhoodIndex(graph);
+
+    var neighborhood = neighborhoodIndex.neighborhood,
+      starts = neighborhoodIndex.starts;
+
+    var order = graph.order;
+
+    var S = new FixedStack(typed.getPointerArray(order), order),
+      sigma = new Uint32Array(order),
+      P = new Array(order),
+      D = new Int32Array(order);
+
+    var Q = new FixedDeque(Uint32Array, order);
+
+    var brandes = function (sourceIndex) {
+      var Dv, sigmav, start, stop, j, v, w;
+
+      for (v = 0; v < order; v++) {
+        P[v] = [];
+        sigma[v] = 0;
+        D[v] = -1;
+      }
+
+      sigma[sourceIndex] = 1;
+      D[sourceIndex] = 0;
+
+      Q.push(sourceIndex);
+
+      while (Q.size !== 0) {
+        v = Q.shift();
+        S.push(v);
+
+        Dv = D[v];
+        sigmav = sigma[v];
+
+        start = starts[v];
+        stop = starts[v + 1];
+
+        for (j = start; j < stop; j++) {
+          w = neighborhood[j];
+
+          if (D[w] === -1) {
+            Q.push(w);
+            D[w] = Dv + 1;
+          }
+
+          if (D[w] === Dv + 1) {
+            sigma[w] += sigmav;
+            P[w].push(v);
+          }
+        }
+      }
+
+      return [S, P, sigma];
+    };
+
+    brandes.index = neighborhoodIndex;
+
+    return brandes;
+  };
+
+function BRANDES_DIJKSTRA_HEAP_COMPARATOR(a, b) {
+  if (a[0] > b[0]) return 1;
+  if (a[0] < b[0]) return -1;
+
+  if (a[1] > b[1]) return 1;
+  if (a[1] < b[1]) return -1;
+
+  if (a[2] > b[2]) return 1;
+  if (a[2] < b[2]) return -1;
+
+  if (a[3] > b[3]) return 1;
+  if (a[3] < b[3]) return -1;
+
+  return 0;
+}
+
+/**
+ * Indexed Dijkstra Brandes routine.
+ *
+ * [Reference]:
+ * Ulrik Brandes: A Faster Algorithm for Betweenness Centrality.
+ * Journal of Mathematical Sociology 25(2):163-177, 2001.
+ *
+ * @param  {Graph}    graph         - The graphology instance.
+ * @param  {string}   getEdgeWeight - Name of the weight attribute or getter function.
+ * @return {function}
+ */
+exports.createDijkstraIndexedBrandes = function createDijkstraIndexedBrandes(
+  graph,
+  getEdgeWeight
+) {
+  var neighborhoodIndex = new WeightedNeighborhoodIndex(
+    graph,
+    getEdgeWeight || 'weight'
+  );
+
+  var neighborhood = neighborhoodIndex.neighborhood,
+    weights = neighborhoodIndex.weights,
+    starts = neighborhoodIndex.starts;
+
+  var order = graph.order;
+
+  var S = new FixedStack(typed.getPointerArray(order), order),
+    sigma = new Uint32Array(order),
+    P = new Array(order),
+    D = new Float64Array(order),
+    seen = new Float64Array(order);
+
+  // TODO: use fixed-size heap
+  var Q = new Heap(BRANDES_DIJKSTRA_HEAP_COMPARATOR);
+
+  var brandes = function (sourceIndex) {
+    var start, stop, item, dist, pred, cost, j, v, w;
+
+    var count = 0;
+
+    for (v = 0; v < order; v++) {
+      P[v] = [];
+      sigma[v] = 0;
+      D[v] = -1;
+      seen[v] = -1;
+    }
+
+    sigma[sourceIndex] = 1;
+    seen[sourceIndex] = 0;
+
+    Q.push([0, count++, sourceIndex, sourceIndex]);
+
+    while (Q.size !== 0) {
+      item = Q.pop();
+      dist = item[0];
+      pred = item[2];
+      v = item[3];
+
+      if (D[v] !== -1) continue;
+
+      S.push(v);
+      D[v] = dist;
+      sigma[v] += sigma[pred];
+
+      start = starts[v];
+      stop = starts[v + 1];
+
+      for (j = start; j < stop; j++) {
+        w = neighborhood[j];
+        cost = dist + weights[j];
+
+        if (D[w] === -1 && (seen[w] === -1 || cost < seen[w])) {
+          seen[w] = cost;
+          Q.push([cost, count++, v, w]);
+          sigma[w] = 0;
+          P[w] = [v];
+        } else if (cost === seen[w]) {
+          sigma[w] += sigma[v];
+          P[w].push(v);
+        }
+      }
+    }
+
+    return [S, P, sigma];
+  };
+
+  brandes.index = neighborhoodIndex;
+
+  return brandes;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/package.json b/libs/shared/graph-layout/node_modules/graphology-shortest-path/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..229d30d25b19b267b00c4e7a9bcbbec830a88492
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "graphology-shortest-path",
+  "version": "2.0.0",
+  "description": "Shortest path functions for graphology.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "files": [
+    "*.d.ts",
+    "dijkstra.js",
+    "index.js",
+    "indexed-brandes.js",
+    "unweighted.js",
+    "utils.js"
+  ],
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "shortest path",
+    "dijkstra",
+    "a star"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.20.0"
+  },
+  "dependencies": {
+    "@yomguithereal/helpers": "^1.1.1",
+    "graphology-indices": "^0.16.3",
+    "graphology-utils": "^2.4.3",
+    "mnemonist": "^0.39.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.d.ts b/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1675280bee685a1ad385db8e5271062acf784c66
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.d.ts
@@ -0,0 +1,29 @@
+import Graph from 'graphology-types';
+
+type ShortestPath = Array<string>;
+type ShortestPathMapping = {[key: string]: ShortestPath};
+type ShortestPathLengthMapping = {[key: string]: number};
+
+type BrandesResult = [
+  Array<string>,
+  {[key: string]: Array<string>},
+  {[key: string]: number}
+];
+
+export function bidirectional(
+  graph: Graph,
+  source: unknown,
+  target: unknown
+): ShortestPath | null;
+
+export function singleSource(
+  graph: Graph,
+  source: unknown
+): ShortestPathMapping;
+
+export function singleSourceLength(
+  graph: Graph,
+  source: unknown
+): ShortestPathLengthMapping;
+
+export function brandes(graph: Graph, source: unknown): BrandesResult;
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.js b/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.js
new file mode 100644
index 0000000000000000000000000000000000000000..938804fa0a0559205cad56a0fa825b3c3f88920b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/unweighted.js
@@ -0,0 +1,350 @@
+/**
+ * Graphology Unweighted Shortest Path
+ * ====================================
+ *
+ * Basic algorithms to find the shortest paths between nodes in a graph
+ * whose edges are not weighted.
+ */
+var isGraph = require('graphology-utils/is-graph');
+var Queue = require('mnemonist/queue');
+var extend = require('@yomguithereal/helpers/extend');
+
+/**
+ * Function attempting to find the shortest path in a graph between
+ * given source & target or `null` if such a path does not exist.
+ *
+ * @param  {Graph}      graph  - Target graph.
+ * @param  {any}        source - Source node.
+ * @param  {any}        target - Target node.
+ * @return {array|null}        - Found path or `null`.
+ */
+function bidirectional(graph, source, target) {
+  if (!isGraph(graph))
+    throw new Error('graphology-shortest-path: invalid graphology instance.');
+
+  if (arguments.length < 3)
+    throw new Error(
+      'graphology-shortest-path: invalid number of arguments. Expecting at least 3.'
+    );
+
+  if (!graph.hasNode(source))
+    throw new Error(
+      'graphology-shortest-path: the "' +
+        source +
+        '" source node does not exist in the given graph.'
+    );
+
+  if (!graph.hasNode(target))
+    throw new Error(
+      'graphology-shortest-path: the "' +
+        target +
+        '" target node does not exist in the given graph.'
+    );
+
+  source = '' + source;
+  target = '' + target;
+
+  // TODO: do we need a self loop to go there?
+  if (source === target) {
+    return [source];
+  }
+
+  // Binding functions
+  var getPredecessors = graph.inboundNeighbors.bind(graph),
+    getSuccessors = graph.outboundNeighbors.bind(graph);
+
+  var predecessor = {},
+    successor = {};
+
+  // Predecessor & successor
+  predecessor[source] = null;
+  successor[target] = null;
+
+  // Fringes
+  var forwardFringe = [source],
+    reverseFringe = [target],
+    currentFringe,
+    node,
+    neighbors,
+    neighbor,
+    i,
+    j,
+    l,
+    m;
+
+  var found = false;
+
+  outer: while (forwardFringe.length && reverseFringe.length) {
+    if (forwardFringe.length <= reverseFringe.length) {
+      currentFringe = forwardFringe;
+      forwardFringe = [];
+
+      for (i = 0, l = currentFringe.length; i < l; i++) {
+        node = currentFringe[i];
+        neighbors = getSuccessors(node);
+
+        for (j = 0, m = neighbors.length; j < m; j++) {
+          neighbor = neighbors[j];
+
+          if (!(neighbor in predecessor)) {
+            forwardFringe.push(neighbor);
+            predecessor[neighbor] = node;
+          }
+
+          if (neighbor in successor) {
+            // Path is found!
+            found = true;
+            break outer;
+          }
+        }
+      }
+    } else {
+      currentFringe = reverseFringe;
+      reverseFringe = [];
+
+      for (i = 0, l = currentFringe.length; i < l; i++) {
+        node = currentFringe[i];
+        neighbors = getPredecessors(node);
+
+        for (j = 0, m = neighbors.length; j < m; j++) {
+          neighbor = neighbors[j];
+
+          if (!(neighbor in successor)) {
+            reverseFringe.push(neighbor);
+            successor[neighbor] = node;
+          }
+
+          if (neighbor in predecessor) {
+            // Path is found!
+            found = true;
+            break outer;
+          }
+        }
+      }
+    }
+  }
+
+  if (!found) return null;
+
+  var path = [];
+
+  while (neighbor) {
+    path.unshift(neighbor);
+    neighbor = predecessor[neighbor];
+  }
+
+  neighbor = successor[path[path.length - 1]];
+
+  while (neighbor) {
+    path.push(neighbor);
+    neighbor = successor[neighbor];
+  }
+
+  return path.length ? path : null;
+}
+
+/**
+ * Function attempting to find the shortest path in the graph between the
+ * given source node & all the other nodes.
+ *
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @return {object}        - The map of found paths.
+ */
+
+// TODO: cutoff option
+function singleSource(graph, source) {
+  if (!isGraph(graph))
+    throw new Error('graphology-shortest-path: invalid graphology instance.');
+
+  if (arguments.length < 2)
+    throw new Error(
+      'graphology-shortest-path: invalid number of arguments. Expecting at least 2.'
+    );
+
+  if (!graph.hasNode(source))
+    throw new Error(
+      'graphology-shortest-path: the "' +
+        source +
+        '" source node does not exist in the given graph.'
+    );
+
+  source = '' + source;
+
+  var nextLevel = {},
+    paths = {},
+    currentLevel,
+    neighbors,
+    v,
+    w,
+    i,
+    l;
+
+  nextLevel[source] = true;
+  paths[source] = [source];
+
+  while (Object.keys(nextLevel).length) {
+    currentLevel = nextLevel;
+    nextLevel = {};
+
+    for (v in currentLevel) {
+      neighbors = graph.outboundNeighbors(v);
+
+      for (i = 0, l = neighbors.length; i < l; i++) {
+        w = neighbors[i];
+
+        if (!paths[w]) {
+          paths[w] = paths[v].concat(w);
+          nextLevel[w] = true;
+        }
+      }
+    }
+  }
+
+  return paths;
+}
+
+/**
+ * Function attempting to find the shortest path lengths in the graph between
+ * the given source node & all the other nodes.
+ *
+ * @param  {string} method - Neighbor collection method name.
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @return {object}        - The map of found path lengths.
+ */
+
+// TODO: cutoff option
+function asbtractSingleSourceLength(method, graph, source) {
+  if (!isGraph(graph))
+    throw new Error('graphology-shortest-path: invalid graphology instance.');
+
+  if (!graph.hasNode(source))
+    throw new Error(
+      'graphology-shortest-path: the "' +
+        source +
+        '" source node does not exist in the given graph.'
+    );
+
+  source = '' + source;
+
+  // Performing BFS to count shortest paths
+  var seen = new Set();
+
+  var lengths = {},
+    level = 0;
+
+  lengths[source] = 0;
+
+  var currentLevel = [source];
+
+  var i, l, node;
+
+  while (currentLevel.length !== 0) {
+    var nextLevel = [];
+
+    for (i = 0, l = currentLevel.length; i < l; i++) {
+      node = currentLevel[i];
+
+      if (seen.has(node)) continue;
+
+      seen.add(node);
+      extend(nextLevel, graph[method](node));
+
+      lengths[node] = level;
+    }
+
+    level++;
+    currentLevel = nextLevel;
+  }
+
+  return lengths;
+}
+
+var singleSourceLength = asbtractSingleSourceLength.bind(
+  null,
+  'outboundNeighbors'
+);
+var undirectedSingleSourceLength = asbtractSingleSourceLength.bind(
+  null,
+  'neighbors'
+);
+
+/**
+ * Function using Ulrik Brandes' method to map single source shortest paths
+ * from selected node.
+ *
+ * [Reference]:
+ * Ulrik Brandes: A Faster Algorithm for Betweenness Centrality.
+ * Journal of Mathematical Sociology 25(2):163-177, 2001.
+ *
+ * @param  {Graph}  graph      - Target graph.
+ * @param  {any}    source     - Source node.
+ * @return {array}             - [Stack, Paths, Sigma]
+ */
+function brandes(graph, source) {
+  source = '' + source;
+
+  var S = [],
+    P = {},
+    sigma = {};
+
+  var nodes = graph.nodes(),
+    Dv,
+    sigmav,
+    neighbors,
+    v,
+    w,
+    i,
+    j,
+    l,
+    m;
+
+  for (i = 0, l = nodes.length; i < l; i++) {
+    v = nodes[i];
+    P[v] = [];
+    sigma[v] = 0;
+  }
+
+  var D = {};
+
+  sigma[source] = 1;
+  D[source] = 0;
+
+  var queue = Queue.of(source);
+
+  while (queue.size) {
+    v = queue.dequeue();
+    S.push(v);
+
+    Dv = D[v];
+    sigmav = sigma[v];
+
+    neighbors = graph.outboundNeighbors(v);
+
+    for (j = 0, m = neighbors.length; j < m; j++) {
+      w = neighbors[j];
+
+      if (!(w in D)) {
+        queue.enqueue(w);
+        D[w] = Dv + 1;
+      }
+
+      if (D[w] === Dv + 1) {
+        sigma[w] += sigmav;
+        P[w].push(v);
+      }
+    }
+  }
+
+  return [S, P, sigma];
+}
+
+/**
+ * Exporting.
+ */
+exports.bidirectional = bidirectional;
+exports.singleSource = singleSource;
+exports.singleSourceLength = singleSourceLength;
+exports.undirectedSingleSourceLength = undirectedSingleSourceLength;
+exports.brandes = brandes;
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.d.ts b/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b4b866f8c2b5e2665622833f601dc800debb6a6d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.d.ts
@@ -0,0 +1,6 @@
+import Graph from 'graphology-types';
+
+export function edgePathFromNodePath(
+  graph: Graph,
+  nodePath: string[]
+): string[];
diff --git a/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.js b/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..88210ced28e8aae7b0e45135dd57537273d44ddd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-shortest-path/utils.js
@@ -0,0 +1,50 @@
+/**
+ * Graphology Shortest Path Utils
+ * ===============================
+ *
+ * Miscellaneous shortest-path helper functions.
+ */
+var returnTrue = function () {
+  return true;
+};
+
+exports.edgePathFromNodePath = function (graph, nodePath) {
+  var l = nodePath.length;
+
+  var i, source, target, edge;
+
+  // Self loops
+  if (l < 2) {
+    source = nodePath[0];
+
+    edge = graph.multi
+      ? graph.findEdge(source, source, returnTrue)
+      : graph.edge(source, source);
+
+    if (edge) return [edge];
+
+    return [];
+  }
+
+  l--;
+
+  var edgePath = new Array(l);
+
+  for (i = 0; i < l; i++) {
+    source = nodePath[i];
+    target = nodePath[i + 1];
+
+    edge = graph.multi
+      ? graph.findOutboundEdge(source, target, returnTrue)
+      : graph.edge(source, target);
+
+    if (edge === undefined)
+      throw new Error(
+        'graphology-shortest-path: given path is impossible in given graph.'
+      );
+
+    edgePath[i] = edge;
+  }
+
+  return edgePath;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology-utils/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df12ee58e0ff4fa313b5bc24e694918ccc1b363a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/README.md b/libs/shared/graph-layout/node_modules/graphology-utils/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..59c13fd0eede7af00ea5cc498e9d7a96c405299c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/README.md
@@ -0,0 +1,274 @@
+# Graphology Utils
+
+Miscellaneous utility functions to be used with [`graphology`](https://graphology.github.io).
+
+## Installation
+
+```
+npm install graphology-utils
+```
+
+## Usage
+
+_Assertions_
+
+- [#.isGraph](#isgraph)
+- [#.isGraphConstructor](#isgraphconstructor)
+
+_Introspection_
+
+- [#.inferMulti](#infermulti)
+- [#.inferType](#infertype)
+
+_Typical edge patterns_
+
+- [#.mergeClique](#mergeclique)
+- [#.mergeCycle](#mergecycle)
+- [#.mergePath](#mergepath)
+- [#.mergeStar](#mergestar)
+
+_Miscellaneous helpers_
+
+- [#.renameGraphKeys](#renamegraphkeys)
+- [#.updateGraphKeys](#updategraphkeys)
+
+### #.isGraph
+
+Function returning whether the given value is a `graphology` implementation's instance.
+
+```js
+import {isGraph} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import isGraph from 'graphology-utils/is-graph';
+
+const graph = new Graph();
+
+isGraph(graph);
+>>> true
+
+isGraph(45);
+>>> false
+
+isGraph({hello: 'world'});
+>>> false
+```
+
+_Arguments_
+
+- **value** _any_: value to test.
+
+### #.isGraphConstructor
+
+Function returning whether the given value is a `graphology` constructor.
+
+```js
+import {isGraphConstructor} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import isGraphConstructor from 'graphology-utils/is-graph-constructor';
+
+isGraphConstructor(Graph);
+>>> true
+
+isGraphConstructor(45);
+>>> false
+
+isGraphConstructor(new Graph());
+>>> false
+```
+
+_Arguments_
+
+- **value** _any_: value to test.
+
+### #.inferMulti
+
+Function returning whether the given graph is truly multi, i.e. if we can find at least one occurrence of multiple edges of the same type and direction between two nodes.
+
+```js
+import {inferMulti} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import inferMulti from 'graphology-utils/infer-multi';
+
+const graph = new MultiGraph();
+graph.addEdge(1, 2);
+
+inferMulti(graph);
+>>> false
+
+graph.addEdge(1, 2);
+
+inferMulti(graph);
+>>> true
+```
+
+### #.inferType
+
+Function returning the inferred type of the given graph. This function is useful to check whether a given mixed graph is in fact a mere `directed` or `undirected` graph based on its actual edges.
+
+```js
+import {inferType} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import inferType from 'graphology-utils/infer-type';
+
+const graph = new Graph();
+graph.addUndirectedEdge(1, 2);
+
+inferType(graph);
+>>> 'directed'
+```
+
+### #.mergeClique
+
+Function adding a clique to the given graph.
+
+```js
+import {mergeClique} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import mergeClique from 'graphology-utils/merge-clique';
+
+const graph = new Graph();
+
+mergeClique(graph, [1, 2, 3]);
+graph.edges().map(e => graph.extremities(e));
+>>> [[1, 2], [1, 3], [2, 3]]
+```
+
+### #.mergeCycle
+
+Function adding a cycle to the given graph.
+
+```js
+import {mergeCycle} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import mergeCycle from 'graphology-utils/merge-cycle';
+
+const graph = new Graph();
+
+mergeCycle(graph, [1, 2, 3, 4, 5]);
+graph.edges().map(e => graph.extremities(e));
+>>> [[1, 2], [2, 3], [3, 4], [4, 5], [5, 1]]
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **cycle** _array_: array of nodes representing the cycle to add.
+
+### #.mergePath
+
+Function adding a path to the given graph.
+
+```js
+import {mergePath} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import mergePath from 'graphology-utils/merge-path';
+
+const graph = new Graph();
+
+mergePath(graph, [1, 2, 3, 4, 5]);
+graph.edges().map(e => graph.extremities(e));
+>>> [[1, 2], [2, 3], [3, 4], [4, 5]]
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **path** _array_: array of nodes representing the path to add.
+
+### #.mergeStar
+
+Function adding a star to the given graph.
+
+```js
+import {mergeStar} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import mergeStar from 'graphology-utils/merge-star';
+
+const graph = new Graph();
+
+mergeStar(graph, [1, 2, 3, 4, 5]);
+graph.edges().map(e => graph.extremities(e));
+>>> [[1, 2], [1, 3], [1, 4], [1, 5]]
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **star** _array_: array of nodes representing the star to add.
+
+### #.renameGraphKeys
+
+Function renaming the nodes & edges key of a graph using mappings and returning a new graph with renamed keys.
+
+```js
+import {renameGraphKeys} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import renameGraphKeys from 'graphology-utils/rename-graph-keys';
+
+const graph = new Graph();
+graph.addNode('Martha');
+graph.addNode('Catherine');
+graph.addNode('John');
+graph.addEdgeWithKey('M->C', 'Martha', 'Catherine');
+graph.addEdgeWithKey('C->J', 'Catherine', 'John');
+
+const renamedGraph = renameGraphKeys(
+  graph,
+  {Martha: 1, Catherine: 2, John: 3},
+  {'M->C': 'rel1', 'C->J': 'rel2'}
+);
+
+renamedGraph.nodes();
+>>> [1, 2, 3];
+
+renamedGraph.edges();
+>>> ['rel1', 'rel2'];
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **nodeKeyMapping** _object_: A key/value map for the node key mapping.
+- **edgeKeyMapping** _?object_: A key/value map for the edge key mapping.
+
+### #.updateGraphKeys
+
+Function updating the nodes & edges key of a graph using functions and returning a new graph with updated keys.
+
+```js
+import {updateGraphKeys} from 'graphology-utils';
+// Alternatively, if you want to only load the relevant code:
+import updateGraphKeys from 'graphology-utils/update-graph-keys';
+
+const graph = new Graph();
+graph.addNode('Martha');
+graph.addNode('Catherine');
+graph.addNode('John');
+graph.addEdgeWithKey('M->C', 'Martha', 'Catherine');
+graph.addEdgeWithKey('C->J', 'Catherine', 'John');
+
+const updatedGraph = updateGraphKeys(
+  graph,
+  (key)=> {
+    if (key === 'Martha') return 1;
+    if (key === 'Catherine') return 2;
+    return 3;
+  },
+  (key) => {
+    if (key === 'M->C') return 'rel1';
+    return 'rel2';
+  }
+);
+
+updatedGraph.nodes();
+>>> [1, 2, 3];
+
+updatedGraph.edges();
+>>> ['rel1', 'rel2'];
+```
+
+_Arguments_
+
+- **graph** _Graph_: target graph.
+- **nodeKeyUdater** _function_: A function to compute a new node key from the same arguments that would be given to [`#.forEachNode`](https://graphology.github.io/iteration.html#foreachnode).
+- **edgeKeyUpdater** _function_: A function to compute a new edge key from the same arguments that would be given to [`#.forEachEdge`](https://graphology.github.io/iteration.html#foreachedge).
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4dee01148216c7124bd589d857be7ba3c0fa08c7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.d.ts
@@ -0,0 +1,37 @@
+import Graph, {Attributes, EdgeMergeResult} from 'graphology-types';
+
+export function addEdge<EdgeAttributes extends Attributes = Attributes>(
+  graph: Graph,
+  undirected: boolean,
+  key: unknown,
+  source: unknown,
+  target: unknown,
+  attributes?: EdgeAttributes
+): string;
+
+export function copyEdge<EdgeAttributes extends Attributes = Attributes>(
+  graph: Graph,
+  undirected: boolean,
+  key: unknown,
+  source: unknown,
+  target: unknown,
+  attributes?: EdgeAttributes
+): string;
+
+export function mergeEdge<EdgeAttributes extends Attributes = Attributes>(
+  graph: Graph,
+  undirected: boolean,
+  key: unknown,
+  source: unknown,
+  target: unknown,
+  attributes?: EdgeAttributes
+): EdgeMergeResult;
+
+export function updateEdge<EdgeAttributes extends Attributes = Attributes>(
+  graph: Graph,
+  undirected: boolean,
+  key: unknown,
+  source: unknown,
+  target: unknown,
+  updater?: (attributes: EdgeAttributes | {}) => EdgeAttributes
+): EdgeMergeResult;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.js b/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.js
new file mode 100644
index 0000000000000000000000000000000000000000..20d13ad6d00b17f1219f986b5a971a68fb1a5f43
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/add-edge.js
@@ -0,0 +1,85 @@
+/**
+ * Graphology Edge Adders
+ * =======================
+ *
+ * Generic edge addition functions that can be used to avoid nasty repetitive
+ * conditions.
+ */
+exports.addEdge = function addEdge(
+  graph,
+  undirected,
+  key,
+  source,
+  target,
+  attributes
+) {
+  if (undirected) {
+    if (key === null || key === undefined)
+      return graph.addUndirectedEdge(source, target, attributes);
+    else return graph.addUndirectedEdgeWithKey(key, source, target, attributes);
+  } else {
+    if (key === null || key === undefined)
+      return graph.addDirectedEdge(source, target, attributes);
+    else return graph.addDirectedEdgeWithKey(key, source, target, attributes);
+  }
+};
+
+exports.copyEdge = function copyEdge(
+  graph,
+  undirected,
+  key,
+  source,
+  target,
+  attributes
+) {
+  attributes = Object.assign({}, attributes);
+
+  if (undirected) {
+    if (key === null || key === undefined)
+      return graph.addUndirectedEdge(source, target, attributes);
+    else return graph.addUndirectedEdgeWithKey(key, source, target, attributes);
+  } else {
+    if (key === null || key === undefined)
+      return graph.addDirectedEdge(source, target, attributes);
+    else return graph.addDirectedEdgeWithKey(key, source, target, attributes);
+  }
+};
+
+exports.mergeEdge = function mergeEdge(
+  graph,
+  undirected,
+  key,
+  source,
+  target,
+  attributes
+) {
+  if (undirected) {
+    if (key === null || key === undefined)
+      return graph.mergeUndirectedEdge(source, target, attributes);
+    else
+      return graph.mergeUndirectedEdgeWithKey(key, source, target, attributes);
+  } else {
+    if (key === null || key === undefined)
+      return graph.mergeDirectedEdge(source, target, attributes);
+    else return graph.mergeDirectedEdgeWithKey(key, source, target, attributes);
+  }
+};
+
+exports.updateEdge = function updateEdge(
+  graph,
+  undirected,
+  key,
+  source,
+  target,
+  updater
+) {
+  if (undirected) {
+    if (key === null || key === undefined)
+      return graph.updateUndirectedEdge(source, target, updater);
+    else return graph.updateUndirectedEdgeWithKey(key, source, target, updater);
+  } else {
+    if (key === null || key === undefined)
+      return graph.updateDirectedEdge(source, target, updater);
+    else return graph.updateDirectedEdgeWithKey(key, source, target, updater);
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/add-node.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/add-node.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4bc89135787101abb00c63aa1394bd5f5df2581e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/add-node.d.ts
@@ -0,0 +1,7 @@
+import Graph, {Attributes} from 'graphology-types';
+
+export function copyNode<NodeAttributes extends Attributes = Attributes>(
+  graph: Graph,
+  key: unknown,
+  attributes?: NodeAttributes
+): string;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/add-node.js b/libs/shared/graph-layout/node_modules/graphology-utils/add-node.js
new file mode 100644
index 0000000000000000000000000000000000000000..0eb14328ceff2311fe2fd6ac87f21fca48c696a3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/add-node.js
@@ -0,0 +1,11 @@
+/**
+ * Graphology Node Adders
+ * =======================
+ *
+ * Generic node addition functions that can be used to avoid nasty repetitive
+ * conditions.
+ */
+exports.copyNode = function (graph, key, attributes) {
+  attributes = Object.assign({}, attributes);
+  return graph.addNode(key, attributes);
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/default.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/default.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71d0e5e1b910fe61b1af241e4a37000e37212631
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/default.d.ts
@@ -0,0 +1,4 @@
+export default function resolveDefaults<T extends {[key: string]: any}>(
+  target: T | undefined | null,
+  defaults: T
+): T;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/defaults.js b/libs/shared/graph-layout/node_modules/graphology-utils/defaults.js
new file mode 100644
index 0000000000000000000000000000000000000000..263f1e1c0fe1151965fe74d071077472fd11cbf4
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/defaults.js
@@ -0,0 +1,47 @@
+/**
+ * Graphology Defaults
+ * ====================
+ *
+ * Helper function used throughout the standard lib to resolve defaults.
+ */
+function isLeaf(o) {
+  return (
+    !o ||
+    typeof o !== 'object' ||
+    typeof o === 'function' ||
+    Array.isArray(o) ||
+    o instanceof Set ||
+    o instanceof Map ||
+    o instanceof RegExp ||
+    o instanceof Date
+  );
+}
+
+function resolveDefaults(target, defaults) {
+  target = target || {};
+
+  var output = {};
+
+  for (var k in defaults) {
+    var existing = target[k];
+    var def = defaults[k];
+
+    // Recursion
+    if (!isLeaf(def)) {
+      output[k] = resolveDefaults(existing, def);
+
+      continue;
+    }
+
+    // Leaf
+    if (existing === undefined) {
+      output[k] = def;
+    } else {
+      output[k] = existing;
+    }
+  }
+
+  return output;
+}
+
+module.exports = resolveDefaults;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/getters.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/getters.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..93ec8518e6c033aba95254624834460ecb831028
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/getters.d.ts
@@ -0,0 +1,79 @@
+import Graph, {Attributes, NodeMapper, EdgeMapper} from 'graphology-types';
+
+export type PartialEdgeMapper<
+  T,
+  EdgeAttributes extends Attributes = Attributes
+> = (
+  edge: string,
+  attributes: EdgeAttributes,
+  source: string,
+  target: string
+) => T;
+
+export type MinimalEdgeMapper<
+  T,
+  EdgeAttributes extends Attributes = Attributes
+> = (edge: string, attributes: EdgeAttributes) => T;
+
+interface NodeValueGetter<T, NodeAttributes extends Attributes = Attributes> {
+  fromGraph(graph: Graph<NodeAttributes>, node: unknown): T;
+  fromAttributes(attributes: NodeAttributes): T;
+  fromEntry: NodeMapper<T, NodeAttributes>;
+}
+
+interface EdgeValueGetter<
+  T,
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+> {
+  fromGraph(graph: Graph<NodeAttributes, EdgeAttributes>, edge: unknown): T;
+  // fromPath(
+  //   graph: Graph<NodeAttributes, EdgeAttributes>,
+  //   source: unknown,
+  //   target: unknown
+  // ): T;
+  // fromDirectedPath(
+  //   graph: Graph<NodeAttributes, EdgeAttributes>,
+  //   source: unknown,
+  //   target: unknown
+  // ): T;
+  // fromUndirectedPath(
+  //   graph: Graph<NodeAttributes, EdgeAttributes>,
+  //   source: unknown,
+  //   target: unknown
+  // ): T;
+  fromAttributes(attributes: EdgeAttributes): T;
+  fromEntry: EdgeMapper<T, NodeAttributes, EdgeAttributes>;
+  fromPartialEntry: PartialEdgeMapper<T, EdgeAttributes>;
+  fromMinimalEntry: MinimalEdgeMapper<T, EdgeAttributes>;
+}
+
+export function createNodeValueGetter<
+  T,
+  NodeAttributes extends Attributes = Attributes
+>(
+  target?: string | NodeMapper<T, NodeAttributes>,
+  defaultValue?: T | ((value: unknown) => T)
+): NodeValueGetter<T, NodeAttributes>;
+
+export function createEdgeValueGetter<
+  T,
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  target?:
+    | string
+    | EdgeMapper<T, NodeAttributes, EdgeAttributes>
+    | PartialEdgeMapper<T, EdgeAttributes>,
+  defaultValue?: T | ((value: unknown) => T)
+): EdgeValueGetter<T, NodeAttributes, EdgeAttributes>;
+
+export function createEdgeWeightGetter<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  target?:
+    | string
+    | EdgeMapper<number, NodeAttributes, EdgeAttributes>
+    | PartialEdgeMapper<number, EdgeAttributes>
+): EdgeValueGetter<number, NodeAttributes, EdgeAttributes>;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/getters.js b/libs/shared/graph-layout/node_modules/graphology-utils/getters.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd933151d681515442918bec746122b1c55087aa
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/getters.js
@@ -0,0 +1,137 @@
+/**
+ * Graphology Weight Getter
+ * =========================
+ *
+ * Function creating weight getters.
+ */
+function coerceWeight(value) {
+  // Ensuring target value is a correct number
+  if (typeof value !== 'number' || isNaN(value)) return 1;
+
+  return value;
+}
+
+function createNodeValueGetter(nameOrFunction, defaultValue) {
+  var getter = {};
+
+  var coerceToDefault = function (v) {
+    if (typeof v === 'undefined') return defaultValue;
+
+    return v;
+  };
+
+  if (typeof defaultValue === 'function') coerceToDefault = defaultValue;
+
+  var get = function (attributes) {
+    return coerceToDefault(attributes[nameOrFunction]);
+  };
+
+  var returnDefault = function () {
+    return coerceToDefault(undefined);
+  };
+
+  if (typeof nameOrFunction === 'string') {
+    getter.fromAttributes = get;
+    getter.fromGraph = function (graph, node) {
+      return get(graph.getNodeAttributes(node));
+    };
+    getter.fromEntry = function (node, attributes) {
+      return get(attributes);
+    };
+  } else if (typeof nameOrFunction === 'function') {
+    getter.fromAttributes = function () {
+      throw new Error(
+        'graphology-utils/getters/createNodeValueGetter: irrelevant usage.'
+      );
+    };
+    getter.fromGraph = function (graph, node) {
+      return coerceToDefault(
+        nameOrFunction(node, graph.getNodeAttributes(node))
+      );
+    };
+    getter.fromEntry = function (node, attributes) {
+      return coerceToDefault(nameOrFunction(node, attributes));
+    };
+  } else {
+    getter.fromAttributes = returnDefault;
+    getter.fromGraph = returnDefault;
+    getter.fromEntry = returnDefault;
+  }
+
+  return getter;
+}
+
+function createEdgeValueGetter(nameOrFunction, defaultValue) {
+  var getter = {};
+
+  var coerceToDefault = function (v) {
+    if (typeof v === 'undefined') return defaultValue;
+
+    return v;
+  };
+
+  if (typeof defaultValue === 'function') coerceToDefault = defaultValue;
+
+  var get = function (attributes) {
+    return coerceToDefault(attributes[nameOrFunction]);
+  };
+
+  var returnDefault = function () {
+    return coerceToDefault(undefined);
+  };
+
+  if (typeof nameOrFunction === 'string') {
+    getter.fromAttributes = get;
+    getter.fromGraph = function (graph, edge) {
+      return get(graph.getEdgeAttributes(edge));
+    };
+    getter.fromEntry = function (edge, attributes) {
+      return get(attributes);
+    };
+    getter.fromPartialEntry = getter.fromEntry;
+    getter.fromMinimalEntry = getter.fromEntry;
+  } else if (typeof nameOrFunction === 'function') {
+    getter.fromAttributes = function () {
+      throw new Error(
+        'graphology-utils/getters/createEdgeValueGetter: irrelevant usage.'
+      );
+    };
+    getter.fromGraph = function (graph, edge) {
+      // TODO: we can do better, check #310
+      var extremities = graph.extremities(edge);
+      return coerceToDefault(
+        nameOrFunction(
+          edge,
+          graph.getEdgeAttributes(edge),
+          extremities[0],
+          extremities[1],
+          graph.getNodeAttributes(extremities[0]),
+          graph.getNodeAttributes(extremities[1]),
+          graph.isUndirected(edge)
+        )
+      );
+    };
+    getter.fromEntry = function (e, a, s, t, sa, ta, u) {
+      return coerceToDefault(nameOrFunction(e, a, s, t, sa, ta, u));
+    };
+    getter.fromPartialEntry = function (e, a, s, t) {
+      return coerceToDefault(nameOrFunction(e, a, s, t));
+    };
+    getter.fromMinimalEntry = function (e, a) {
+      return coerceToDefault(nameOrFunction(e, a));
+    };
+  } else {
+    getter.fromAttributes = returnDefault;
+    getter.fromGraph = returnDefault;
+    getter.fromEntry = returnDefault;
+    getter.fromMinimalEntry = returnDefault;
+  }
+
+  return getter;
+}
+
+exports.createNodeValueGetter = createNodeValueGetter;
+exports.createEdgeValueGetter = createEdgeValueGetter;
+exports.createEdgeWeightGetter = function (name) {
+  return createEdgeValueGetter(name, coerceWeight);
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/index.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab390bf0c29f614ff6b46e45213b8bc85ee3b5a0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/index.d.ts
@@ -0,0 +1,10 @@
+export {default as inferMulti} from './infer-multi';
+export {default as inferType} from './infer-type';
+export {default as isGraph} from './is-graph';
+export {default as isGraphConstructor} from './is-graph-constructor';
+export {default as mergeClique} from './merge-clique';
+export {default as mergeCycle} from './merge-cycle';
+export {default as mergePath} from './merge-path';
+export {default as mergeStar} from './merge-star';
+export {default as renameGraphKeys} from './rename-graph-keys';
+export {default as updateGraphKeys} from './update-graph-keys';
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/index.js b/libs/shared/graph-layout/node_modules/graphology-utils/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..517d5ef2dcf176a6433e080109a86c03e5a4a234
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/index.js
@@ -0,0 +1,16 @@
+/**
+ * Graphology Utils
+ * =================
+ *
+ * Library endpoint.
+ */
+exports.inferMulti = require('./infer-multi.js');
+exports.inferType = require('./infer-type.js');
+exports.isGraph = require('./is-graph.js');
+exports.isGraphConstructor = require('./is-graph-constructor.js');
+exports.mergeClique = require('./merge-clique.js');
+exports.mergeCycle = require('./merge-cycle.js');
+exports.mergePath = require('./merge-path.js');
+exports.mergeStar = require('./merge-star.js');
+exports.renameGraphKeys = require('./rename-graph-keys.js');
+exports.updateGraphKeys = require('./update-graph-keys.js');
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2552a2814427128e04e9bd80d66d2f9c88269d9b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function inferMulti(graph: Graph): boolean;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.js b/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.js
new file mode 100644
index 0000000000000000000000000000000000000000..02c1f0397e757b1f16f24c1254ce1fdea09ee207
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/infer-multi.js
@@ -0,0 +1,48 @@
+/**
+ * Graphology inferMulti
+ * ======================
+ *
+ * Useful function used to "guess" if the given graph is truly multi.
+ */
+var isGraph = require('./is-graph.js');
+
+/**
+ * Returning whether the given graph is inferred as multi.
+ *
+ * @param  {Graph}   graph - Target graph.
+ * @return {boolean}
+ */
+module.exports = function inferMulti(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-utils/infer-multi: expecting a valid graphology instance.'
+    );
+
+  if (!graph.multi || graph.order === 0 || graph.size < 2) return false;
+
+  var multi = false;
+
+  // TODO: improve this with suitable methods
+  var previousSource, previousTarget, wasUndirected, tmp;
+
+  graph.forEachAssymetricAdjacencyEntry(function (s, t, sa, ta, e, ea, u) {
+    if (multi) return; // TODO: we need #.someAdjacencyEntry
+
+    if (u && s > t) {
+      tmp = t;
+      t = s;
+      s = tmp;
+    }
+
+    if (s === previousSource && t === previousTarget && u === wasUndirected) {
+      multi = true;
+      return;
+    }
+
+    previousSource = s;
+    previousTarget = t;
+    wasUndirected = u;
+  });
+
+  return multi;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87cd2eaee52988a9594f195d34d69d3c52c89e0f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.d.ts
@@ -0,0 +1,3 @@
+import Graph, {GraphType} from 'graphology-types';
+
+export default function inferType(graph: Graph): GraphType;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.js b/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.js
new file mode 100644
index 0000000000000000000000000000000000000000..186aeadf5edbd4744a121a0875855d6d8adcc31b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/infer-type.js
@@ -0,0 +1,35 @@
+/**
+ * Graphology inferType
+ * =====================
+ *
+ * Useful function used to "guess" the real type of the given Graph using
+ * introspection.
+ */
+var isGraph = require('./is-graph.js');
+
+/**
+ * Returning the inferred type of the given graph.
+ *
+ * @param  {Graph}   graph - Target graph.
+ * @return {boolean}
+ */
+module.exports = function inferType(graph) {
+  if (!isGraph(graph))
+    throw new Error(
+      'graphology-utils/infer-type: expecting a valid graphology instance.'
+    );
+
+  var declaredType = graph.type;
+
+  if (declaredType !== 'mixed') return declaredType;
+
+  if (
+    (graph.directedSize === 0 && graph.undirectedSize === 0) ||
+    (graph.directedSize > 0 && graph.undirectedSize > 0)
+  )
+    return 'mixed';
+
+  if (graph.directedSize > 0) return 'directed';
+
+  return 'undirected';
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..25368247a1716f6a791cd862846df09bd26f719c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.d.ts
@@ -0,0 +1 @@
+export default function isGraphConstructor(value: any): boolean;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.js b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.js
new file mode 100644
index 0000000000000000000000000000000000000000..61335f6fb5ba48a93562f408b25dfcd53a60031d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph-constructor.js
@@ -0,0 +1,23 @@
+/**
+ * Graphology isGraphConstructor
+ * ==============================
+ *
+ * Very simple function aiming at ensuring the given variable is a
+ * graphology constructor.
+ */
+
+/**
+ * Checking the value is a graphology constructor.
+ *
+ * @param  {any}     value - Target value.
+ * @return {boolean}
+ */
+module.exports = function isGraphConstructor(value) {
+  return (
+    value !== null &&
+    typeof value === 'function' &&
+    typeof value.prototype === 'object' &&
+    typeof value.prototype.addUndirectedEdgeWithKey === 'function' &&
+    typeof value.prototype.dropNode === 'function'
+  );
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..477c593763f20bf41d13177244fd710bea2ae44c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.d.ts
@@ -0,0 +1 @@
+export default function isGraph(value: any): boolean;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.js b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.js
new file mode 100644
index 0000000000000000000000000000000000000000..002184b7e7f1b7a9540486b896e951b11c2f1c67
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/is-graph.js
@@ -0,0 +1,23 @@
+/**
+ * Graphology isGraph
+ * ===================
+ *
+ * Very simple function aiming at ensuring the given variable is a
+ * graphology instance.
+ */
+
+/**
+ * Checking the value is a graphology instance.
+ *
+ * @param  {any}     value - Target value.
+ * @return {boolean}
+ */
+module.exports = function isGraph(value) {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    typeof value.addUndirectedEdgeWithKey === 'function' &&
+    typeof value.dropNode === 'function' &&
+    typeof value.multi === 'boolean'
+  );
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac9bfffa5847d33b2d6ea9489516b9ca6d359f1b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function mergeClique(graph: Graph, nodes: Array<unknown>): void;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.js b/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.js
new file mode 100644
index 0000000000000000000000000000000000000000..09a9fbc30c922df78929457313af9bd5d6c13a7a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-clique.js
@@ -0,0 +1,28 @@
+/**
+ * Graphology mergeClique
+ * =======================
+ *
+ * Function merging the given clique to the graph.
+ */
+
+/**
+ * Merging the given clique to the graph.
+ *
+ * @param  {Graph} graph - Target graph.
+ * @param  {array} nodes - Nodes representing the clique to merge.
+ */
+module.exports = function mergeClique(graph, nodes) {
+  if (nodes.length === 0) return;
+
+  var source, target, i, j, l;
+
+  for (i = 0, l = nodes.length; i < l; i++) {
+    source = nodes[i];
+
+    for (j = i + 1; j < l; j++) {
+      target = nodes[j];
+
+      graph.mergeEdge(source, target);
+    }
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bc5858e9cadd93ae987776abd675678375420b8d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function mergeCycle(graph: Graph, nodes: Array<unknown>): void;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.js b/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.js
new file mode 100644
index 0000000000000000000000000000000000000000..d768043cf984f2b23184850afe5f672fdc168970
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-cycle.js
@@ -0,0 +1,31 @@
+/**
+ * Graphology mergeCycle
+ * =====================
+ *
+ * Function merging the given cycle to the graph.
+ */
+
+/**
+ * Merging the given cycle to the graph.
+ *
+ * @param  {Graph} graph - Target graph.
+ * @param  {array} nodes - Nodes representing the cycle to merge.
+ */
+module.exports = function mergeCycle(graph, nodes) {
+  if (nodes.length === 0) return;
+
+  var previousNode, node, i, l;
+
+  graph.mergeNode(nodes[0]);
+
+  if (nodes.length === 1) return;
+
+  for (i = 1, l = nodes.length; i < l; i++) {
+    previousNode = nodes[i - 1];
+    node = nodes[i];
+
+    graph.mergeEdge(previousNode, node);
+  }
+
+  graph.mergeEdge(node, nodes[0]);
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9478975a4005f66b0f4face7fee3915f5786ef9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function mergePath(graph: Graph, nodes: Array<unknown>): void;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.js b/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.js
new file mode 100644
index 0000000000000000000000000000000000000000..7bf77a316b6f6df3d6b94cf355f3351c23467ea8
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-path.js
@@ -0,0 +1,27 @@
+/**
+ * Graphology mergePath
+ * =====================
+ *
+ * Function merging the given path to the graph.
+ */
+
+/**
+ * Merging the given path to the graph.
+ *
+ * @param  {Graph} graph - Target graph.
+ * @param  {array} nodes - Nodes representing the path to merge.
+ */
+module.exports = function mergePath(graph, nodes) {
+  if (nodes.length === 0) return;
+
+  var previousNode, node, i, l;
+
+  graph.mergeNode(nodes[0]);
+
+  for (i = 1, l = nodes.length; i < l; i++) {
+    previousNode = nodes[i - 1];
+    node = nodes[i];
+
+    graph.mergeEdge(previousNode, node);
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..950900e9ed26e0fd0da2507ed6aea11d9fdf7df6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.d.ts
@@ -0,0 +1,3 @@
+import Graph from 'graphology-types';
+
+export default function mergeStar(graph: Graph, nodes: Array<unknown>): void;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.js b/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1340cd290b5acbed3062a671ee47cdfa88b5082
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/merge-star.js
@@ -0,0 +1,28 @@
+/**
+ * Graphology mergeStar
+ * =====================
+ *
+ * Function merging the given star to the graph.
+ */
+
+/**
+ * Merging the given star to the graph.
+ *
+ * @param  {Graph} graph - Target graph.
+ * @param  {array} nodes - Nodes to add, first one being the center of the star.
+ */
+module.exports = function mergeStar(graph, nodes) {
+  if (nodes.length === 0) return;
+
+  var node, i, l;
+
+  var center = nodes[0];
+
+  graph.mergeNode(center);
+
+  for (i = 1, l = nodes.length; i < l; i++) {
+    node = nodes[i];
+
+    graph.mergeEdge(center, node);
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/package.json b/libs/shared/graph-layout/node_modules/graphology-utils/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..96a41cbd1a5d7a804cd2dd2df8ca7ba6f0eb749c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/package.json
@@ -0,0 +1,56 @@
+{
+  "name": "graphology-utils",
+  "version": "2.5.1",
+  "description": "Miscellaneous utils for graphology.",
+  "main": "index.js",
+  "files": [
+    "*.d.ts",
+    "add-edge.js",
+    "add-node.js",
+    "defaults.js",
+    "getters.js",
+    "index.js",
+    "infer-type.js",
+    "infer-multi.js",
+    "is-graph.js",
+    "is-graph-constructor.js",
+    "merge-clique.js",
+    "merge-cycle.js",
+    "merge-path.js",
+    "merge-star.js",
+    "rename-graph-keys.js",
+    "update-graph-keys.js"
+  ],
+  "types": "./index.d.ts",
+  "scripts": {
+    "prepublishOnly": "npm test",
+    "test": "mocha test.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "keywords": [
+    "graph",
+    "graphology",
+    "utils"
+  ],
+  "contributors": [
+    {
+      "name": "Guillaume Plique",
+      "url": "http://github.com/Yomguithereal"
+    },
+    {
+      "name": "Jules Farjas",
+      "url": "http://github.com/farjasju"
+    }
+  ],
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "peerDependencies": {
+    "graphology-types": ">=0.23.0"
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c1f392cf79607ac4afb5f611a3feab4f5ee8d471
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.d.ts
@@ -0,0 +1,7 @@
+import Graph from 'graphology-types';
+
+export default function renameGraphKeys(
+  graph: Graph,
+  nodeKeyMapping: Record<string, unknown>,
+  edgeKeyMapping: Record<string, unknown>
+): Graph;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.js b/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.js
new file mode 100644
index 0000000000000000000000000000000000000000..79c4336407f24a9da2b4f959e960a94eb64e69b9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/rename-graph-keys.js
@@ -0,0 +1,68 @@
+/**
+ * Graphology Rename Graph Keys
+ * =============================
+ *
+ * Helpers allowing you to rename (ie. change the key) of nodes & edges .
+ */
+var copyEdge = require('./add-edge.js').copyEdge;
+
+module.exports = function renameGraphKeys(
+  graph,
+  nodeKeyMapping,
+  edgeKeyMapping
+) {
+  if (typeof nodeKeyMapping === 'undefined') nodeKeyMapping = {};
+  if (typeof edgeKeyMapping === 'undefined') edgeKeyMapping = {};
+
+  var renamed = graph.nullCopy();
+
+  // Renaming nodes
+  graph.forEachNode(function (key, attr) {
+    var renamedKey = nodeKeyMapping[key];
+
+    if (typeof renamedKey === 'undefined') renamedKey = key;
+
+    renamed.addNode(renamedKey, attr);
+  });
+
+  // Renaming edges
+  var currentSource, currentSourceRenamed;
+
+  graph.forEachAssymetricAdjacencyEntry(function (
+    source,
+    target,
+    _sa,
+    _ta,
+    key,
+    attr,
+    undirected
+  ) {
+    // Leveraging the ordered adjacency to save lookups
+    if (source !== currentSource) {
+      currentSource = source;
+      currentSourceRenamed = nodeKeyMapping[source];
+
+      if (typeof currentSourceRenamed === 'undefined')
+        currentSourceRenamed = source;
+    }
+
+    var targetRenamed = nodeKeyMapping[target];
+
+    if (typeof targetRenamed === 'undefined') targetRenamed = target;
+
+    var renamedKey = edgeKeyMapping[key];
+
+    if (typeof renamedKey === 'undefined') renamedKey = key;
+
+    copyEdge(
+      renamed,
+      undirected,
+      renamedKey,
+      currentSourceRenamed,
+      targetRenamed,
+      attr
+    );
+  });
+
+  return renamed;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.d.ts b/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df7f3eef93d914be211492c439d8a699fb82b6c2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.d.ts
@@ -0,0 +1,18 @@
+import Graph, {Attributes} from 'graphology-types';
+
+export default function updateGraphKeys<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes
+>(
+  graph: Graph<NodeAttributes, EdgeAttributes>,
+  nodeKeyUpdater: (key: string, attributes: NodeAttributes) => unknown,
+  edgeKeyUpdater: (
+    key: string,
+    attributes: EdgeAttributes,
+    source: string,
+    target: string,
+    sourceAttributes: NodeAttributes,
+    targetAttributes: NodeAttributes,
+    undirected: boolean
+  ) => unknown
+): Graph;
diff --git a/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.js b/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.js
new file mode 100644
index 0000000000000000000000000000000000000000..5850b0925a6a6b53d50308488681c0334ec84e55
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology-utils/update-graph-keys.js
@@ -0,0 +1,69 @@
+/**
+ * Graphology Update Graph Keys
+ * =============================
+ *
+ * Helpers allowing you to update keys of nodes & edges .
+ */
+var copyEdge = require('./add-edge.js').copyEdge;
+
+module.exports = function updateGraphKeys(
+  graph,
+  nodeKeyUpdater,
+  edgeKeyUpdater
+) {
+  var renamed = graph.nullCopy();
+
+  // Renaming nodes
+  graph.forEachNode(function (key, attr) {
+    var renamedKey = nodeKeyUpdater ? nodeKeyUpdater(key, attr) : key;
+    renamed.addNode(renamedKey, attr);
+  });
+
+  // Renaming edges
+  var currentSource, currentSourceRenamed;
+
+  graph.forEachAssymetricAdjacencyEntry(function (
+    source,
+    target,
+    sourceAttr,
+    targetAttr,
+    key,
+    attr,
+    undirected
+  ) {
+    // Leveraging the ordered adjacency to save calls
+    if (source !== currentSource) {
+      currentSource = source;
+      currentSourceRenamed = nodeKeyUpdater
+        ? nodeKeyUpdater(source, sourceAttr)
+        : source;
+    }
+
+    var targetRenamed = nodeKeyUpdater
+      ? nodeKeyUpdater(target, targetAttr)
+      : target;
+
+    var renamedKey = edgeKeyUpdater
+      ? edgeKeyUpdater(
+          key,
+          attr,
+          source,
+          target,
+          sourceAttr,
+          targetAttr,
+          undirected
+        )
+      : key;
+
+    copyEdge(
+      renamed,
+      undirected,
+      renamedKey,
+      currentSourceRenamed,
+      targetRenamed,
+      attr
+    );
+  });
+
+  return renamed;
+};
diff --git a/libs/shared/graph-layout/node_modules/graphology/LICENSE.txt b/libs/shared/graph-layout/node_modules/graphology/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/graphology/README.md b/libs/shared/graph-layout/node_modules/graphology/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..cb1dd44524f0dc851423a35ffaeb09e4c5712936
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/README.md
@@ -0,0 +1,9 @@
+# Graphology
+
+`graphology` is a specification for a robust & multipurpose JavaScript `Graph` object and aiming at supporting various kinds of graphs under a same unified interface.
+
+You will also find here the source for the reference implementation of this specification.
+
+## Documentation
+
+Full documentation for the library/specs is available [here](https://graphology.github.io).
diff --git a/libs/shared/graph-layout/node_modules/graphology/dist/graphology.cjs.js b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.cjs.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bd8ac9c7830be84464ffb288f5e5deba8119d69
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.cjs.js
@@ -0,0 +1,5463 @@
+'use strict';
+
+var events = require('events');
+var Iterator = require('obliterator/iterator');
+var take = require('obliterator/take');
+var chain = require('obliterator/chain');
+
+function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
+
+var Iterator__default = /*#__PURE__*/_interopDefaultLegacy(Iterator);
+var take__default = /*#__PURE__*/_interopDefaultLegacy(take);
+var chain__default = /*#__PURE__*/_interopDefaultLegacy(chain);
+
+function _typeof(obj) {
+  "@babel/helpers - typeof";
+
+  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+    return typeof obj;
+  } : function (obj) {
+    return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+  }, _typeof(obj);
+}
+
+function _inheritsLoose(subClass, superClass) {
+  subClass.prototype = Object.create(superClass.prototype);
+  subClass.prototype.constructor = subClass;
+
+  _setPrototypeOf(subClass, superClass);
+}
+
+function _getPrototypeOf(o) {
+  _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
+    return o.__proto__ || Object.getPrototypeOf(o);
+  };
+  return _getPrototypeOf(o);
+}
+
+function _setPrototypeOf(o, p) {
+  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
+    o.__proto__ = p;
+    return o;
+  };
+
+  return _setPrototypeOf(o, p);
+}
+
+function _isNativeReflectConstruct() {
+  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
+  if (Reflect.construct.sham) return false;
+  if (typeof Proxy === "function") return true;
+
+  try {
+    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function _construct(Parent, args, Class) {
+  if (_isNativeReflectConstruct()) {
+    _construct = Reflect.construct;
+  } else {
+    _construct = function _construct(Parent, args, Class) {
+      var a = [null];
+      a.push.apply(a, args);
+      var Constructor = Function.bind.apply(Parent, a);
+      var instance = new Constructor();
+      if (Class) _setPrototypeOf(instance, Class.prototype);
+      return instance;
+    };
+  }
+
+  return _construct.apply(null, arguments);
+}
+
+function _isNativeFunction(fn) {
+  return Function.toString.call(fn).indexOf("[native code]") !== -1;
+}
+
+function _wrapNativeSuper(Class) {
+  var _cache = typeof Map === "function" ? new Map() : undefined;
+
+  _wrapNativeSuper = function _wrapNativeSuper(Class) {
+    if (Class === null || !_isNativeFunction(Class)) return Class;
+
+    if (typeof Class !== "function") {
+      throw new TypeError("Super expression must either be null or a function");
+    }
+
+    if (typeof _cache !== "undefined") {
+      if (_cache.has(Class)) return _cache.get(Class);
+
+      _cache.set(Class, Wrapper);
+    }
+
+    function Wrapper() {
+      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
+    }
+
+    Wrapper.prototype = Object.create(Class.prototype, {
+      constructor: {
+        value: Wrapper,
+        enumerable: false,
+        writable: true,
+        configurable: true
+      }
+    });
+    return _setPrototypeOf(Wrapper, Class);
+  };
+
+  return _wrapNativeSuper(Class);
+}
+
+function _assertThisInitialized(self) {
+  if (self === void 0) {
+    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+  }
+
+  return self;
+}
+
+/**
+ * Graphology Utilities
+ * =====================
+ *
+ * Collection of helpful functions used by the implementation.
+ */
+
+/**
+ * Object.assign-like polyfill.
+ *
+ * @param  {object} target       - First object.
+ * @param  {object} [...objects] - Objects to merge.
+ * @return {object}
+ */
+function assignPolyfill() {
+  var target = arguments[0];
+
+  for (var i = 1, l = arguments.length; i < l; i++) {
+    if (!arguments[i]) continue;
+
+    for (var k in arguments[i]) {
+      target[k] = arguments[i][k];
+    }
+  }
+
+  return target;
+}
+
+var assign = assignPolyfill;
+if (typeof Object.assign === 'function') assign = Object.assign;
+/**
+ * Function returning the first matching edge for given path.
+ * Note: this function does not check the existence of source & target. This
+ * must be performed by the caller.
+ *
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @param  {any}    target - Target node.
+ * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+ * @return {string|null}
+ */
+
+function getMatchingEdge(graph, source, target, type) {
+  var sourceData = graph._nodes.get(source);
+
+  var edge = null;
+  if (!sourceData) return edge;
+
+  if (type === 'mixed') {
+    edge = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target];
+  } else if (type === 'directed') {
+    edge = sourceData.out && sourceData.out[target];
+  } else {
+    edge = sourceData.undirected && sourceData.undirected[target];
+  }
+
+  return edge;
+}
+/**
+ * Checks whether the given value is a Graph implementation instance.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+
+function isGraph(value) {
+  return value !== null && _typeof(value) === 'object' && typeof value.addUndirectedEdgeWithKey === 'function' && typeof value.dropNode === 'function';
+}
+/**
+ * Checks whether the given value is a plain object.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+
+function isPlainObject(value) {
+  return _typeof(value) === 'object' && value !== null && value.constructor === Object;
+}
+/**
+ * Checks whether the given object is empty.
+ *
+ * @param  {object}  o - Target Object.
+ * @return {boolean}
+ */
+
+function isEmpty(o) {
+  var k;
+
+  for (k in o) {
+    return false;
+  }
+
+  return true;
+}
+/**
+ * Creates a "private" property for the given member name by concealing it
+ * using the `enumerable` option.
+ *
+ * @param {object} target - Target object.
+ * @param {string} name   - Member name.
+ */
+
+function privateProperty(target, name, value) {
+  Object.defineProperty(target, name, {
+    enumerable: false,
+    configurable: false,
+    writable: true,
+    value: value
+  });
+}
+/**
+ * Creates a read-only property for the given member name & the given getter.
+ *
+ * @param {object}   target - Target object.
+ * @param {string}   name   - Member name.
+ * @param {mixed}    value  - The attached getter or fixed value.
+ */
+
+function readOnlyProperty(target, name, value) {
+  var descriptor = {
+    enumerable: true,
+    configurable: true
+  };
+
+  if (typeof value === 'function') {
+    descriptor.get = value;
+  } else {
+    descriptor.value = value;
+    descriptor.writable = false;
+  }
+
+  Object.defineProperty(target, name, descriptor);
+}
+/**
+ * Returns whether the given object constitute valid hints.
+ *
+ * @param {object} hints - Target object.
+ */
+
+function validateHints(hints) {
+  if (!isPlainObject(hints)) return false;
+  if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+  return true;
+}
+/**
+ * Creates a function generating incremental ids for edges.
+ *
+ * @return {function}
+ */
+
+function incrementalIdStartingFromRandomByte() {
+  var i = Math.floor(Math.random() * 256) & 0xff;
+  return function () {
+    return i++;
+  };
+}
+
+/**
+ * Graphology Custom Errors
+ * =========================
+ *
+ * Defining custom errors for ease of use & easy unit tests across
+ * implementations (normalized typology rather than relying on error
+ * messages to check whether the correct error was found).
+ */
+var GraphError = /*#__PURE__*/function (_Error) {
+  _inheritsLoose(GraphError, _Error);
+
+  function GraphError(message) {
+    var _this;
+
+    _this = _Error.call(this) || this;
+    _this.name = 'GraphError';
+    _this.message = message;
+    return _this;
+  }
+
+  return GraphError;
+}( /*#__PURE__*/_wrapNativeSuper(Error));
+var InvalidArgumentsGraphError = /*#__PURE__*/function (_GraphError) {
+  _inheritsLoose(InvalidArgumentsGraphError, _GraphError);
+
+  function InvalidArgumentsGraphError(message) {
+    var _this2;
+
+    _this2 = _GraphError.call(this, message) || this;
+    _this2.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this2), InvalidArgumentsGraphError.prototype.constructor);
+    return _this2;
+  }
+
+  return InvalidArgumentsGraphError;
+}(GraphError);
+var NotFoundGraphError = /*#__PURE__*/function (_GraphError2) {
+  _inheritsLoose(NotFoundGraphError, _GraphError2);
+
+  function NotFoundGraphError(message) {
+    var _this3;
+
+    _this3 = _GraphError2.call(this, message) || this;
+    _this3.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this3), NotFoundGraphError.prototype.constructor);
+    return _this3;
+  }
+
+  return NotFoundGraphError;
+}(GraphError);
+var UsageGraphError = /*#__PURE__*/function (_GraphError3) {
+  _inheritsLoose(UsageGraphError, _GraphError3);
+
+  function UsageGraphError(message) {
+    var _this4;
+
+    _this4 = _GraphError3.call(this, message) || this;
+    _this4.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this4), UsageGraphError.prototype.constructor);
+    return _this4;
+  }
+
+  return UsageGraphError;
+}(GraphError);
+
+/**
+ * Graphology Internal Data Classes
+ * =================================
+ *
+ * Internal classes hopefully reduced to structs by engines & storing
+ * necessary information for nodes & edges.
+ *
+ * Note that those classes don't rely on the `class` keyword to avoid some
+ * cruft introduced by most of ES2015 transpilers.
+ */
+
+/**
+ * MixedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function MixedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+MixedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+  this.undirectedDegree = 0; // Indices
+
+  this["in"] = {};
+  this.out = {};
+  this.undirected = {};
+};
+/**
+ * DirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+
+
+function DirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+DirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0; // Indices
+
+  this["in"] = {};
+  this.out = {};
+};
+/**
+ * UndirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+
+
+function UndirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+UndirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.undirectedDegree = 0; // Indices
+
+  this.undirected = {};
+};
+/**
+ * EdgeData class.
+ *
+ * @constructor
+ * @param {boolean} undirected   - Whether the edge is undirected.
+ * @param {string}  string       - The edge's key.
+ * @param {string}  source       - Source of the edge.
+ * @param {string}  target       - Target of the edge.
+ * @param {object}  attributes   - Edge's attributes.
+ */
+
+
+function EdgeData(undirected, key, source, target, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.undirected = undirected; // Extremities
+
+  this.source = source;
+  this.target = target;
+}
+
+EdgeData.prototype.attach = function () {
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected';
+  var source = this.source.key;
+  var target = this.target.key; // Handling source
+
+  this.source[outKey][target] = this;
+  if (this.undirected && source === target) return; // Handling target
+
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.attachMulti = function () {
+  var outKey = 'out';
+  var inKey = 'in';
+  var source = this.source.key;
+  var target = this.target.key;
+  if (this.undirected) outKey = inKey = 'undirected'; // Handling source
+
+  var adj = this.source[outKey];
+  var head = adj[target];
+
+  if (typeof head === 'undefined') {
+    adj[target] = this; // Self-loop optimization
+
+    if (!(this.undirected && source === target)) {
+      // Handling target
+      this.target[inKey][source] = this;
+    }
+
+    return;
+  } // Prepending to doubly-linked list
+
+
+  head.previous = this;
+  this.next = head; // Pointing to new head
+  // NOTE: use mutating swap later to avoid lookup?
+
+  adj[target] = this;
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.detach = function () {
+  var source = this.source.key;
+  var target = this.target.key;
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected';
+  delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+  delete this.target[inKey][source];
+};
+
+EdgeData.prototype.detachMulti = function () {
+  var source = this.source.key;
+  var target = this.target.key;
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list
+
+  if (this.previous === undefined) {
+    // We are dealing with the head
+    // Should we delete the adjacency entry because it is now empty?
+    if (this.next === undefined) {
+      delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+      delete this.target[inKey][source];
+    } else {
+      // Detaching
+      this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method
+
+      this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop
+
+      this.target[inKey][source] = this.next;
+    }
+  } else {
+    // We are dealing with another list node
+    this.previous.next = this.next; // If not last
+
+    if (this.next !== undefined) {
+      this.next.previous = this.previous;
+    }
+  }
+};
+
+/**
+ * Graphology Node Attributes methods
+ * ===================================
+ */
+var NODE = 0;
+var SOURCE = 1;
+var TARGET = 2;
+var OPPOSITE = 3;
+
+function findRelevantNodeData(graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2) {
+  var nodeData, edgeData, arg1, arg2;
+  nodeOrEdge = '' + nodeOrEdge;
+
+  if (mode === NODE) {
+    nodeData = graph._nodes.get(nodeOrEdge);
+    if (!nodeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" node in the graph."));
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  } else if (mode === OPPOSITE) {
+    nameOrEdge = '' + nameOrEdge;
+    edgeData = graph._edges.get(nameOrEdge);
+    if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nameOrEdge, "\" edge in the graph."));
+    var source = edgeData.source.key;
+    var target = edgeData.target.key;
+
+    if (nodeOrEdge === source) {
+      nodeData = edgeData.target;
+    } else if (nodeOrEdge === target) {
+      nodeData = edgeData.source;
+    } else {
+      throw new NotFoundGraphError("Graph.".concat(method, ": the \"").concat(nodeOrEdge, "\" node is not attached to the \"").concat(nameOrEdge, "\" edge (").concat(source, ", ").concat(target, ")."));
+    }
+
+    arg1 = add1;
+    arg2 = add2;
+  } else {
+    edgeData = graph._edges.get(nodeOrEdge);
+    if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" edge in the graph."));
+
+    if (mode === SOURCE) {
+      nodeData = edgeData.source;
+    } else {
+      nodeData = edgeData.target;
+    }
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  }
+
+  return [nodeData, arg1, arg2];
+}
+
+function attachNodeAttributeGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData[0],
+        name = _findRelevantNodeData[1];
+
+    return data.attributes[name];
+  };
+}
+
+function attachNodeAttributesGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+    var _findRelevantNodeData2 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge),
+        data = _findRelevantNodeData2[0];
+
+    return data.attributes;
+  };
+}
+
+function attachNodeAttributeChecker(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData3 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData3[0],
+        name = _findRelevantNodeData3[1];
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+function attachNodeAttributeSetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    var _findRelevantNodeData4 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+        data = _findRelevantNodeData4[0],
+        name = _findRelevantNodeData4[1],
+        value = _findRelevantNodeData4[2];
+
+    data.attributes[name] = value; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributeUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    var _findRelevantNodeData5 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+        data = _findRelevantNodeData5[0],
+        name = _findRelevantNodeData5[1],
+        updater = _findRelevantNodeData5[2];
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+    var attributes = data.attributes;
+    var value = updater(attributes[name]);
+    attributes[name] = value; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributeRemover(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData6 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData6[0],
+        name = _findRelevantNodeData6[1];
+
+    delete data.attributes[name]; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesReplacer(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData7 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData7[0],
+        attributes = _findRelevantNodeData7[1];
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    data.attributes = attributes; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesMerger(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData8 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData8[0],
+        attributes = _findRelevantNodeData8[1];
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    assign(data.attributes, attributes); // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData9 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData9[0],
+        updater = _findRelevantNodeData9[1];
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+    data.attributes = updater(data.attributes); // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * List of methods to attach.
+ */
+
+
+var NODE_ATTRIBUTES_METHODS = [{
+  name: function name(element) {
+    return "get".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeGetter
+}, {
+  name: function name(element) {
+    return "get".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesGetter
+}, {
+  name: function name(element) {
+    return "has".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeChecker
+}, {
+  name: function name(element) {
+    return "set".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeSetter
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeUpdater
+}, {
+  name: function name(element) {
+    return "remove".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeRemover
+}, {
+  name: function name(element) {
+    return "replace".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesReplacer
+}, {
+  name: function name(element) {
+    return "merge".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesMerger
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesUpdater
+}];
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+
+function attachNodeAttributesMethods(Graph) {
+  NODE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+    var name = _ref.name,
+        attacher = _ref.attacher;
+    // For nodes
+    attacher(Graph, name('Node'), NODE); // For sources
+
+    attacher(Graph, name('Source'), SOURCE); // For targets
+
+    attacher(Graph, name('Target'), TARGET); // For opposites
+
+    attacher(Graph, name('Opposite'), OPPOSITE);
+  });
+}
+
+/**
+ * Graphology Edge Attributes methods
+ * ===================================
+ */
+/**
+ * Attach an attribute getter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+function attachEdgeAttributeGetter(Class, method, type) {
+  /**
+   * Get the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {mixed}          - The attribute's value.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes[name];
+  };
+}
+/**
+ * Attach an attributes getter method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesGetter(Class, method, type) {
+  /**
+   * Retrieves all the target element's attributes.
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   *
+   * @return {object}          - The element's attributes.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 1) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + arguments[1];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes;
+  };
+}
+/**
+ * Attach an attribute checker method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeChecker(Class, method, type) {
+  /**
+   * Checks whether the desired attribute is set for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+/**
+ * Attach an attribute setter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeSetter(Class, method, type) {
+  /**
+   * Set the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, value) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 3) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      value = arguments[3];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    data.attributes[name] = value; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeUpdater(Class, method, type) {
+  /**
+   * Update the desired attribute for the given element (node or edge) using
+   * the provided function.
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, updater) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 3) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      updater = arguments[3];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+    data.attributes[name] = updater(data.attributes[name]); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute remover method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeRemover(Class, method, type) {
+  /**
+   * Remove the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    delete data.attributes[name]; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute replacer method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesReplacer(Class, method, type) {
+  /**
+   * Replace the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - New attributes.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - New attributes.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + attributes;
+      attributes = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    data.attributes = attributes; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute merger method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesMerger(Class, method, type) {
+  /**
+   * Merge the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - Attributes to merge.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - Attributes to merge.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + attributes;
+      attributes = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    assign(data.attributes, attributes); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesUpdater(Class, method, type) {
+  /**
+   * Update the attributes of the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, updater) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + updater;
+      updater = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+    data.attributes = updater(data.attributes); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * List of methods to attach.
+ */
+
+
+var EDGE_ATTRIBUTES_METHODS = [{
+  name: function name(element) {
+    return "get".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeGetter
+}, {
+  name: function name(element) {
+    return "get".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesGetter
+}, {
+  name: function name(element) {
+    return "has".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeChecker
+}, {
+  name: function name(element) {
+    return "set".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeSetter
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeUpdater
+}, {
+  name: function name(element) {
+    return "remove".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeRemover
+}, {
+  name: function name(element) {
+    return "replace".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesReplacer
+}, {
+  name: function name(element) {
+    return "merge".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesMerger
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesUpdater
+}];
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+
+function attachEdgeAttributesMethods(Graph) {
+  EDGE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+    var name = _ref.name,
+        attacher = _ref.attacher;
+    // For edges
+    attacher(Graph, name('Edge'), 'mixed'); // For directed edges
+
+    attacher(Graph, name('DirectedEdge'), 'directed'); // For undirected edges
+
+    attacher(Graph, name('UndirectedEdge'), 'undirected');
+  });
+}
+
+/**
+ * Graphology Edge Iteration
+ * ==========================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's edges.
+ */
+/**
+ * Definitions.
+ */
+
+var EDGES_ITERATION = [{
+  name: 'edges',
+  type: 'mixed'
+}, {
+  name: 'inEdges',
+  type: 'directed',
+  direction: 'in'
+}, {
+  name: 'outEdges',
+  type: 'directed',
+  direction: 'out'
+}, {
+  name: 'inboundEdges',
+  type: 'mixed',
+  direction: 'in'
+}, {
+  name: 'outboundEdges',
+  type: 'mixed',
+  direction: 'out'
+}, {
+  name: 'directedEdges',
+  type: 'directed'
+}, {
+  name: 'undirectedEdges',
+  type: 'undirected'
+}];
+/**
+ * Function iterating over edges from the given object to match one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {function} callback - Function to call.
+ */
+
+function forEachSimple(breakable, object, callback, avoid) {
+  var shouldBreak = false;
+
+  for (var k in object) {
+    if (k === avoid) continue;
+    var edgeData = object[k];
+    shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+    if (breakable && shouldBreak) return edgeData.key;
+  }
+
+  return;
+}
+
+function forEachMulti(breakable, object, callback, avoid) {
+  var edgeData, source, target;
+  var shouldBreak = false;
+
+  for (var k in object) {
+    if (k === avoid) continue;
+    edgeData = object[k];
+
+    do {
+      source = edgeData.source;
+      target = edgeData.target;
+      shouldBreak = callback(edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+  }
+
+  return;
+}
+/**
+ * Function returning an iterator over edges from the given object.
+ *
+ * @param  {object}   object - Target object.
+ * @return {Iterator}
+ */
+
+
+function createIterator(object, avoid) {
+  var keys = Object.keys(object);
+  var l = keys.length;
+  var edgeData;
+  var i = 0;
+  return new Iterator__default["default"](function next() {
+    do {
+      if (!edgeData) {
+        if (i >= l) return {
+          done: true
+        };
+        var k = keys[i++];
+
+        if (k === avoid) {
+          edgeData = undefined;
+          continue;
+        }
+
+        edgeData = object[k];
+      } else {
+        edgeData = edgeData.next;
+      }
+    } while (!edgeData);
+
+    return {
+      done: false,
+      value: {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      }
+    };
+  });
+}
+/**
+ * Function iterating over the egdes from the object at given key to match
+ * one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {mixed}    k        - Neighbor key.
+ * @param {function} callback - Callback to use.
+ */
+
+
+function forEachForKeySimple(breakable, object, k, callback) {
+  var edgeData = object[k];
+  if (!edgeData) return;
+  var sourceData = edgeData.source;
+  var targetData = edgeData.target;
+  if (callback(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected) && breakable) return edgeData.key;
+}
+
+function forEachForKeyMulti(breakable, object, k, callback) {
+  var edgeData = object[k];
+  if (!edgeData) return;
+  var shouldBreak = false;
+
+  do {
+    shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+    if (breakable && shouldBreak) return edgeData.key;
+    edgeData = edgeData.next;
+  } while (edgeData !== undefined);
+
+  return;
+}
+/**
+ * Function returning an iterator over the egdes from the object at given key.
+ *
+ * @param  {object}   object   - Target object.
+ * @param  {mixed}    k        - Neighbor key.
+ * @return {Iterator}
+ */
+
+
+function createIteratorForKey(object, k) {
+  var edgeData = object[k];
+
+  if (edgeData.next !== undefined) {
+    return new Iterator__default["default"](function () {
+      if (!edgeData) return {
+        done: true
+      };
+      var value = {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      };
+      edgeData = edgeData.next;
+      return {
+        done: false,
+        value: value
+      };
+    });
+  }
+
+  return Iterator__default["default"].of({
+    edge: edgeData.key,
+    attributes: edgeData.attributes,
+    source: edgeData.source.key,
+    target: edgeData.target.key,
+    sourceAttributes: edgeData.source.attributes,
+    targetAttributes: edgeData.target.attributes,
+    undirected: edgeData.undirected
+  });
+}
+/**
+ * Function creating an array of edges for the given type.
+ *
+ * @param  {Graph}   graph - Target Graph instance.
+ * @param  {string}  type  - Type of edges to retrieve.
+ * @return {array}         - Array of edges.
+ */
+
+
+function createEdgeArray(graph, type) {
+  if (graph.size === 0) return [];
+
+  if (type === 'mixed' || type === graph.type) {
+    if (typeof Array.from === 'function') return Array.from(graph._edges.keys());
+    return take__default["default"](graph._edges.keys(), graph._edges.size);
+  }
+
+  var size = type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+  var list = new Array(size),
+      mask = type === 'undirected';
+
+  var iterator = graph._edges.values();
+
+  var i = 0;
+  var step, data;
+
+  while (step = iterator.next(), step.done !== true) {
+    data = step.value;
+    if (data.undirected === mask) list[i++] = data.key;
+  }
+
+  return list;
+}
+/**
+ * Function iterating over a graph's edges using a callback to match one of
+ * them.
+ *
+ * @param  {Graph}    graph    - Target Graph instance.
+ * @param  {string}   type     - Type of edges to retrieve.
+ * @param  {function} callback - Function to call.
+ */
+
+
+function forEachEdge(breakable, graph, type, callback) {
+  if (graph.size === 0) return;
+  var shouldFilter = type !== 'mixed' && type !== graph.type;
+  var mask = type === 'undirected';
+  var step, data;
+  var shouldBreak = false;
+
+  var iterator = graph._edges.values();
+
+  while (step = iterator.next(), step.done !== true) {
+    data = step.value;
+    if (shouldFilter && data.undirected !== mask) continue;
+    var _data = data,
+        key = _data.key,
+        attributes = _data.attributes,
+        source = _data.source,
+        target = _data.target;
+    shouldBreak = callback(key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected);
+    if (breakable && shouldBreak) return key;
+  }
+
+  return;
+}
+/**
+ * Function creating an iterator of edges for the given type.
+ *
+ * @param  {Graph}    graph - Target Graph instance.
+ * @param  {string}   type  - Type of edges to retrieve.
+ * @return {Iterator}
+ */
+
+
+function createEdgeIterator(graph, type) {
+  if (graph.size === 0) return Iterator__default["default"].empty();
+  var shouldFilter = type !== 'mixed' && type !== graph.type;
+  var mask = type === 'undirected';
+
+  var iterator = graph._edges.values();
+
+  return new Iterator__default["default"](function next() {
+    var step, data; // eslint-disable-next-line no-constant-condition
+
+    while (true) {
+      step = iterator.next();
+      if (step.done) return step;
+      data = step.value;
+      if (shouldFilter && data.undirected !== mask) continue;
+      break;
+    }
+
+    var value = {
+      edge: data.key,
+      attributes: data.attributes,
+      source: data.source.key,
+      target: data.target.key,
+      sourceAttributes: data.source.attributes,
+      targetAttributes: data.target.attributes,
+      undirected: data.undirected
+    };
+    return {
+      value: value,
+      done: false
+    };
+  });
+}
+/**
+ * Function iterating over a node's edges using a callback to match one of them.
+ *
+ * @param  {boolean}  multi     - Whether the graph is multi or not.
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Function to call.
+ */
+
+
+function forEachEdgeForNode(breakable, multi, type, direction, nodeData, callback) {
+  var fn = multi ? forEachMulti : forEachSimple;
+  var found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = fn(breakable, nodeData["in"], callback);
+      if (breakable && found) return found;
+    }
+
+    if (direction !== 'in') {
+      found = fn(breakable, nodeData.out, callback, !direction ? nodeData.key : undefined);
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    found = fn(breakable, nodeData.undirected, callback);
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+/**
+ * Function creating an array of edges for the given type & the given node.
+ *
+ * @param  {boolean} multi     - Whether the graph is multi or not.
+ * @param  {string}  type      - Type of edges to retrieve.
+ * @param  {string}  direction - In or out?
+ * @param  {any}     nodeData  - Target node's data.
+ * @return {array}             - Array of edges.
+ */
+
+
+function createEdgeArrayForNode(multi, type, direction, nodeData) {
+  var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+    edges.push(key);
+  });
+  return edges;
+}
+/**
+ * Function iterating over a node's edges using a callback.
+ *
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+
+
+function createEdgeIteratorForNode(type, direction, nodeData) {
+  var iterator = Iterator__default["default"].empty();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out' && typeof nodeData["in"] !== 'undefined') iterator = chain__default["default"](iterator, createIterator(nodeData["in"]));
+    if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator = chain__default["default"](iterator, createIterator(nodeData.out, !direction ? nodeData.key : undefined));
+  }
+
+  if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+    iterator = chain__default["default"](iterator, createIterator(nodeData.undirected));
+  }
+
+  return iterator;
+}
+/**
+ * Function iterating over edges for the given path using a callback to match
+ * one of them.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+
+
+function forEachEdgeForPath(breakable, type, multi, direction, sourceData, target, callback) {
+  var fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+  var found;
+
+  if (type !== 'undirected') {
+    if (typeof sourceData["in"] !== 'undefined' && direction !== 'out') {
+      found = fn(breakable, sourceData["in"], target, callback);
+      if (breakable && found) return found;
+    }
+
+    if (typeof sourceData.out !== 'undefined' && direction !== 'in' && (direction || sourceData.key !== target)) {
+      found = fn(breakable, sourceData.out, target, callback);
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined') {
+      found = fn(breakable, sourceData.undirected, target, callback);
+      if (breakable && found) return found;
+    }
+  }
+
+  return;
+}
+/**
+ * Function creating an array of edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {any}      target     - Target node.
+ * @return {array}               - Array of edges.
+ */
+
+
+function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+  var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForPath(false, type, multi, direction, sourceData, target, function (key) {
+    edges.push(key);
+  });
+  return edges;
+}
+/**
+ * Function returning an iterator over edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+
+
+function createEdgeIteratorForPath(type, direction, sourceData, target) {
+  var iterator = Iterator__default["default"].empty();
+
+  if (type !== 'undirected') {
+    if (typeof sourceData["in"] !== 'undefined' && direction !== 'out' && target in sourceData["in"]) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData["in"], target));
+    if (typeof sourceData.out !== 'undefined' && direction !== 'in' && target in sourceData.out && (direction || sourceData.key !== target)) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData.out, target));
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined' && target in sourceData.undirected) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData.undirected, target));
+  }
+
+  return iterator;
+}
+/**
+ * Function attaching an edge array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachEdgeArrayCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  /**
+   * Function returning an array of certain edges.
+   *
+   * Arity 0: Return all the relevant edges.
+   *
+   * Arity 1: Return all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Return the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+    if (!arguments.length) return createEdgeArray(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+      return createEdgeArrayForNode(this.multi, type === 'mixed' ? this.type : type, direction, nodeData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return createEdgeArrayForPath(type, this.multi, direction, sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+  };
+}
+/**
+ * Function attaching a edge callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachForEachEdge(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[forEachName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(false, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+
+      return forEachEdgeForNode(false, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return forEachEdgeForPath(false, type, this.multi, direction, sourceData, target, callback);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(forEachName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+  };
+  /**
+   * Function mapping the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Map all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Map all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Map the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    var result; // We know the result length beforehand
+
+    if (args.length === 0) {
+      var length = 0;
+      if (type !== 'directed') length += this.undirectedSize;
+      if (type !== 'undirected') length += this.directedSize;
+      result = new Array(length);
+      var i = 0;
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        result[i++] = callback(e, ea, s, t, sa, ta, u);
+      });
+    } // We don't know the result length beforehand
+    // TODO: we can in some instances of simple graphs, knowing degree
+    else {
+      result = [];
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        result.push(callback(e, ea, s, t, sa, ta, u));
+      });
+    }
+
+    this[forEachName].apply(this, args);
+    return result;
+  };
+  /**
+   * Function filtering the graph's relevant edges using the provided predicate
+   * function.
+   *
+   * Arity 1: Filter all the relevant edges.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 2: Filter all of a node's relevant edges.
+   * @param  {any}      node      - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 3: Filter the relevant edges across the given path.
+   * @param  {any}      source    - Source node.
+   * @param  {any}      target    - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    var result = [];
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+    });
+    this[forEachName].apply(this, args);
+    return result;
+  };
+  /**
+   * Function reducing the graph's relevant edges using the provided accumulator
+   * function.
+   *
+   * Arity 1: Reduce all the relevant edges.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 2: Reduce all of a node's relevant edges.
+   * @param  {any}      node         - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 3: Reduce the relevant edges across the given path.
+   * @param  {any}      source       - Source node.
+   * @param  {any}      target       - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+
+    if (args.length < 2 || args.length > 4) {
+      throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(args.length, ")."));
+    }
+
+    if (typeof args[args.length - 1] === 'function' && typeof args[args.length - 2] !== 'function') {
+      throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+    }
+
+    var callback;
+    var initialValue;
+
+    if (args.length === 2) {
+      callback = args[0];
+      initialValue = args[1];
+      args = [];
+    } else if (args.length === 3) {
+      callback = args[1];
+      initialValue = args[2];
+      args = [args[0]];
+    } else if (args.length === 4) {
+      callback = args[2];
+      initialValue = args[3];
+      args = [args[0], args[1]];
+    }
+
+    var accumulator = initialValue;
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+    });
+    this[forEachName].apply(this, args);
+    return accumulator;
+  };
+}
+/**
+ * Function attaching a breakable edge callback iterator method to the Graph
+ * prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachFindEdge(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over the graph's relevant edges in order to match
+   * one of them using the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[findEdgeName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return false;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(true, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findEdgeName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+
+      return forEachEdgeForNode(true, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return forEachEdgeForPath(true, type, this.multi, direction, sourceData, target, callback);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(findEdgeName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+  };
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether any one of them matches the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[someName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      return callback(e, ea, s, t, sa, ta, u);
+    });
+    var found = this[findEdgeName].apply(this, args);
+    if (found) return true;
+    return false;
+  };
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether all of them matche the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[everyName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      return !callback(e, ea, s, t, sa, ta, u);
+    });
+    var found = this[findEdgeName].apply(this, args);
+    if (found) return false;
+    return true;
+  };
+}
+/**
+ * Function attaching an edge iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachEdgeIteratorCreator(Class, description) {
+  var originalName = description.name,
+      type = description.type,
+      direction = description.direction;
+  var name = originalName.slice(0, -1) + 'Entries';
+  /**
+   * Function returning an iterator over the graph's edges.
+   *
+   * Arity 0: Iterate over all the relevant edges.
+   *
+   * Arity 1: Iterate over all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Iterate over the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator__default["default"].empty();
+    if (!arguments.length) return createEdgeIterator(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+      return createEdgeIteratorForNode(type, direction, sourceData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      var _sourceData = this._nodes.get(source);
+
+      if (!_sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return createEdgeIteratorForPath(type, direction, _sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+  };
+}
+/**
+ * Function attaching every edge iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+
+
+function attachEdgeIterationMethods(Graph) {
+  EDGES_ITERATION.forEach(function (description) {
+    attachEdgeArrayCreator(Graph, description);
+    attachForEachEdge(Graph, description);
+    attachFindEdge(Graph, description);
+    attachEdgeIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Neighbor Iteration
+ * ==============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over
+ * neighbors.
+ */
+/**
+ * Definitions.
+ */
+
+var NEIGHBORS_ITERATION = [{
+  name: 'neighbors',
+  type: 'mixed'
+}, {
+  name: 'inNeighbors',
+  type: 'directed',
+  direction: 'in'
+}, {
+  name: 'outNeighbors',
+  type: 'directed',
+  direction: 'out'
+}, {
+  name: 'inboundNeighbors',
+  type: 'mixed',
+  direction: 'in'
+}, {
+  name: 'outboundNeighbors',
+  type: 'mixed',
+  direction: 'out'
+}, {
+  name: 'directedNeighbors',
+  type: 'directed'
+}, {
+  name: 'undirectedNeighbors',
+  type: 'undirected'
+}];
+/**
+ * Helpers.
+ */
+
+function CompositeSetWrapper() {
+  this.A = null;
+  this.B = null;
+}
+
+CompositeSetWrapper.prototype.wrap = function (set) {
+  if (this.A === null) this.A = set;else if (this.B === null) this.B = set;
+};
+
+CompositeSetWrapper.prototype.has = function (key) {
+  if (this.A !== null && key in this.A) return true;
+  if (this.B !== null && key in this.B) return true;
+  return false;
+};
+/**
+ * Function iterating over the given node's relevant neighbors to match
+ * one of them using a predicated function.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Callback to use.
+ */
+
+
+function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+  for (var k in object) {
+    var edgeData = object[k];
+    var sourceData = edgeData.source;
+    var targetData = edgeData.target;
+    var neighborData = sourceData === nodeData ? targetData : sourceData;
+    if (visited && visited.has(neighborData.key)) continue;
+    var shouldBreak = callback(neighborData.key, neighborData.attributes);
+    if (breakable && shouldBreak) return neighborData.key;
+  }
+
+  return;
+}
+
+function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return forEachInObjectOnce(breakable, null, nodeData, nodeData.undirected, callback);
+    if (typeof direction === 'string') return forEachInObjectOnce(breakable, null, nodeData, nodeData[direction], callback);
+  } // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+
+
+  var visited = new CompositeSetWrapper();
+  var found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = forEachInObjectOnce(breakable, null, nodeData, nodeData["in"], callback);
+      if (breakable && found) return found;
+      visited.wrap(nodeData["in"]);
+    }
+
+    if (direction !== 'in') {
+      found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.out, callback);
+      if (breakable && found) return found;
+      visited.wrap(nodeData.out);
+    }
+  }
+
+  if (type !== 'directed') {
+    found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.undirected, callback);
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+/**
+ * Function creating an array of relevant neighbors for the given node.
+ *
+ * @param  {string}       type      - Type of neighbors.
+ * @param  {string}       direction - Direction.
+ * @param  {any}          nodeData  - Target node's data.
+ * @return {Array}                  - The list of neighbors.
+ */
+
+
+function createNeighborArrayForNode(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return Object.keys(nodeData.undirected);
+    if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+  }
+
+  var neighbors = [];
+  forEachNeighbor(false, type, direction, nodeData, function (key) {
+    neighbors.push(key);
+  });
+  return neighbors;
+}
+/**
+ * Function returning an iterator over the given node's relevant neighbors.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+
+
+function createDedupedObjectIterator(visited, nodeData, object) {
+  var keys = Object.keys(object);
+  var l = keys.length;
+  var i = 0;
+  return new Iterator__default["default"](function next() {
+    var neighborData = null;
+
+    do {
+      if (i >= l) {
+        if (visited) visited.wrap(object);
+        return {
+          done: true
+        };
+      }
+
+      var edgeData = object[keys[i++]];
+      var sourceData = edgeData.source;
+      var targetData = edgeData.target;
+      neighborData = sourceData === nodeData ? targetData : sourceData;
+
+      if (visited && visited.has(neighborData.key)) {
+        neighborData = null;
+        continue;
+      }
+    } while (neighborData === null);
+
+    return {
+      done: false,
+      value: {
+        neighbor: neighborData.key,
+        attributes: neighborData.attributes
+      }
+    };
+  });
+}
+
+function createNeighborIterator(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+    if (typeof direction === 'string') return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+  }
+
+  var iterator = Iterator__default["default"].empty(); // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+
+  var visited = new CompositeSetWrapper();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData["in"]));
+    }
+
+    if (direction !== 'in') {
+      iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData.out));
+    }
+  }
+
+  if (type !== 'directed') {
+    iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData.undirected));
+  }
+
+  return iterator;
+}
+/**
+ * Function attaching a neighbors array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachNeighborArrayCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  /**
+   * Function returning an array of certain neighbors.
+   *
+   * @param  {any}   node   - Target node.
+   * @return {array} - The neighbors of neighbors.
+   *
+   * @throws {Error} - Will throw if node is not found in the graph.
+   */
+
+  Class.prototype[name] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return createNeighborArrayForNode(type === 'mixed' ? this.type : type, direction, nodeData);
+  };
+}
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachForEachNeighbor(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[forEachName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    forEachNeighbor(false, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+  };
+  /**
+   * Function mapping the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function (node, callback) {
+    // TODO: optimize when size is known beforehand
+    var result = [];
+    this[forEachName](node, function (n, a) {
+      result.push(callback(n, a));
+    });
+    return result;
+  };
+  /**
+   * Function filtering the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function (node, callback) {
+    var result = [];
+    this[forEachName](node, function (n, a) {
+      if (callback(n, a)) result.push(n);
+    });
+    return result;
+  };
+  /**
+   * Function reducing the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function (node, callback, initialValue) {
+    if (arguments.length < 3) throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+    var accumulator = initialValue;
+    this[forEachName](node, function (n, a) {
+      accumulator = callback(accumulator, n, a);
+    });
+    return accumulator;
+  };
+}
+/**
+ * Function attaching a breakable neighbors callback iterator method to the
+ * Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachFindNeighbor(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+  var findName = 'find' + capitalizedSingular;
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[findName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return forEachNeighbor(true, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+  };
+  /**
+   * Function iterating over all the relevant neighbors to find if any of them
+   * matches the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var someName = 'some' + capitalizedSingular;
+
+  Class.prototype[someName] = function (node, callback) {
+    var found = this[findName](node, callback);
+    if (found) return true;
+    return false;
+  };
+  /**
+   * Function iterating over all the relevant neighbors to find if all of them
+   * matche the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var everyName = 'every' + capitalizedSingular;
+
+  Class.prototype[everyName] = function (node, callback) {
+    var found = this[findName](node, function (n, a) {
+      return !callback(n, a);
+    });
+    if (found) return false;
+    return true;
+  };
+}
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachNeighborIteratorCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var iteratorName = name.slice(0, -1) + 'Entries';
+  /**
+   * Function returning an iterator over all the relevant neighbors.
+   *
+   * @param  {any}      node     - Target node.
+   * @return {Iterator}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[iteratorName] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator__default["default"].empty();
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(iteratorName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return createNeighborIterator(type === 'mixed' ? this.type : type, direction, nodeData);
+  };
+}
+/**
+ * Function attaching every neighbor iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+
+
+function attachNeighborIterationMethods(Graph) {
+  NEIGHBORS_ITERATION.forEach(function (description) {
+    attachNeighborArrayCreator(Graph, description);
+    attachForEachNeighbor(Graph, description);
+    attachFindNeighbor(Graph, description);
+    attachNeighborIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Adjacency Iteration
+ * ===============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's adjacency.
+ */
+
+/**
+ * Function iterating over a simple graph's adjacency using a callback.
+ *
+ * @param {boolean}  breakable         - Can we break?
+ * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+ * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+ * @param {Graph}    graph             - Target Graph instance.
+ * @param {callback} function          - Iteration callback.
+ */
+function forEachAdjacency(breakable, assymetric, disconnectedNodes, graph, callback) {
+  var iterator = graph._nodes.values();
+
+  var type = graph.type;
+  var step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+  while (step = iterator.next(), step.done !== true) {
+    var hasEdges = false;
+    sourceData = step.value;
+
+    if (type !== 'undirected') {
+      adj = sourceData.out;
+
+      for (neighbor in adj) {
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+          hasEdges = true;
+          shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+          if (breakable && shouldBreak) return edgeData;
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (type !== 'directed') {
+      adj = sourceData.undirected;
+
+      for (neighbor in adj) {
+        if (assymetric && sourceData.key > neighbor) continue;
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+          if (targetData.key !== neighbor) targetData = edgeData.source;
+          hasEdges = true;
+          shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+          if (breakable && shouldBreak) return edgeData;
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (disconnectedNodes && !hasEdges) {
+      shouldBreak = callback(sourceData.key, null, sourceData.attributes, null, null, null, null);
+      if (breakable && shouldBreak) return null;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Graphology Serialization Utilities
+ * ===================================
+ *
+ * Collection of functions used by the graph serialization schemes.
+ */
+/**
+ * Formats internal node data into a serialized node.
+ *
+ * @param  {any}    key  - The node's key.
+ * @param  {object} data - Internal node's data.
+ * @return {array}       - The serialized node.
+ */
+
+function serializeNode(key, data) {
+  var serialized = {
+    key: key
+  };
+  if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+  return serialized;
+}
+/**
+ * Formats internal edge data into a serialized edge.
+ *
+ * @param  {any}    key  - The edge's key.
+ * @param  {object} data - Internal edge's data.
+ * @return {array}       - The serialized edge.
+ */
+
+function serializeEdge(key, data) {
+  var serialized = {
+    key: key,
+    source: data.source.key,
+    target: data.target.key
+  };
+  if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+  if (data.undirected) serialized.undirected = true;
+  return serialized;
+}
+/**
+ * Checks whether the given value is a serialized node.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+
+function validateSerializedNode(value) {
+  if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');
+  if (!('key' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized node is missing its key.');
+  if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+}
+/**
+ * Checks whether the given value is a serialized edge.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+
+function validateSerializedEdge(value) {
+  if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');
+  if (!('source' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its source.');
+  if (!('target' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its target.');
+  if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+  if ('undirected' in value && typeof value.undirected !== 'boolean') throw new InvalidArgumentsGraphError('Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.');
+}
+
+/**
+ * Constants.
+ */
+
+var INSTANCE_ID = incrementalIdStartingFromRandomByte();
+/**
+ * Enums.
+ */
+
+var TYPES = new Set(['directed', 'undirected', 'mixed']);
+var EMITTER_PROPS = new Set(['domain', '_events', '_eventsCount', '_maxListeners']);
+var EDGE_ADD_METHODS = [{
+  name: function name(verb) {
+    return "".concat(verb, "Edge");
+  },
+  generateKey: true
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "DirectedEdge");
+  },
+  generateKey: true,
+  type: 'directed'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "UndirectedEdge");
+  },
+  generateKey: true,
+  type: 'undirected'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "EdgeWithKey");
+  }
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "DirectedEdgeWithKey");
+  },
+  type: 'directed'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "UndirectedEdgeWithKey");
+  },
+  type: 'undirected'
+}];
+/**
+ * Default options.
+ */
+
+var DEFAULTS = {
+  allowSelfLoops: true,
+  multi: false,
+  type: 'mixed'
+};
+/**
+ * Abstract functions used by the Graph class for various methods.
+ */
+
+/**
+ * Internal method used to add a node to the given graph
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {any}     node            - The node's key.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {NodeData}                - Created node data.
+ */
+
+function _addNode(graph, node, attributes) {
+  if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.addNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+  node = '' + node;
+  attributes = attributes || {};
+  if (graph._nodes.has(node)) throw new UsageGraphError("Graph.addNode: the \"".concat(node, "\" node already exist in the graph."));
+  var data = new graph.NodeDataClass(node, attributes); // Adding the node to internal register
+
+  graph._nodes.set(node, data); // Emitting
+
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes: attributes
+  });
+  return data;
+}
+/**
+ * Same as the above but without sanity checks because we call this in contexts
+ * where necessary checks were already done.
+ */
+
+
+function unsafeAddNode(graph, node, attributes) {
+  var data = new graph.NodeDataClass(node, attributes);
+
+  graph._nodes.set(node, data);
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes: attributes
+  });
+  return data;
+}
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+
+
+function addEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));
+  if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));
+  if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\"")); // Coercion of source & target:
+
+  source = '' + source;
+  target = '' + target;
+  attributes = attributes || {};
+  if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+  var sourceData = graph._nodes.get(source),
+      targetData = graph._nodes.get(target);
+
+  if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": source node \"").concat(source, "\" not found."));
+  if (!targetData) throw new NotFoundGraphError("Graph.".concat(name, ": target node \"").concat(target, "\" not found.")); // Must the graph generate an id for this edge?
+
+  var eventData = {
+    key: null,
+    undirected: undirected,
+    source: source,
+    target: target,
+    attributes: attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge; // Here, we have a key collision
+
+    if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+  } // Here, we might have a source / target collision
+
+
+  if (!graph.multi && (undirected ? typeof sourceData.undirected[target] !== 'undefined' : typeof sourceData.out[target] !== 'undefined')) {
+    throw new UsageGraphError("Graph.".concat(name, ": an edge linking \"").concat(source, "\" to \"").concat(target, "\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));
+  } // Storing some data
+
+
+  var edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+  graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+  var isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  } // Updating relevant index
+
+
+  if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+  if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+  eventData.key = edge;
+  graph.emit('edgeAdded', eventData);
+  return edge;
+}
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @param  {boolean} [asUpdater]       - Are we updating or merging?
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+
+
+function mergeEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes, asUpdater) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));
+  if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));
+
+  if (attributes) {
+    if (asUpdater) {
+      if (typeof attributes !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid updater function. Expecting a function but got \"").concat(attributes, "\""));
+    } else {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\""));
+    }
+  } // Coercion of source & target:
+
+
+  source = '' + source;
+  target = '' + target;
+  var updater;
+
+  if (asUpdater) {
+    updater = attributes;
+    attributes = undefined;
+  }
+
+  if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+  var sourceData = graph._nodes.get(source);
+
+  var targetData = graph._nodes.get(target);
+
+  var edgeData; // Do we need to handle duplicate?
+
+  var alreadyExistingEdgeData;
+
+  if (!mustGenerateKey) {
+    edgeData = graph._edges.get(edge);
+
+    if (edgeData) {
+      // Here, we need to ensure, if the user gave a key, that source & target
+      // are consistent
+      if (edgeData.source.key !== source || edgeData.target.key !== target) {
+        // If source or target inconsistent
+        if (!undirected || edgeData.source.key !== target || edgeData.target.key !== source) {
+          // If directed, or source/target aren't flipped
+          throw new UsageGraphError("Graph.".concat(name, ": inconsistency detected when attempting to merge the \"").concat(edge, "\" edge with \"").concat(source, "\" source & \"").concat(target, "\" target vs. (\"").concat(edgeData.source.key, "\", \"").concat(edgeData.target.key, "\")."));
+        }
+      }
+
+      alreadyExistingEdgeData = edgeData;
+    }
+  } // Here, we might have a source / target collision
+
+
+  if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+    alreadyExistingEdgeData = undirected ? sourceData.undirected[target] : sourceData.out[target];
+  } // Handling duplicates
+
+
+  if (alreadyExistingEdgeData) {
+    var info = [alreadyExistingEdgeData.key, false, false, false]; // We can skip the attribute merging part if the user did not provide them
+
+    if (asUpdater ? !updater : !attributes) return info; // Updating the attributes
+
+    if (asUpdater) {
+      var oldAttributes = alreadyExistingEdgeData.attributes;
+      alreadyExistingEdgeData.attributes = updater(oldAttributes);
+      graph.emit('edgeAttributesUpdated', {
+        type: 'replace',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes
+      });
+    } // Merging the attributes
+    else {
+      assign(alreadyExistingEdgeData.attributes, attributes);
+      graph.emit('edgeAttributesUpdated', {
+        type: 'merge',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes,
+        data: attributes
+      });
+    }
+
+    return info;
+  }
+
+  attributes = attributes || {};
+  if (asUpdater && updater) attributes = updater(attributes); // Must the graph generate an id for this edge?
+
+  var eventData = {
+    key: null,
+    undirected: undirected,
+    source: source,
+    target: target,
+    attributes: attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge; // Here, we have a key collision
+
+    if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+  }
+
+  var sourceWasAdded = false;
+  var targetWasAdded = false;
+
+  if (!sourceData) {
+    sourceData = unsafeAddNode(graph, source, {});
+    sourceWasAdded = true;
+
+    if (source === target) {
+      targetData = sourceData;
+      targetWasAdded = true;
+    }
+  }
+
+  if (!targetData) {
+    targetData = unsafeAddNode(graph, target, {});
+    targetWasAdded = true;
+  } // Storing some data
+
+
+  edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+  graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+  var isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  } // Updating relevant index
+
+
+  if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+  if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+  eventData.key = edge;
+  graph.emit('edgeAdded', eventData);
+  return [edge, true, sourceWasAdded, targetWasAdded];
+}
+/**
+ * Internal method used to drop an edge.
+ *
+ * @param  {Graph}    graph    - Target graph.
+ * @param  {EdgeData} edgeData - Data of the edge to drop.
+ */
+
+
+function dropEdgeFromData(graph, edgeData) {
+  // Dropping the edge from the register
+  graph._edges["delete"](edgeData.key); // Updating related degrees
+
+
+  var sourceData = edgeData.source,
+      targetData = edgeData.target,
+      attributes = edgeData.attributes;
+  var undirected = edgeData.undirected;
+  var isSelfLoop = sourceData === targetData;
+
+  if (undirected) {
+    sourceData.undirectedDegree--;
+    targetData.undirectedDegree--;
+    if (isSelfLoop) graph._undirectedSelfLoopCount--;
+  } else {
+    sourceData.outDegree--;
+    targetData.inDegree--;
+    if (isSelfLoop) graph._directedSelfLoopCount--;
+  } // Clearing index
+
+
+  if (graph.multi) edgeData.detachMulti();else edgeData.detach();
+  if (undirected) graph._undirectedSize--;else graph._directedSize--; // Emitting
+
+  graph.emit('edgeDropped', {
+    key: edgeData.key,
+    attributes: attributes,
+    source: sourceData.key,
+    target: targetData.key,
+    undirected: undirected
+  });
+}
+/**
+ * Graph class
+ *
+ * @constructor
+ * @param  {object}  [options] - Options:
+ * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+ * @param  {string}    [type]           - Type of the graph.
+ * @param  {boolean}   [map]            - Allow references as keys?
+ * @param  {boolean}   [multi]          - Allow parallel edges?
+ *
+ * @throws {Error} - Will throw if the arguments are not valid.
+ */
+
+
+var Graph = /*#__PURE__*/function (_EventEmitter) {
+  _inheritsLoose(Graph, _EventEmitter);
+
+  function Graph(options) {
+    var _this;
+
+    _this = _EventEmitter.call(this) || this; //-- Solving options
+
+    options = assign({}, DEFAULTS, options); // Enforcing options validity
+
+    if (typeof options.multi !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(options.multi, "\"."));
+    if (!TYPES.has(options.type)) throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'type' option. Should be one of \"mixed\", \"directed\" or \"undirected\" but got \"".concat(options.type, "\"."));
+    if (typeof options.allowSelfLoops !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(options.allowSelfLoops, "\".")); //-- Private properties
+    // Utilities
+
+    var NodeDataClass = options.type === 'mixed' ? MixedNodeData : options.type === 'directed' ? DirectedNodeData : UndirectedNodeData;
+    privateProperty(_assertThisInitialized(_this), 'NodeDataClass', NodeDataClass); // Internal edge key generator
+    // NOTE: this internal generator produce keys that are strings
+    // composed of a weird prefix, an incremental instance id starting from
+    // a random byte and finally an internal instance incremental id.
+    // All this to avoid intra-frame and cross-frame adversarial inputs
+    // that can force a single #.addEdge call to degenerate into a O(n)
+    // available key search loop.
+    // It also ensures that automatically generated edge keys are unlikely
+    // to produce collisions with arbitrary keys given by users.
+
+    var instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+    var edgeId = 0;
+
+    var edgeKeyGenerator = function edgeKeyGenerator() {
+      var availableEdgeKey;
+
+      do {
+        availableEdgeKey = instancePrefix + edgeId++;
+      } while (_this._edges.has(availableEdgeKey));
+
+      return availableEdgeKey;
+    }; // Indexes
+
+
+    privateProperty(_assertThisInitialized(_this), '_attributes', {});
+    privateProperty(_assertThisInitialized(_this), '_nodes', new Map());
+    privateProperty(_assertThisInitialized(_this), '_edges', new Map());
+    privateProperty(_assertThisInitialized(_this), '_directedSize', 0);
+    privateProperty(_assertThisInitialized(_this), '_undirectedSize', 0);
+    privateProperty(_assertThisInitialized(_this), '_directedSelfLoopCount', 0);
+    privateProperty(_assertThisInitialized(_this), '_undirectedSelfLoopCount', 0);
+    privateProperty(_assertThisInitialized(_this), '_edgeKeyGenerator', edgeKeyGenerator); // Options
+
+    privateProperty(_assertThisInitialized(_this), '_options', options); // Emitter properties
+
+    EMITTER_PROPS.forEach(function (prop) {
+      return privateProperty(_assertThisInitialized(_this), prop, _this[prop]);
+    }); //-- Properties readers
+
+    readOnlyProperty(_assertThisInitialized(_this), 'order', function () {
+      return _this._nodes.size;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'size', function () {
+      return _this._edges.size;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'directedSize', function () {
+      return _this._directedSize;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'undirectedSize', function () {
+      return _this._undirectedSize;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'selfLoopCount', function () {
+      return _this._directedSelfLoopCount + _this._undirectedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'directedSelfLoopCount', function () {
+      return _this._directedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'undirectedSelfLoopCount', function () {
+      return _this._undirectedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'multi', _this._options.multi);
+    readOnlyProperty(_assertThisInitialized(_this), 'type', _this._options.type);
+    readOnlyProperty(_assertThisInitialized(_this), 'allowSelfLoops', _this._options.allowSelfLoops);
+    readOnlyProperty(_assertThisInitialized(_this), 'implementation', function () {
+      return 'graphology';
+    });
+    return _this;
+  }
+
+  var _proto = Graph.prototype;
+
+  _proto._resetInstanceCounters = function _resetInstanceCounters() {
+    this._directedSize = 0;
+    this._undirectedSize = 0;
+    this._directedSelfLoopCount = 0;
+    this._undirectedSelfLoopCount = 0;
+  }
+  /**---------------------------------------------------------------------------
+   * Read
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning whether the given node is found in the graph.
+   *
+   * @param  {any}     node - The node.
+   * @return {boolean}
+   */
+  ;
+
+  _proto.hasNode = function hasNode(node) {
+    return this._nodes.has('' + node);
+  }
+  /**
+   * Method returning whether the given directed edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasDirectedEdge = function hasDirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'undirected') return false;
+
+    if (arguments.length === 1) {
+      var edge = '' + source;
+
+      var edgeData = this._edges.get(edge);
+
+      return !!edgeData && !edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = nodeData.out[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning whether the given undirected edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasUndirectedEdge = function hasUndirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'directed') return false;
+
+    if (arguments.length === 1) {
+      var edge = '' + source;
+
+      var edgeData = this._edges.get(edge);
+
+      return !!edgeData && edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = nodeData.undirected[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning whether the given edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasEdge = function hasEdge(source, target) {
+    if (arguments.length === 1) {
+      var edge = '' + source;
+      return this._edges.has(edge);
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+      if (!edges) edges = typeof nodeData.undirected !== 'undefined' && nodeData.undirected[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning the edge matching source & target in a directed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.directedEdge = function directedEdge(source, target) {
+    if (this.type === 'undirected') return;
+    source = '' + source;
+    target = '' + target;
+    if (this.multi) throw new UsageGraphError('Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.');
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.out && sourceData.out[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning the edge matching source & target in a undirected fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.undirectedEdge = function undirectedEdge(source, target) {
+    if (this.type === 'directed') return;
+    source = '' + source;
+    target = '' + target;
+    if (this.multi) throw new UsageGraphError('Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.');
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.undirected && sourceData.undirected[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning the edge matching source & target in a mixed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.edge = function edge(source, target) {
+    if (this.multi) throw new UsageGraphError('Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.');
+    source = '' + source;
+    target = '' + target;
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning whether two nodes are directed neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areDirectedNeighbors = function areDirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areDirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData["in"] || neighbor in nodeData.out;
+  }
+  /**
+   * Method returning whether two nodes are out neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areOutNeighbors = function areOutNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areOutNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData.out;
+  }
+  /**
+   * Method returning whether two nodes are in neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areInNeighbors = function areInNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areInNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData["in"];
+  }
+  /**
+   * Method returning whether two nodes are undirected neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areUndirectedNeighbors = function areUndirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areUndirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return false;
+    return neighbor in nodeData.undirected;
+  }
+  /**
+   * Method returning whether two nodes are neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areNeighbors = function areNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData["in"] || neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether two nodes are inbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areInboundNeighbors = function areInboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areInboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData["in"]) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether two nodes are outbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areOutboundNeighbors = function areOutboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areOutboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning the given node's in degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inDegree = function inDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.inDegree;
+  }
+  /**
+   * Method returning the given node's out degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outDegree = function outDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.outDegree;
+  }
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.directedDegree = function directedDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.directedDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.inDegree + nodeData.outDegree;
+  }
+  /**
+   * Method returning the given node's undirected degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.undirectedDegree = function undirectedDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return 0;
+    return nodeData.undirectedDegree;
+  }
+  /**
+   * Method returning the given node's inbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inboundDegree = function inboundDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's outbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outboundDegree = function outboundDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.degree = function degree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.degree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's in degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inDegreeWithoutSelfLoops = function inDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData["in"][node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.inDegree - loops;
+  }
+  /**
+   * Method returning the given node's out degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outDegreeWithoutSelfLoops = function outDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData.out[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.outDegree - loops;
+  }
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.directedDegreeWithoutSelfLoops = function directedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.directedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData.out[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.inDegree + nodeData.outDegree - loops * 2;
+  }
+  /**
+   * Method returning the given node's undirected degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.undirectedDegreeWithoutSelfLoops = function undirectedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return 0;
+    var self = nodeData.undirected[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.undirectedDegree - loops * 2;
+  }
+  /**
+   * Method returning the given node's inbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inboundDegreeWithoutSelfLoops = function inboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+      self = nodeData.out[node];
+      loops += self ? this.multi ? self.size : 1 : 0;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given node's outbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outboundDegreeWithoutSelfLoops = function outboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+      self = nodeData["in"][node];
+      loops += self ? this.multi ? self.size : 1 : 0;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.degreeWithoutSelfLoops = function degreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.degreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+      self = nodeData.out[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given edge's source.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's source.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.source = function source(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.source: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source.key;
+  }
+  /**
+   * Method returning the given edge's target.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's target.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.target = function target(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.target: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.target.key;
+  }
+  /**
+   * Method returning the given edge's extremities.
+   *
+   * @param  {any}   edge - The edge's key.
+   * @return {array}      - The edge's extremities.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.extremities = function extremities(edge) {
+    edge = '' + edge;
+
+    var edgeData = this._edges.get(edge);
+
+    if (!edgeData) throw new NotFoundGraphError("Graph.extremities: could not find the \"".concat(edge, "\" edge in the graph."));
+    return [edgeData.source.key, edgeData.target.key];
+  }
+  /**
+   * Given a node & an edge, returns the other extremity of the edge.
+   *
+   * @param  {any}   node - The node's key.
+   * @param  {any}   edge - The edge's key.
+   * @return {any}        - The related node.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph or if the
+   *                   edge & node are not related.
+   */
+  ;
+
+  _proto.opposite = function opposite(node, edge) {
+    node = '' + node;
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.opposite: could not find the \"".concat(edge, "\" edge in the graph."));
+    var source = data.source.key;
+    var target = data.target.key;
+    if (node === source) return target;
+    if (node === target) return source;
+    throw new NotFoundGraphError("Graph.opposite: the \"".concat(node, "\" node is not attached to the \"").concat(edge, "\" edge (").concat(source, ", ").concat(target, ")."));
+  }
+  /**
+   * Returns whether the given edge has the given node as extremity.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @param  {any}     node - The node's key.
+   * @return {boolean}      - The related node.
+   *
+   * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+   */
+  ;
+
+  _proto.hasExtremity = function hasExtremity(edge, node) {
+    edge = '' + edge;
+    node = '' + node;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.hasExtremity: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source.key === node || data.target.key === node;
+  }
+  /**
+   * Method returning whether the given edge is undirected.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isUndirected = function isUndirected(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isUndirected: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.undirected;
+  }
+  /**
+   * Method returning whether the given edge is directed.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isDirected = function isDirected(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isDirected: could not find the \"".concat(edge, "\" edge in the graph."));
+    return !data.undirected;
+  }
+  /**
+   * Method returning whether the given edge is a self loop.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isSelfLoop = function isSelfLoop(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isSelfLoop: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source === data.target;
+  }
+  /**---------------------------------------------------------------------------
+   * Mutation
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to add a node to the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   *
+   * @throws {Error} - Will throw if the given node already exist.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   */
+  ;
+
+  _proto.addNode = function addNode(node, attributes) {
+    var nodeData = _addNode(this, node, attributes);
+
+    return nodeData.key;
+  }
+  /**
+   * Method used to merge a node into the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   */
+  ;
+
+  _proto.mergeNode = function mergeNode(node, attributes) {
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.mergeNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+    node = '' + node;
+    attributes = attributes || {}; // If the node already exists, we merge the attributes
+
+    var data = this._nodes.get(node);
+
+    if (data) {
+      if (attributes) {
+        assign(data.attributes, attributes);
+        this.emit('nodeAttributesUpdated', {
+          type: 'merge',
+          key: node,
+          attributes: data.attributes,
+          data: attributes
+        });
+      }
+
+      return [node, false];
+    }
+
+    data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    this._nodes.set(node, data); // Emitting
+
+
+    this.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return [node, true];
+  }
+  /**
+   * Method used to add a node if it does not exist in the graph or else to
+   * update its attributes using a function.
+   *
+   * @param  {any}      node      - The node.
+   * @param  {function} [updater] - Optional updater function.
+   * @return {any}                - The node.
+   */
+  ;
+
+  _proto.updateNode = function updateNode(node, updater) {
+    if (updater && typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.updateNode: invalid updater function. Expecting a function but got \"".concat(updater, "\"")); // String coercion
+
+    node = '' + node; // If the node already exists, we update the attributes
+
+    var data = this._nodes.get(node);
+
+    if (data) {
+      if (updater) {
+        var oldAttributes = data.attributes;
+        data.attributes = updater(oldAttributes);
+        this.emit('nodeAttributesUpdated', {
+          type: 'replace',
+          key: node,
+          attributes: data.attributes
+        });
+      }
+
+      return [node, false];
+    }
+
+    var attributes = updater ? updater({}) : {};
+    data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    this._nodes.set(node, data); // Emitting
+
+
+    this.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return [node, true];
+  }
+  /**
+   * Method used to drop a single node & all its attached edges from the graph.
+   *
+   * @param  {any}    node - The node.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the node doesn't exist.
+   */
+  ;
+
+  _proto.dropNode = function dropNode(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.dropNode: could not find the \"".concat(node, "\" node in the graph."));
+    var edgeData; // Removing attached edges
+    // NOTE: we could be faster here, but this is such a pain to maintain
+
+    if (this.type !== 'undirected') {
+      for (var neighbor in nodeData.out) {
+        edgeData = nodeData.out[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+
+      for (var _neighbor in nodeData["in"]) {
+        edgeData = nodeData["in"][_neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (this.type !== 'directed') {
+      for (var _neighbor2 in nodeData.undirected) {
+        edgeData = nodeData.undirected[_neighbor2];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    } // Dropping the node from the register
+
+
+    this._nodes["delete"](node); // Emitting
+
+
+    this.emit('nodeDropped', {
+      key: node,
+      attributes: nodeData.attributes
+    });
+  }
+  /**
+   * Method used to drop a single edge from the graph.
+   *
+   * Arity 1:
+   * @param  {any}    edge - The edge.
+   *
+   * Arity 2:
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropEdge = function dropEdge(edge) {
+    var edgeData;
+
+    if (arguments.length > 1) {
+      var source = '' + arguments[0];
+      var target = '' + arguments[1];
+      edgeData = getMatchingEdge(this, source, target, this.type);
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    } else {
+      edge = '' + edge;
+      edgeData = this._edges.get(edge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(edge, "\" edge in the graph."));
+    }
+
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to drop a single directed edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropDirectedEdge = function dropDirectedEdge(source, target) {
+    if (arguments.length < 2) throw new UsageGraphError('Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+    if (this.multi) throw new UsageGraphError('Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+    source = '' + source;
+    target = '' + target;
+    var edgeData = getMatchingEdge(this, source, target, 'directed');
+    if (!edgeData) throw new NotFoundGraphError("Graph.dropDirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to drop a single undirected edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropUndirectedEdge = function dropUndirectedEdge(source, target) {
+    if (arguments.length < 2) throw new UsageGraphError('Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+    if (this.multi) throw new UsageGraphError('Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+    var edgeData = getMatchingEdge(this, source, target, 'undirected');
+    if (!edgeData) throw new NotFoundGraphError("Graph.dropUndirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to remove every edge & every node from the graph.
+   *
+   * @return {Graph}
+   */
+  ;
+
+  _proto.clear = function clear() {
+    // Clearing edges
+    this._edges.clear(); // Clearing nodes
+
+
+    this._nodes.clear(); // Reset counters
+
+
+    this._resetInstanceCounters(); // Emitting
+
+
+    this.emit('cleared');
+  }
+  /**
+   * Method used to remove every edge from the graph.
+   *
+   * @return {Graph}
+   */
+  ;
+
+  _proto.clearEdges = function clearEdges() {
+    // Clearing structure index
+    var iterator = this._nodes.values();
+
+    var step;
+
+    while (step = iterator.next(), step.done !== true) {
+      step.value.clear();
+    } // Clearing edges
+
+
+    this._edges.clear(); // Reset counters
+
+
+    this._resetInstanceCounters(); // Emitting
+
+
+    this.emit('edgesCleared');
+  }
+  /**---------------------------------------------------------------------------
+   * Attributes-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning the desired graph's attribute.
+   *
+   * @param  {string} name - Name of the attribute.
+   * @return {any}
+   */
+  ;
+
+  _proto.getAttribute = function getAttribute(name) {
+    return this._attributes[name];
+  }
+  /**
+   * Method returning the graph's attributes.
+   *
+   * @return {object}
+   */
+  ;
+
+  _proto.getAttributes = function getAttributes() {
+    return this._attributes;
+  }
+  /**
+   * Method returning whether the graph has the desired attribute.
+   *
+   * @param  {string}  name - Name of the attribute.
+   * @return {boolean}
+   */
+  ;
+
+  _proto.hasAttribute = function hasAttribute(name) {
+    return this._attributes.hasOwnProperty(name);
+  }
+  /**
+   * Method setting a value for the desired graph's attribute.
+   *
+   * @param  {string}  name  - Name of the attribute.
+   * @param  {any}     value - Value for the attribute.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.setAttribute = function setAttribute(name, value) {
+    this._attributes[name] = value; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method using a function to update the desired graph's attribute's value.
+   *
+   * @param  {string}   name    - Name of the attribute.
+   * @param  {function} updater - Function use to update the attribute's value.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.updateAttribute = function updateAttribute(name, updater) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttribute: updater should be a function.');
+    var value = this._attributes[name];
+    this._attributes[name] = updater(value); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method removing the desired graph's attribute.
+   *
+   * @param  {string} name  - Name of the attribute.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.removeAttribute = function removeAttribute(name) {
+    delete this._attributes[name]; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'remove',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method replacing the graph's attributes.
+   *
+   * @param  {object} attributes - New attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  ;
+
+  _proto.replaceAttributes = function replaceAttributes(attributes) {
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.replaceAttributes: provided attributes are not a plain object.');
+    this._attributes = attributes; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'replace',
+      attributes: this._attributes
+    });
+    return this;
+  }
+  /**
+   * Method merging the graph's attributes.
+   *
+   * @param  {object} attributes - Attributes to merge.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  ;
+
+  _proto.mergeAttributes = function mergeAttributes(attributes) {
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.mergeAttributes: provided attributes are not a plain object.');
+    assign(this._attributes, attributes); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'merge',
+      attributes: this._attributes,
+      data: attributes
+    });
+    return this;
+  }
+  /**
+   * Method updating the graph's attributes.
+   *
+   * @param  {function} updater - Function used to update the attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given updater is not a function.
+   */
+  ;
+
+  _proto.updateAttributes = function updateAttributes(updater) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttributes: provided updater is not a function.');
+    this._attributes = updater(this._attributes); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'update',
+      attributes: this._attributes
+    });
+    return this;
+  }
+  /**
+   * Method used to update each node's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  ;
+
+  _proto.updateEachNodeAttributes = function updateEachNodeAttributes(updater, hints) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: expecting an updater function.');
+    if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+    }
+
+    this.emit('eachNodeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+  /**
+   * Method used to update each edge's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  ;
+
+  _proto.updateEachEdgeAttributes = function updateEachEdgeAttributes(updater, hints) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: expecting an updater function.');
+    if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+    var iterator = this._edges.values();
+
+    var step, edgeData, sourceData, targetData;
+
+    while (step = iterator.next(), step.done !== true) {
+      edgeData = step.value;
+      sourceData = edgeData.source;
+      targetData = edgeData.target;
+      edgeData.attributes = updater(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected);
+    }
+
+    this.emit('eachEdgeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+  /**---------------------------------------------------------------------------
+   * Iteration-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method iterating over the graph's adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  ;
+
+  _proto.forEachAdjacencyEntry = function forEachAdjacencyEntry(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntry: expecting a callback.');
+    forEachAdjacency(false, false, false, this, callback);
+  };
+
+  _proto.forEachAdjacencyEntryWithOrphans = function forEachAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.');
+    forEachAdjacency(false, false, true, this, callback);
+  }
+  /**
+   * Method iterating over the graph's assymetric adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  ;
+
+  _proto.forEachAssymetricAdjacencyEntry = function forEachAssymetricAdjacencyEntry(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntry: expecting a callback.');
+    forEachAdjacency(false, true, false, this, callback);
+  };
+
+  _proto.forEachAssymetricAdjacencyEntryWithOrphans = function forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.');
+    forEachAdjacency(false, true, true, this, callback);
+  }
+  /**
+   * Method returning the list of the graph's nodes.
+   *
+   * @return {array} - The nodes.
+   */
+  ;
+
+  _proto.nodes = function nodes() {
+    if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+    return take__default["default"](this._nodes.keys(), this._nodes.size);
+  }
+  /**
+   * Method iterating over the graph's nodes using the given callback.
+   *
+   * @param  {function}  callback - Callback (key, attributes, index).
+   */
+  ;
+
+  _proto.forEachNode = function forEachNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      callback(nodeData.key, nodeData.attributes);
+    }
+  }
+  /**
+   * Method iterating attempting to find a node matching the given predicate
+   * function.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.findNode = function findNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.findNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+    }
+
+    return;
+  }
+  /**
+   * Method mapping nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.mapNodes = function mapNodes(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.mapNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+    var result = new Array(this.order);
+    var i = 0;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      result[i++] = callback(nodeData.key, nodeData.attributes);
+    }
+
+    return result;
+  }
+  /**
+   * Method returning whether some node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.someNode = function someNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.someNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether all node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.everyNode = function everyNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.everyNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (!callback(nodeData.key, nodeData.attributes)) return false;
+    }
+
+    return true;
+  }
+  /**
+   * Method filtering nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.filterNodes = function filterNodes(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.filterNodes: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+    var result = [];
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) result.push(nodeData.key);
+    }
+
+    return result;
+  }
+  /**
+   * Method reducing nodes.
+   *
+   * @param  {function}  callback - Callback (accumulator, key, attributes).
+   */
+  ;
+
+  _proto.reduceNodes = function reduceNodes(callback, initialValue) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.reduceNodes: expecting a callback.');
+    if (arguments.length < 2) throw new InvalidArgumentsGraphError('Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.');
+    var accumulator = initialValue;
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+    }
+
+    return accumulator;
+  }
+  /**
+   * Method returning an iterator over the graph's node entries.
+   *
+   * @return {Iterator}
+   */
+  ;
+
+  _proto.nodeEntries = function nodeEntries() {
+    var iterator = this._nodes.values();
+
+    return new Iterator__default["default"](function () {
+      var step = iterator.next();
+      if (step.done) return step;
+      var data = step.value;
+      return {
+        value: {
+          node: data.key,
+          attributes: data.attributes
+        },
+        done: false
+      };
+    });
+  }
+  /**---------------------------------------------------------------------------
+   * Serialization
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to export the whole graph.
+   *
+   * @return {object} - The serialized graph.
+   */
+  ;
+
+  _proto["export"] = function _export() {
+    var nodes = new Array(this._nodes.size);
+    var i = 0;
+
+    this._nodes.forEach(function (data, key) {
+      nodes[i++] = serializeNode(key, data);
+    });
+
+    var edges = new Array(this._edges.size);
+    i = 0;
+
+    this._edges.forEach(function (data, key) {
+      edges[i++] = serializeEdge(key, data);
+    });
+
+    return {
+      options: {
+        type: this.type,
+        multi: this.multi,
+        allowSelfLoops: this.allowSelfLoops
+      },
+      attributes: this.getAttributes(),
+      nodes: nodes,
+      edges: edges
+    };
+  }
+  /**
+   * Method used to import a serialized graph.
+   *
+   * @param  {object|Graph} data  - The serialized graph.
+   * @param  {boolean}      merge - Whether to merge data.
+   * @return {Graph}              - Returns itself for chaining.
+   */
+  ;
+
+  _proto["import"] = function _import(data) {
+    var _this2 = this;
+
+    var merge = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+    // Importing a Graph instance directly
+    if (isGraph(data)) {
+      // Nodes
+      data.forEachNode(function (n, a) {
+        if (merge) _this2.mergeNode(n, a);else _this2.addNode(n, a);
+      }); // Edges
+
+      data.forEachEdge(function (e, a, s, t, _sa, _ta, u) {
+        if (merge) {
+          if (u) _this2.mergeUndirectedEdgeWithKey(e, s, t, a);else _this2.mergeDirectedEdgeWithKey(e, s, t, a);
+        } else {
+          if (u) _this2.addUndirectedEdgeWithKey(e, s, t, a);else _this2.addDirectedEdgeWithKey(e, s, t, a);
+        }
+      });
+      return this;
+    } // Importing a serialized graph
+
+
+    if (!isPlainObject(data)) throw new InvalidArgumentsGraphError('Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.');
+
+    if (data.attributes) {
+      if (!isPlainObject(data.attributes)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Expecting a plain object.');
+      if (merge) this.mergeAttributes(data.attributes);else this.replaceAttributes(data.attributes);
+    }
+
+    var i, l, list, node, edge;
+
+    if (data.nodes) {
+      list = data.nodes;
+      if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid nodes. Expecting an array.');
+
+      for (i = 0, l = list.length; i < l; i++) {
+        node = list[i]; // Validating
+
+        validateSerializedNode(node); // Adding the node
+
+        var _node = node,
+            key = _node.key,
+            attributes = _node.attributes;
+        if (merge) this.mergeNode(key, attributes);else this.addNode(key, attributes);
+      }
+    }
+
+    if (data.edges) {
+      list = data.edges;
+      if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid edges. Expecting an array.');
+
+      for (i = 0, l = list.length; i < l; i++) {
+        edge = list[i]; // Validating
+
+        validateSerializedEdge(edge); // Adding the edge
+
+        var _edge = edge,
+            source = _edge.source,
+            target = _edge.target,
+            _attributes = _edge.attributes,
+            _edge$undirected = _edge.undirected,
+            undirected = _edge$undirected === void 0 ? false : _edge$undirected;
+        var method = void 0;
+
+        if ('key' in edge) {
+          method = merge ? undirected ? this.mergeUndirectedEdgeWithKey : this.mergeDirectedEdgeWithKey : undirected ? this.addUndirectedEdgeWithKey : this.addDirectedEdgeWithKey;
+          method.call(this, edge.key, source, target, _attributes);
+        } else {
+          method = merge ? undirected ? this.mergeUndirectedEdge : this.mergeDirectedEdge : undirected ? this.addUndirectedEdge : this.addDirectedEdge;
+          method.call(this, source, target, _attributes);
+        }
+      }
+    }
+
+    return this;
+  }
+  /**---------------------------------------------------------------------------
+   * Utils
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning a null copy of the graph, i.e. a graph without nodes
+   * & edges but with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The null copy.
+   */
+  ;
+
+  _proto.nullCopy = function nullCopy(options) {
+    var graph = new Graph(assign({}, this._options, options));
+    graph.replaceAttributes(assign({}, this.getAttributes()));
+    return graph;
+  }
+  /**
+   * Method returning an empty copy of the graph, i.e. a graph without edges but
+   * with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The empty copy.
+   */
+  ;
+
+  _proto.emptyCopy = function emptyCopy(options) {
+    var graph = this.nullCopy(options);
+
+    this._nodes.forEach(function (nodeData, key) {
+      var attributes = assign({}, nodeData.attributes); // NOTE: no need to emit events since user cannot access the instance yet
+
+      nodeData = new graph.NodeDataClass(key, attributes);
+
+      graph._nodes.set(key, nodeData);
+    });
+
+    return graph;
+  }
+  /**
+   * Method returning an exact copy of the graph.
+   *
+   * @param  {object} options - Upgrade options.
+   * @return {Graph}          - The copy.
+   */
+  ;
+
+  _proto.copy = function copy(options) {
+    options = options || {};
+    if (typeof options.type === 'string' && options.type !== this.type && options.type !== 'mixed') throw new UsageGraphError("Graph.copy: cannot create an incompatible copy from \"".concat(this.type, "\" type to \"").concat(options.type, "\" because this would mean losing information about the current graph."));
+    if (typeof options.multi === 'boolean' && options.multi !== this.multi && options.multi !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.');
+    if (typeof options.allowSelfLoops === 'boolean' && options.allowSelfLoops !== this.allowSelfLoops && options.allowSelfLoops !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.');
+    var graph = this.emptyCopy(options);
+
+    var iterator = this._edges.values();
+
+    var step, edgeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      edgeData = step.value; // NOTE: no need to emit events since user cannot access the instance yet
+
+      addEdge(graph, 'copy', false, edgeData.undirected, edgeData.key, edgeData.source.key, edgeData.target.key, assign({}, edgeData.attributes));
+    }
+
+    return graph;
+  }
+  /**---------------------------------------------------------------------------
+   * Known methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used by JavaScript to perform JSON serialization.
+   *
+   * @return {object} - The serialized graph.
+   */
+  ;
+
+  _proto.toJSON = function toJSON() {
+    return this["export"]();
+  }
+  /**
+   * Method returning [object Graph].
+   */
+  ;
+
+  _proto.toString = function toString() {
+    return '[object Graph]';
+  }
+  /**
+   * Method used internally by node's console to display a custom object.
+   *
+   * @return {object} - Formatted object representation of the graph.
+   */
+  ;
+
+  _proto.inspect = function inspect() {
+    var _this3 = this;
+
+    var nodes = {};
+
+    this._nodes.forEach(function (data, key) {
+      nodes[key] = data.attributes;
+    });
+
+    var edges = {},
+        multiIndex = {};
+
+    this._edges.forEach(function (data, key) {
+      var direction = data.undirected ? '--' : '->';
+      var label = '';
+      var source = data.source.key;
+      var target = data.target.key;
+      var tmp;
+
+      if (data.undirected && source > target) {
+        tmp = source;
+        source = target;
+        target = tmp;
+      }
+
+      var desc = "(".concat(source, ")").concat(direction, "(").concat(target, ")");
+
+      if (!key.startsWith('geid_')) {
+        label += "[".concat(key, "]: ");
+      } else if (_this3.multi) {
+        if (typeof multiIndex[desc] === 'undefined') {
+          multiIndex[desc] = 0;
+        } else {
+          multiIndex[desc]++;
+        }
+
+        label += "".concat(multiIndex[desc], ". ");
+      }
+
+      label += desc;
+      edges[label] = data.attributes;
+    });
+
+    var dummy = {};
+
+    for (var k in this) {
+      if (this.hasOwnProperty(k) && !EMITTER_PROPS.has(k) && typeof this[k] !== 'function' && _typeof(k) !== 'symbol') dummy[k] = this[k];
+    }
+
+    dummy.attributes = this._attributes;
+    dummy.nodes = nodes;
+    dummy.edges = edges;
+    privateProperty(dummy, 'constructor', this.constructor);
+    return dummy;
+  };
+
+  return Graph;
+}(events.EventEmitter);
+if (typeof Symbol !== 'undefined') Graph.prototype[Symbol["for"]('nodejs.util.inspect.custom')] = Graph.prototype.inspect;
+/**
+ * Related to edge addition.
+ */
+
+EDGE_ADD_METHODS.forEach(function (method) {
+  ['add', 'merge', 'update'].forEach(function (verb) {
+    var name = method.name(verb);
+    var fn = verb === 'add' ? addEdge : mergeEdge;
+
+    if (method.generateKey) {
+      Graph.prototype[name] = function (source, target, attributes) {
+        return fn(this, name, true, (method.type || this.type) === 'undirected', null, source, target, attributes, verb === 'update');
+      };
+    } else {
+      Graph.prototype[name] = function (edge, source, target, attributes) {
+        return fn(this, name, false, (method.type || this.type) === 'undirected', edge, source, target, attributes, verb === 'update');
+      };
+    }
+  });
+});
+/**
+ * Attributes-related.
+ */
+
+attachNodeAttributesMethods(Graph);
+attachEdgeAttributesMethods(Graph);
+/**
+ * Edge iteration-related.
+ */
+
+attachEdgeIterationMethods(Graph);
+/**
+ * Neighbor iteration-related.
+ */
+
+attachNeighborIterationMethods(Graph);
+
+/**
+ * Alternative constructors.
+ */
+
+var DirectedGraph = /*#__PURE__*/function (_Graph) {
+  _inheritsLoose(DirectedGraph, _Graph);
+
+  function DirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'directed'
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+    if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph.call(this, finalOptions) || this;
+  }
+
+  return DirectedGraph;
+}(Graph);
+
+var UndirectedGraph = /*#__PURE__*/function (_Graph2) {
+  _inheritsLoose(UndirectedGraph, _Graph2);
+
+  function UndirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'undirected'
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+    if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph2.call(this, finalOptions) || this;
+  }
+
+  return UndirectedGraph;
+}(Graph);
+
+var MultiGraph = /*#__PURE__*/function (_Graph3) {
+  _inheritsLoose(MultiGraph, _Graph3);
+
+  function MultiGraph(options) {
+    var finalOptions = assign({
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiGraph.from: inconsistent indication that the graph should be simple in given options!');
+    return _Graph3.call(this, finalOptions) || this;
+  }
+
+  return MultiGraph;
+}(Graph);
+
+var MultiDirectedGraph = /*#__PURE__*/function (_Graph4) {
+  _inheritsLoose(MultiDirectedGraph, _Graph4);
+
+  function MultiDirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'directed',
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+    if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph4.call(this, finalOptions) || this;
+  }
+
+  return MultiDirectedGraph;
+}(Graph);
+
+var MultiUndirectedGraph = /*#__PURE__*/function (_Graph5) {
+  _inheritsLoose(MultiUndirectedGraph, _Graph5);
+
+  function MultiUndirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'undirected',
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+    if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph5.call(this, finalOptions) || this;
+  }
+
+  return MultiUndirectedGraph;
+}(Graph);
+/**
+ * Attaching static #.from method to each of the constructors.
+ */
+
+
+function attachStaticFromMethod(Class) {
+  /**
+   * Builds a graph from serialized data or another graph's data.
+   *
+   * @param  {Graph|SerializedGraph} data      - Hydratation data.
+   * @param  {object}                [options] - Options.
+   * @return {Class}
+   */
+  Class.from = function (data, options) {
+    // Merging given options with serialized ones
+    var finalOptions = assign({}, data.options, options);
+    var instance = new Class(finalOptions);
+    instance["import"](data);
+    return instance;
+  };
+}
+
+attachStaticFromMethod(Graph);
+attachStaticFromMethod(DirectedGraph);
+attachStaticFromMethod(UndirectedGraph);
+attachStaticFromMethod(MultiGraph);
+attachStaticFromMethod(MultiDirectedGraph);
+attachStaticFromMethod(MultiUndirectedGraph);
+Graph.Graph = Graph;
+Graph.DirectedGraph = DirectedGraph;
+Graph.UndirectedGraph = UndirectedGraph;
+Graph.MultiGraph = MultiGraph;
+Graph.MultiDirectedGraph = MultiDirectedGraph;
+Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+Graph.NotFoundGraphError = NotFoundGraphError;
+Graph.UsageGraphError = UsageGraphError;
+
+/**
+ * Graphology CommonJS Endoint
+ * ============================
+ *
+ * Endpoint for CommonJS modules consumers.
+ */
+
+module.exports = Graph;
+//# sourceMappingURL=graphology.cjs.js.map
diff --git a/libs/shared/graph-layout/node_modules/graphology/dist/graphology.d.ts b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d2f14ffca954a992db281fa4780e6b17d2e33ab3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.d.ts
@@ -0,0 +1,36 @@
+import {AbstractGraph, Attributes} from 'graphology-types';
+
+export default class Graph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends AbstractGraph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class DirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class UndirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiDirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiUndirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+
+export class InvalidArgumentsGraphError extends Error {}
+export class NotFoundGraphError extends Error {}
+export class UsageGraphError extends Error {}
diff --git a/libs/shared/graph-layout/node_modules/graphology/dist/graphology.esm.js b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.esm.js
new file mode 100644
index 0000000000000000000000000000000000000000..f89b5be2e04f484e09a562e17b20b2336dc00126
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.esm.js
@@ -0,0 +1,6671 @@
+import { EventEmitter } from 'events';
+import Iterator from 'obliterator/iterator';
+import take from 'obliterator/take';
+import chain from 'obliterator/chain';
+
+/**
+ * Graphology Utilities
+ * =====================
+ *
+ * Collection of helpful functions used by the implementation.
+ */
+
+/**
+ * Object.assign-like polyfill.
+ *
+ * @param  {object} target       - First object.
+ * @param  {object} [...objects] - Objects to merge.
+ * @return {object}
+ */
+function assignPolyfill() {
+  const target = arguments[0];
+
+  for (let i = 1, l = arguments.length; i < l; i++) {
+    if (!arguments[i]) continue;
+
+    for (const k in arguments[i]) target[k] = arguments[i][k];
+  }
+
+  return target;
+}
+
+let assign = assignPolyfill;
+
+if (typeof Object.assign === 'function') assign = Object.assign;
+
+/**
+ * Function returning the first matching edge for given path.
+ * Note: this function does not check the existence of source & target. This
+ * must be performed by the caller.
+ *
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @param  {any}    target - Target node.
+ * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+ * @return {string|null}
+ */
+function getMatchingEdge(graph, source, target, type) {
+  const sourceData = graph._nodes.get(source);
+
+  let edge = null;
+
+  if (!sourceData) return edge;
+
+  if (type === 'mixed') {
+    edge =
+      (sourceData.out && sourceData.out[target]) ||
+      (sourceData.undirected && sourceData.undirected[target]);
+  } else if (type === 'directed') {
+    edge = sourceData.out && sourceData.out[target];
+  } else {
+    edge = sourceData.undirected && sourceData.undirected[target];
+  }
+
+  return edge;
+}
+
+/**
+ * Checks whether the given value is a Graph implementation instance.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+function isGraph(value) {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    typeof value.addUndirectedEdgeWithKey === 'function' &&
+    typeof value.dropNode === 'function'
+  );
+}
+
+/**
+ * Checks whether the given value is a plain object.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+function isPlainObject(value) {
+  return (
+    typeof value === 'object' && value !== null && value.constructor === Object
+  );
+}
+
+/**
+ * Checks whether the given object is empty.
+ *
+ * @param  {object}  o - Target Object.
+ * @return {boolean}
+ */
+function isEmpty(o) {
+  let k;
+
+  for (k in o) return false;
+
+  return true;
+}
+
+/**
+ * Creates a "private" property for the given member name by concealing it
+ * using the `enumerable` option.
+ *
+ * @param {object} target - Target object.
+ * @param {string} name   - Member name.
+ */
+function privateProperty(target, name, value) {
+  Object.defineProperty(target, name, {
+    enumerable: false,
+    configurable: false,
+    writable: true,
+    value
+  });
+}
+
+/**
+ * Creates a read-only property for the given member name & the given getter.
+ *
+ * @param {object}   target - Target object.
+ * @param {string}   name   - Member name.
+ * @param {mixed}    value  - The attached getter or fixed value.
+ */
+function readOnlyProperty(target, name, value) {
+  const descriptor = {
+    enumerable: true,
+    configurable: true
+  };
+
+  if (typeof value === 'function') {
+    descriptor.get = value;
+  } else {
+    descriptor.value = value;
+    descriptor.writable = false;
+  }
+
+  Object.defineProperty(target, name, descriptor);
+}
+
+/**
+ * Returns whether the given object constitute valid hints.
+ *
+ * @param {object} hints - Target object.
+ */
+function validateHints(hints) {
+  if (!isPlainObject(hints)) return false;
+
+  if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+
+  return true;
+}
+
+/**
+ * Creates a function generating incremental ids for edges.
+ *
+ * @return {function}
+ */
+function incrementalIdStartingFromRandomByte() {
+  let i = Math.floor(Math.random() * 256) & 0xff;
+
+  return () => {
+    return i++;
+  };
+}
+
+/**
+ * Graphology Custom Errors
+ * =========================
+ *
+ * Defining custom errors for ease of use & easy unit tests across
+ * implementations (normalized typology rather than relying on error
+ * messages to check whether the correct error was found).
+ */
+class GraphError extends Error {
+  constructor(message) {
+    super();
+    this.name = 'GraphError';
+    this.message = message;
+  }
+}
+
+class InvalidArgumentsGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'InvalidArgumentsGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(
+        this,
+        InvalidArgumentsGraphError.prototype.constructor
+      );
+  }
+}
+
+class NotFoundGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'NotFoundGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(this, NotFoundGraphError.prototype.constructor);
+  }
+}
+
+class UsageGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'UsageGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(this, UsageGraphError.prototype.constructor);
+  }
+}
+
+/**
+ * Graphology Internal Data Classes
+ * =================================
+ *
+ * Internal classes hopefully reduced to structs by engines & storing
+ * necessary information for nodes & edges.
+ *
+ * Note that those classes don't rely on the `class` keyword to avoid some
+ * cruft introduced by most of ES2015 transpilers.
+ */
+
+/**
+ * MixedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function MixedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+MixedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+  this.undirectedDegree = 0;
+
+  // Indices
+  this.in = {};
+  this.out = {};
+  this.undirected = {};
+};
+
+/**
+ * DirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function DirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+DirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+
+  // Indices
+  this.in = {};
+  this.out = {};
+};
+
+/**
+ * UndirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function UndirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+UndirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.undirectedDegree = 0;
+
+  // Indices
+  this.undirected = {};
+};
+
+/**
+ * EdgeData class.
+ *
+ * @constructor
+ * @param {boolean} undirected   - Whether the edge is undirected.
+ * @param {string}  string       - The edge's key.
+ * @param {string}  source       - Source of the edge.
+ * @param {string}  target       - Target of the edge.
+ * @param {object}  attributes   - Edge's attributes.
+ */
+function EdgeData(undirected, key, source, target, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.undirected = undirected;
+
+  // Extremities
+  this.source = source;
+  this.target = target;
+}
+
+EdgeData.prototype.attach = function () {
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  const source = this.source.key;
+  const target = this.target.key;
+
+  // Handling source
+  this.source[outKey][target] = this;
+
+  if (this.undirected && source === target) return;
+
+  // Handling target
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.attachMulti = function () {
+  let outKey = 'out';
+  let inKey = 'in';
+
+  const source = this.source.key;
+  const target = this.target.key;
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  // Handling source
+  const adj = this.source[outKey];
+  const head = adj[target];
+
+  if (typeof head === 'undefined') {
+    adj[target] = this;
+
+    // Self-loop optimization
+    if (!(this.undirected && source === target)) {
+      // Handling target
+      this.target[inKey][source] = this;
+    }
+
+    return;
+  }
+
+  // Prepending to doubly-linked list
+  head.previous = this;
+  this.next = head;
+
+  // Pointing to new head
+  // NOTE: use mutating swap later to avoid lookup?
+  adj[target] = this;
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.detach = function () {
+  const source = this.source.key;
+  const target = this.target.key;
+
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  delete this.source[outKey][target];
+
+  // No-op delete in case of undirected self-loop
+  delete this.target[inKey][source];
+};
+
+EdgeData.prototype.detachMulti = function () {
+  const source = this.source.key;
+  const target = this.target.key;
+
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  // Deleting from doubly-linked list
+  if (this.previous === undefined) {
+    // We are dealing with the head
+
+    // Should we delete the adjacency entry because it is now empty?
+    if (this.next === undefined) {
+      delete this.source[outKey][target];
+
+      // No-op delete in case of undirected self-loop
+      delete this.target[inKey][source];
+    } else {
+      // Detaching
+      this.next.previous = undefined;
+
+      // NOTE: could avoid the lookups by creating a #.become mutating method
+      this.source[outKey][target] = this.next;
+
+      // No-op delete in case of undirected self-loop
+      this.target[inKey][source] = this.next;
+    }
+  } else {
+    // We are dealing with another list node
+    this.previous.next = this.next;
+
+    // If not last
+    if (this.next !== undefined) {
+      this.next.previous = this.previous;
+    }
+  }
+};
+
+/**
+ * Graphology Node Attributes methods
+ * ===================================
+ */
+
+const NODE = 0;
+const SOURCE = 1;
+const TARGET = 2;
+const OPPOSITE = 3;
+
+function findRelevantNodeData(
+  graph,
+  method,
+  mode,
+  nodeOrEdge,
+  nameOrEdge,
+  add1,
+  add2
+) {
+  let nodeData, edgeData, arg1, arg2;
+
+  nodeOrEdge = '' + nodeOrEdge;
+
+  if (mode === NODE) {
+    nodeData = graph._nodes.get(nodeOrEdge);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nodeOrEdge}" node in the graph.`
+      );
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  } else if (mode === OPPOSITE) {
+    nameOrEdge = '' + nameOrEdge;
+
+    edgeData = graph._edges.get(nameOrEdge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nameOrEdge}" edge in the graph.`
+      );
+
+    const source = edgeData.source.key;
+    const target = edgeData.target.key;
+
+    if (nodeOrEdge === source) {
+      nodeData = edgeData.target;
+    } else if (nodeOrEdge === target) {
+      nodeData = edgeData.source;
+    } else {
+      throw new NotFoundGraphError(
+        `Graph.${method}: the "${nodeOrEdge}" node is not attached to the "${nameOrEdge}" edge (${source}, ${target}).`
+      );
+    }
+
+    arg1 = add1;
+    arg2 = add2;
+  } else {
+    edgeData = graph._edges.get(nodeOrEdge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nodeOrEdge}" edge in the graph.`
+      );
+
+    if (mode === SOURCE) {
+      nodeData = edgeData.source;
+    } else {
+      nodeData = edgeData.target;
+    }
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  }
+
+  return [nodeData, arg1, arg2];
+}
+
+function attachNodeAttributeGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    return data.attributes[name];
+  };
+}
+
+function attachNodeAttributesGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+    const [data] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge
+    );
+
+    return data.attributes;
+  };
+}
+
+function attachNodeAttributeChecker(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+function attachNodeAttributeSetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    const [data, name, value] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1,
+      add2
+    );
+
+    data.attributes[name] = value;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributeUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    const [data, name, updater] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1,
+      add2
+    );
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: updater should be a function.`
+      );
+
+    const attributes = data.attributes;
+    const value = updater(attributes[name]);
+
+    attributes[name] = value;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributeRemover(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    delete data.attributes[name];
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesReplacer(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, attributes] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    data.attributes = attributes;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesMerger(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, attributes] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    assign(data.attributes, attributes);
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, updater] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided updater is not a function.`
+      );
+
+    data.attributes = updater(data.attributes);
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * List of methods to attach.
+ */
+const NODE_ATTRIBUTES_METHODS = [
+  {
+    name: element => `get${element}Attribute`,
+    attacher: attachNodeAttributeGetter
+  },
+  {
+    name: element => `get${element}Attributes`,
+    attacher: attachNodeAttributesGetter
+  },
+  {
+    name: element => `has${element}Attribute`,
+    attacher: attachNodeAttributeChecker
+  },
+  {
+    name: element => `set${element}Attribute`,
+    attacher: attachNodeAttributeSetter
+  },
+  {
+    name: element => `update${element}Attribute`,
+    attacher: attachNodeAttributeUpdater
+  },
+  {
+    name: element => `remove${element}Attribute`,
+    attacher: attachNodeAttributeRemover
+  },
+  {
+    name: element => `replace${element}Attributes`,
+    attacher: attachNodeAttributesReplacer
+  },
+  {
+    name: element => `merge${element}Attributes`,
+    attacher: attachNodeAttributesMerger
+  },
+  {
+    name: element => `update${element}Attributes`,
+    attacher: attachNodeAttributesUpdater
+  }
+];
+
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+function attachNodeAttributesMethods(Graph) {
+  NODE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) {
+    // For nodes
+    attacher(Graph, name('Node'), NODE);
+
+    // For sources
+    attacher(Graph, name('Source'), SOURCE);
+
+    // For targets
+    attacher(Graph, name('Target'), TARGET);
+
+    // For opposites
+    attacher(Graph, name('Opposite'), OPPOSITE);
+  });
+}
+
+/**
+ * Graphology Edge Attributes methods
+ * ===================================
+ */
+
+/**
+ * Attach an attribute getter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeGetter(Class, method, type) {
+  /**
+   * Get the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {mixed}          - The attribute's value.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes[name];
+  };
+}
+
+/**
+ * Attach an attributes getter method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+function attachEdgeAttributesGetter(Class, method, type) {
+  /**
+   * Retrieves all the target element's attributes.
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   *
+   * @return {object}          - The element's attributes.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 1) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + arguments[1];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes;
+  };
+}
+
+/**
+ * Attach an attribute checker method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+function attachEdgeAttributeChecker(Class, method, type) {
+  /**
+   * Checks whether the desired attribute is set for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+/**
+ * Attach an attribute setter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeSetter(Class, method, type) {
+  /**
+   * Set the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, value) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 3) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+      value = arguments[3];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    data.attributes[name] = value;
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeUpdater(Class, method, type) {
+  /**
+   * Update the desired attribute for the given element (node or edge) using
+   * the provided function.
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, updater) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 3) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+      updater = arguments[3];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: updater should be a function.`
+      );
+
+    data.attributes[name] = updater(data.attributes[name]);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute remover method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeRemover(Class, method, type) {
+  /**
+   * Remove the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    delete data.attributes[name];
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute replacer method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesReplacer(Class, method, type) {
+  /**
+   * Replace the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - New attributes.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - New attributes.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + attributes;
+
+      attributes = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    data.attributes = attributes;
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute merger method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesMerger(Class, method, type) {
+  /**
+   * Merge the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - Attributes to merge.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - Attributes to merge.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + attributes;
+
+      attributes = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    assign(data.attributes, attributes);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesUpdater(Class, method, type) {
+  /**
+   * Update the attributes of the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, updater) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + updater;
+
+      updater = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided updater is not a function.`
+      );
+
+    data.attributes = updater(data.attributes);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * List of methods to attach.
+ */
+const EDGE_ATTRIBUTES_METHODS = [
+  {
+    name: element => `get${element}Attribute`,
+    attacher: attachEdgeAttributeGetter
+  },
+  {
+    name: element => `get${element}Attributes`,
+    attacher: attachEdgeAttributesGetter
+  },
+  {
+    name: element => `has${element}Attribute`,
+    attacher: attachEdgeAttributeChecker
+  },
+  {
+    name: element => `set${element}Attribute`,
+    attacher: attachEdgeAttributeSetter
+  },
+  {
+    name: element => `update${element}Attribute`,
+    attacher: attachEdgeAttributeUpdater
+  },
+  {
+    name: element => `remove${element}Attribute`,
+    attacher: attachEdgeAttributeRemover
+  },
+  {
+    name: element => `replace${element}Attributes`,
+    attacher: attachEdgeAttributesReplacer
+  },
+  {
+    name: element => `merge${element}Attributes`,
+    attacher: attachEdgeAttributesMerger
+  },
+  {
+    name: element => `update${element}Attributes`,
+    attacher: attachEdgeAttributesUpdater
+  }
+];
+
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+function attachEdgeAttributesMethods(Graph) {
+  EDGE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) {
+    // For edges
+    attacher(Graph, name('Edge'), 'mixed');
+
+    // For directed edges
+    attacher(Graph, name('DirectedEdge'), 'directed');
+
+    // For undirected edges
+    attacher(Graph, name('UndirectedEdge'), 'undirected');
+  });
+}
+
+/**
+ * Graphology Edge Iteration
+ * ==========================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's edges.
+ */
+
+/**
+ * Definitions.
+ */
+const EDGES_ITERATION = [
+  {
+    name: 'edges',
+    type: 'mixed'
+  },
+  {
+    name: 'inEdges',
+    type: 'directed',
+    direction: 'in'
+  },
+  {
+    name: 'outEdges',
+    type: 'directed',
+    direction: 'out'
+  },
+  {
+    name: 'inboundEdges',
+    type: 'mixed',
+    direction: 'in'
+  },
+  {
+    name: 'outboundEdges',
+    type: 'mixed',
+    direction: 'out'
+  },
+  {
+    name: 'directedEdges',
+    type: 'directed'
+  },
+  {
+    name: 'undirectedEdges',
+    type: 'undirected'
+  }
+];
+
+/**
+ * Function iterating over edges from the given object to match one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {function} callback - Function to call.
+ */
+function forEachSimple(breakable, object, callback, avoid) {
+  let shouldBreak = false;
+
+  for (const k in object) {
+    if (k === avoid) continue;
+
+    const edgeData = object[k];
+
+    shouldBreak = callback(
+      edgeData.key,
+      edgeData.attributes,
+      edgeData.source.key,
+      edgeData.target.key,
+      edgeData.source.attributes,
+      edgeData.target.attributes,
+      edgeData.undirected
+    );
+
+    if (breakable && shouldBreak) return edgeData.key;
+  }
+
+  return;
+}
+
+function forEachMulti(breakable, object, callback, avoid) {
+  let edgeData, source, target;
+
+  let shouldBreak = false;
+
+  for (const k in object) {
+    if (k === avoid) continue;
+
+    edgeData = object[k];
+
+    do {
+      source = edgeData.source;
+      target = edgeData.target;
+
+      shouldBreak = callback(
+        edgeData.key,
+        edgeData.attributes,
+        source.key,
+        target.key,
+        source.attributes,
+        target.attributes,
+        edgeData.undirected
+      );
+
+      if (breakable && shouldBreak) return edgeData.key;
+
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+  }
+
+  return;
+}
+
+/**
+ * Function returning an iterator over edges from the given object.
+ *
+ * @param  {object}   object - Target object.
+ * @return {Iterator}
+ */
+function createIterator(object, avoid) {
+  const keys = Object.keys(object);
+  const l = keys.length;
+
+  let edgeData;
+  let i = 0;
+
+  return new Iterator(function next() {
+    do {
+      if (!edgeData) {
+        if (i >= l) return {done: true};
+
+        const k = keys[i++];
+
+        if (k === avoid) {
+          edgeData = undefined;
+          continue;
+        }
+
+        edgeData = object[k];
+      } else {
+        edgeData = edgeData.next;
+      }
+    } while (!edgeData);
+
+    return {
+      done: false,
+      value: {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      }
+    };
+  });
+}
+
+/**
+ * Function iterating over the egdes from the object at given key to match
+ * one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {mixed}    k        - Neighbor key.
+ * @param {function} callback - Callback to use.
+ */
+function forEachForKeySimple(breakable, object, k, callback) {
+  const edgeData = object[k];
+
+  if (!edgeData) return;
+
+  const sourceData = edgeData.source;
+  const targetData = edgeData.target;
+
+  if (
+    callback(
+      edgeData.key,
+      edgeData.attributes,
+      sourceData.key,
+      targetData.key,
+      sourceData.attributes,
+      targetData.attributes,
+      edgeData.undirected
+    ) &&
+    breakable
+  )
+    return edgeData.key;
+}
+
+function forEachForKeyMulti(breakable, object, k, callback) {
+  let edgeData = object[k];
+
+  if (!edgeData) return;
+
+  let shouldBreak = false;
+
+  do {
+    shouldBreak = callback(
+      edgeData.key,
+      edgeData.attributes,
+      edgeData.source.key,
+      edgeData.target.key,
+      edgeData.source.attributes,
+      edgeData.target.attributes,
+      edgeData.undirected
+    );
+
+    if (breakable && shouldBreak) return edgeData.key;
+
+    edgeData = edgeData.next;
+  } while (edgeData !== undefined);
+
+  return;
+}
+
+/**
+ * Function returning an iterator over the egdes from the object at given key.
+ *
+ * @param  {object}   object   - Target object.
+ * @param  {mixed}    k        - Neighbor key.
+ * @return {Iterator}
+ */
+function createIteratorForKey(object, k) {
+  let edgeData = object[k];
+
+  if (edgeData.next !== undefined) {
+    return new Iterator(function () {
+      if (!edgeData) return {done: true};
+
+      const value = {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      };
+
+      edgeData = edgeData.next;
+
+      return {
+        done: false,
+        value
+      };
+    });
+  }
+
+  return Iterator.of({
+    edge: edgeData.key,
+    attributes: edgeData.attributes,
+    source: edgeData.source.key,
+    target: edgeData.target.key,
+    sourceAttributes: edgeData.source.attributes,
+    targetAttributes: edgeData.target.attributes,
+    undirected: edgeData.undirected
+  });
+}
+
+/**
+ * Function creating an array of edges for the given type.
+ *
+ * @param  {Graph}   graph - Target Graph instance.
+ * @param  {string}  type  - Type of edges to retrieve.
+ * @return {array}         - Array of edges.
+ */
+function createEdgeArray(graph, type) {
+  if (graph.size === 0) return [];
+
+  if (type === 'mixed' || type === graph.type) {
+    if (typeof Array.from === 'function')
+      return Array.from(graph._edges.keys());
+
+    return take(graph._edges.keys(), graph._edges.size);
+  }
+
+  const size =
+    type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+
+  const list = new Array(size),
+    mask = type === 'undirected';
+
+  const iterator = graph._edges.values();
+
+  let i = 0;
+  let step, data;
+
+  while (((step = iterator.next()), step.done !== true)) {
+    data = step.value;
+
+    if (data.undirected === mask) list[i++] = data.key;
+  }
+
+  return list;
+}
+
+/**
+ * Function iterating over a graph's edges using a callback to match one of
+ * them.
+ *
+ * @param  {Graph}    graph    - Target Graph instance.
+ * @param  {string}   type     - Type of edges to retrieve.
+ * @param  {function} callback - Function to call.
+ */
+function forEachEdge(breakable, graph, type, callback) {
+  if (graph.size === 0) return;
+
+  const shouldFilter = type !== 'mixed' && type !== graph.type;
+  const mask = type === 'undirected';
+
+  let step, data;
+  let shouldBreak = false;
+  const iterator = graph._edges.values();
+
+  while (((step = iterator.next()), step.done !== true)) {
+    data = step.value;
+
+    if (shouldFilter && data.undirected !== mask) continue;
+
+    const {key, attributes, source, target} = data;
+
+    shouldBreak = callback(
+      key,
+      attributes,
+      source.key,
+      target.key,
+      source.attributes,
+      target.attributes,
+      data.undirected
+    );
+
+    if (breakable && shouldBreak) return key;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an iterator of edges for the given type.
+ *
+ * @param  {Graph}    graph - Target Graph instance.
+ * @param  {string}   type  - Type of edges to retrieve.
+ * @return {Iterator}
+ */
+function createEdgeIterator(graph, type) {
+  if (graph.size === 0) return Iterator.empty();
+
+  const shouldFilter = type !== 'mixed' && type !== graph.type;
+  const mask = type === 'undirected';
+
+  const iterator = graph._edges.values();
+
+  return new Iterator(function next() {
+    let step, data;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      step = iterator.next();
+
+      if (step.done) return step;
+
+      data = step.value;
+
+      if (shouldFilter && data.undirected !== mask) continue;
+
+      break;
+    }
+
+    const value = {
+      edge: data.key,
+      attributes: data.attributes,
+      source: data.source.key,
+      target: data.target.key,
+      sourceAttributes: data.source.attributes,
+      targetAttributes: data.target.attributes,
+      undirected: data.undirected
+    };
+
+    return {value, done: false};
+  });
+}
+
+/**
+ * Function iterating over a node's edges using a callback to match one of them.
+ *
+ * @param  {boolean}  multi     - Whether the graph is multi or not.
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Function to call.
+ */
+function forEachEdgeForNode(
+  breakable,
+  multi,
+  type,
+  direction,
+  nodeData,
+  callback
+) {
+  const fn = multi ? forEachMulti : forEachSimple;
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = fn(breakable, nodeData.in, callback);
+
+      if (breakable && found) return found;
+    }
+    if (direction !== 'in') {
+      found = fn(
+        breakable,
+        nodeData.out,
+        callback,
+        !direction ? nodeData.key : undefined
+      );
+
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    found = fn(breakable, nodeData.undirected, callback);
+
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of edges for the given type & the given node.
+ *
+ * @param  {boolean} multi     - Whether the graph is multi or not.
+ * @param  {string}  type      - Type of edges to retrieve.
+ * @param  {string}  direction - In or out?
+ * @param  {any}     nodeData  - Target node's data.
+ * @return {array}             - Array of edges.
+ */
+function createEdgeArrayForNode(multi, type, direction, nodeData) {
+  const edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+    edges.push(key);
+  });
+
+  return edges;
+}
+
+/**
+ * Function iterating over a node's edges using a callback.
+ *
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+function createEdgeIteratorForNode(type, direction, nodeData) {
+  let iterator = Iterator.empty();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out' && typeof nodeData.in !== 'undefined')
+      iterator = chain(iterator, createIterator(nodeData.in));
+    if (direction !== 'in' && typeof nodeData.out !== 'undefined')
+      iterator = chain(
+        iterator,
+        createIterator(nodeData.out, !direction ? nodeData.key : undefined)
+      );
+  }
+
+  if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+    iterator = chain(iterator, createIterator(nodeData.undirected));
+  }
+
+  return iterator;
+}
+
+/**
+ * Function iterating over edges for the given path using a callback to match
+ * one of them.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+function forEachEdgeForPath(
+  breakable,
+  type,
+  multi,
+  direction,
+  sourceData,
+  target,
+  callback
+) {
+  const fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (typeof sourceData.in !== 'undefined' && direction !== 'out') {
+      found = fn(breakable, sourceData.in, target, callback);
+
+      if (breakable && found) return found;
+    }
+
+    if (
+      typeof sourceData.out !== 'undefined' &&
+      direction !== 'in' &&
+      (direction || sourceData.key !== target)
+    ) {
+      found = fn(breakable, sourceData.out, target, callback);
+
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined') {
+      found = fn(breakable, sourceData.undirected, target, callback);
+
+      if (breakable && found) return found;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {any}      target     - Target node.
+ * @return {array}               - Array of edges.
+ */
+function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+  const edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForPath(
+    false,
+    type,
+    multi,
+    direction,
+    sourceData,
+    target,
+    function (key) {
+      edges.push(key);
+    }
+  );
+
+  return edges;
+}
+
+/**
+ * Function returning an iterator over edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+function createEdgeIteratorForPath(type, direction, sourceData, target) {
+  let iterator = Iterator.empty();
+
+  if (type !== 'undirected') {
+    if (
+      typeof sourceData.in !== 'undefined' &&
+      direction !== 'out' &&
+      target in sourceData.in
+    )
+      iterator = chain(iterator, createIteratorForKey(sourceData.in, target));
+
+    if (
+      typeof sourceData.out !== 'undefined' &&
+      direction !== 'in' &&
+      target in sourceData.out &&
+      (direction || sourceData.key !== target)
+    )
+      iterator = chain(iterator, createIteratorForKey(sourceData.out, target));
+  }
+
+  if (type !== 'directed') {
+    if (
+      typeof sourceData.undirected !== 'undefined' &&
+      target in sourceData.undirected
+    )
+      iterator = chain(
+        iterator,
+        createIteratorForKey(sourceData.undirected, target)
+      );
+  }
+
+  return iterator;
+}
+
+/**
+ * Function attaching an edge array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachEdgeArrayCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  /**
+   * Function returning an array of certain edges.
+   *
+   * Arity 0: Return all the relevant edges.
+   *
+   * Arity 1: Return all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Return the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return [];
+
+    if (!arguments.length) return createEdgeArray(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${name}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      return createEdgeArrayForNode(
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData
+      );
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return createEdgeArrayForPath(
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).`
+    );
+  };
+}
+
+/**
+ * Function attaching a edge callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachForEachEdge(Class, description) {
+  const {name, type, direction} = description;
+
+  const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[forEachName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(false, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+      return forEachEdgeForNode(
+        false,
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData,
+        callback
+      );
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return forEachEdgeForPath(
+        false,
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target,
+        callback
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${forEachName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).`
+    );
+  };
+
+  /**
+   * Function mapping the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Map all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Map all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Map the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    let result;
+
+    // We know the result length beforehand
+    if (args.length === 0) {
+      let length = 0;
+
+      if (type !== 'directed') length += this.undirectedSize;
+      if (type !== 'undirected') length += this.directedSize;
+
+      result = new Array(length);
+
+      let i = 0;
+
+      args.push((e, ea, s, t, sa, ta, u) => {
+        result[i++] = callback(e, ea, s, t, sa, ta, u);
+      });
+    }
+
+    // We don't know the result length beforehand
+    // TODO: we can in some instances of simple graphs, knowing degree
+    else {
+      result = [];
+
+      args.push((e, ea, s, t, sa, ta, u) => {
+        result.push(callback(e, ea, s, t, sa, ta, u));
+      });
+    }
+
+    this[forEachName].apply(this, args);
+
+    return result;
+  };
+
+  /**
+   * Function filtering the graph's relevant edges using the provided predicate
+   * function.
+   *
+   * Arity 1: Filter all the relevant edges.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 2: Filter all of a node's relevant edges.
+   * @param  {any}      node      - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 3: Filter the relevant edges across the given path.
+   * @param  {any}      source    - Source node.
+   * @param  {any}      target    - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    const result = [];
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+    });
+
+    this[forEachName].apply(this, args);
+
+    return result;
+  };
+
+  /**
+   * Function reducing the graph's relevant edges using the provided accumulator
+   * function.
+   *
+   * Arity 1: Reduce all the relevant edges.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 2: Reduce all of a node's relevant edges.
+   * @param  {any}      node         - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 3: Reduce the relevant edges across the given path.
+   * @param  {any}      source       - Source node.
+   * @param  {any}      target       - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function () {
+    let args = Array.prototype.slice.call(arguments);
+
+    if (args.length < 2 || args.length > 4) {
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: invalid number of arguments (expecting 2, 3 or 4 and got ${args.length}).`
+      );
+    }
+
+    if (
+      typeof args[args.length - 1] === 'function' &&
+      typeof args[args.length - 2] !== 'function'
+    ) {
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.`
+      );
+    }
+
+    let callback;
+    let initialValue;
+
+    if (args.length === 2) {
+      callback = args[0];
+      initialValue = args[1];
+      args = [];
+    } else if (args.length === 3) {
+      callback = args[1];
+      initialValue = args[2];
+      args = [args[0]];
+    } else if (args.length === 4) {
+      callback = args[2];
+      initialValue = args[3];
+      args = [args[0], args[1]];
+    }
+
+    let accumulator = initialValue;
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+    });
+
+    this[forEachName].apply(this, args);
+
+    return accumulator;
+  };
+}
+
+/**
+ * Function attaching a breakable edge callback iterator method to the Graph
+ * prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachFindEdge(Class, description) {
+  const {name, type, direction} = description;
+
+  const findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over the graph's relevant edges in order to match
+   * one of them using the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[findEdgeName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return false;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(true, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+      return forEachEdgeForNode(
+        true,
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData,
+        callback
+      );
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return forEachEdgeForPath(
+        true,
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target,
+        callback
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${findEdgeName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).`
+    );
+  };
+
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether any one of them matches the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[someName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      return callback(e, ea, s, t, sa, ta, u);
+    });
+
+    const found = this[findEdgeName].apply(this, args);
+
+    if (found) return true;
+
+    return false;
+  };
+
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether all of them matche the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[everyName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      return !callback(e, ea, s, t, sa, ta, u);
+    });
+
+    const found = this[findEdgeName].apply(this, args);
+
+    if (found) return false;
+
+    return true;
+  };
+}
+
+/**
+ * Function attaching an edge iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachEdgeIteratorCreator(Class, description) {
+  const {name: originalName, type, direction} = description;
+
+  const name = originalName.slice(0, -1) + 'Entries';
+
+  /**
+   * Function returning an iterator over the graph's edges.
+   *
+   * Arity 0: Iterate over all the relevant edges.
+   *
+   * Arity 1: Iterate over all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Iterate over the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return Iterator.empty();
+
+    if (!arguments.length) return createEdgeIterator(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      return createEdgeIteratorForNode(type, direction, sourceData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return createEdgeIteratorForPath(type, direction, sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).`
+    );
+  };
+}
+
+/**
+ * Function attaching every edge iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+function attachEdgeIterationMethods(Graph) {
+  EDGES_ITERATION.forEach(description => {
+    attachEdgeArrayCreator(Graph, description);
+    attachForEachEdge(Graph, description);
+    attachFindEdge(Graph, description);
+    attachEdgeIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Neighbor Iteration
+ * ==============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over
+ * neighbors.
+ */
+
+/**
+ * Definitions.
+ */
+const NEIGHBORS_ITERATION = [
+  {
+    name: 'neighbors',
+    type: 'mixed'
+  },
+  {
+    name: 'inNeighbors',
+    type: 'directed',
+    direction: 'in'
+  },
+  {
+    name: 'outNeighbors',
+    type: 'directed',
+    direction: 'out'
+  },
+  {
+    name: 'inboundNeighbors',
+    type: 'mixed',
+    direction: 'in'
+  },
+  {
+    name: 'outboundNeighbors',
+    type: 'mixed',
+    direction: 'out'
+  },
+  {
+    name: 'directedNeighbors',
+    type: 'directed'
+  },
+  {
+    name: 'undirectedNeighbors',
+    type: 'undirected'
+  }
+];
+
+/**
+ * Helpers.
+ */
+function CompositeSetWrapper() {
+  this.A = null;
+  this.B = null;
+}
+
+CompositeSetWrapper.prototype.wrap = function (set) {
+  if (this.A === null) this.A = set;
+  else if (this.B === null) this.B = set;
+};
+
+CompositeSetWrapper.prototype.has = function (key) {
+  if (this.A !== null && key in this.A) return true;
+  if (this.B !== null && key in this.B) return true;
+  return false;
+};
+
+/**
+ * Function iterating over the given node's relevant neighbors to match
+ * one of them using a predicated function.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Callback to use.
+ */
+function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+  for (const k in object) {
+    const edgeData = object[k];
+
+    const sourceData = edgeData.source;
+    const targetData = edgeData.target;
+
+    const neighborData = sourceData === nodeData ? targetData : sourceData;
+
+    if (visited && visited.has(neighborData.key)) continue;
+
+    const shouldBreak = callback(neighborData.key, neighborData.attributes);
+
+    if (breakable && shouldBreak) return neighborData.key;
+  }
+
+  return;
+}
+
+function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected')
+      return forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData.undirected,
+        callback
+      );
+
+    if (typeof direction === 'string')
+      return forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData[direction],
+        callback
+      );
+  }
+
+  // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+  const visited = new CompositeSetWrapper();
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData.in,
+        callback
+      );
+
+      if (breakable && found) return found;
+
+      visited.wrap(nodeData.in);
+    }
+    if (direction !== 'in') {
+      found = forEachInObjectOnce(
+        breakable,
+        visited,
+        nodeData,
+        nodeData.out,
+        callback
+      );
+
+      if (breakable && found) return found;
+
+      visited.wrap(nodeData.out);
+    }
+  }
+
+  if (type !== 'directed') {
+    found = forEachInObjectOnce(
+      breakable,
+      visited,
+      nodeData,
+      nodeData.undirected,
+      callback
+    );
+
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of relevant neighbors for the given node.
+ *
+ * @param  {string}       type      - Type of neighbors.
+ * @param  {string}       direction - Direction.
+ * @param  {any}          nodeData  - Target node's data.
+ * @return {Array}                  - The list of neighbors.
+ */
+function createNeighborArrayForNode(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return Object.keys(nodeData.undirected);
+
+    if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+  }
+
+  const neighbors = [];
+
+  forEachNeighbor(false, type, direction, nodeData, function (key) {
+    neighbors.push(key);
+  });
+
+  return neighbors;
+}
+
+/**
+ * Function returning an iterator over the given node's relevant neighbors.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+function createDedupedObjectIterator(visited, nodeData, object) {
+  const keys = Object.keys(object);
+  const l = keys.length;
+
+  let i = 0;
+
+  return new Iterator(function next() {
+    let neighborData = null;
+
+    do {
+      if (i >= l) {
+        if (visited) visited.wrap(object);
+        return {done: true};
+      }
+
+      const edgeData = object[keys[i++]];
+
+      const sourceData = edgeData.source;
+      const targetData = edgeData.target;
+
+      neighborData = sourceData === nodeData ? targetData : sourceData;
+
+      if (visited && visited.has(neighborData.key)) {
+        neighborData = null;
+        continue;
+      }
+    } while (neighborData === null);
+
+    return {
+      done: false,
+      value: {neighbor: neighborData.key, attributes: neighborData.attributes}
+    };
+  });
+}
+
+function createNeighborIterator(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected')
+      return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+
+    if (typeof direction === 'string')
+      return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+  }
+
+  let iterator = Iterator.empty();
+
+  // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+  const visited = new CompositeSetWrapper();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      iterator = chain(
+        iterator,
+        createDedupedObjectIterator(visited, nodeData, nodeData.in)
+      );
+    }
+    if (direction !== 'in') {
+      iterator = chain(
+        iterator,
+        createDedupedObjectIterator(visited, nodeData, nodeData.out)
+      );
+    }
+  }
+
+  if (type !== 'directed') {
+    iterator = chain(
+      iterator,
+      createDedupedObjectIterator(visited, nodeData, nodeData.undirected)
+    );
+  }
+
+  return iterator;
+}
+
+/**
+ * Function attaching a neighbors array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachNeighborArrayCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  /**
+   * Function returning an array of certain neighbors.
+   *
+   * @param  {any}   node   - Target node.
+   * @return {array} - The neighbors of neighbors.
+   *
+   * @throws {Error} - Will throw if node is not found in the graph.
+   */
+  Class.prototype[name] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return [];
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${name}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return createNeighborArrayForNode(
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData
+    );
+  };
+}
+
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachForEachNeighbor(Class, description) {
+  const {name, type, direction} = description;
+
+  const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[forEachName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${forEachName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    forEachNeighbor(
+      false,
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData,
+      callback
+    );
+  };
+
+  /**
+   * Function mapping the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function (node, callback) {
+    // TODO: optimize when size is known beforehand
+    const result = [];
+
+    this[forEachName](node, (n, a) => {
+      result.push(callback(n, a));
+    });
+
+    return result;
+  };
+
+  /**
+   * Function filtering the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function (node, callback) {
+    const result = [];
+
+    this[forEachName](node, (n, a) => {
+      if (callback(n, a)) result.push(n);
+    });
+
+    return result;
+  };
+
+  /**
+   * Function reducing the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function (node, callback, initialValue) {
+    if (arguments.length < 3)
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.`
+      );
+
+    let accumulator = initialValue;
+
+    this[forEachName](node, (n, a) => {
+      accumulator = callback(accumulator, n, a);
+    });
+
+    return accumulator;
+  };
+}
+
+/**
+ * Function attaching a breakable neighbors callback iterator method to the
+ * Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachFindNeighbor(Class, description) {
+  const {name, type, direction} = description;
+
+  const capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+
+  const findName = 'find' + capitalizedSingular;
+
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[findName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${findName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return forEachNeighbor(
+      true,
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData,
+      callback
+    );
+  };
+
+  /**
+   * Function iterating over all the relevant neighbors to find if any of them
+   * matches the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const someName = 'some' + capitalizedSingular;
+
+  Class.prototype[someName] = function (node, callback) {
+    const found = this[findName](node, callback);
+
+    if (found) return true;
+
+    return false;
+  };
+
+  /**
+   * Function iterating over all the relevant neighbors to find if all of them
+   * matche the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const everyName = 'every' + capitalizedSingular;
+
+  Class.prototype[everyName] = function (node, callback) {
+    const found = this[findName](node, (n, a) => {
+      return !callback(n, a);
+    });
+
+    if (found) return false;
+
+    return true;
+  };
+}
+
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachNeighborIteratorCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  const iteratorName = name.slice(0, -1) + 'Entries';
+
+  /**
+   * Function returning an iterator over all the relevant neighbors.
+   *
+   * @param  {any}      node     - Target node.
+   * @return {Iterator}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[iteratorName] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return Iterator.empty();
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${iteratorName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return createNeighborIterator(
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData
+    );
+  };
+}
+
+/**
+ * Function attaching every neighbor iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+function attachNeighborIterationMethods(Graph) {
+  NEIGHBORS_ITERATION.forEach(description => {
+    attachNeighborArrayCreator(Graph, description);
+    attachForEachNeighbor(Graph, description);
+    attachFindNeighbor(Graph, description);
+    attachNeighborIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Adjacency Iteration
+ * ===============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's adjacency.
+ */
+
+/**
+ * Function iterating over a simple graph's adjacency using a callback.
+ *
+ * @param {boolean}  breakable         - Can we break?
+ * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+ * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+ * @param {Graph}    graph             - Target Graph instance.
+ * @param {callback} function          - Iteration callback.
+ */
+function forEachAdjacency(
+  breakable,
+  assymetric,
+  disconnectedNodes,
+  graph,
+  callback
+) {
+  const iterator = graph._nodes.values();
+
+  const type = graph.type;
+
+  let step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+  while (((step = iterator.next()), step.done !== true)) {
+    let hasEdges = false;
+
+    sourceData = step.value;
+
+    if (type !== 'undirected') {
+      adj = sourceData.out;
+
+      for (neighbor in adj) {
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+
+          hasEdges = true;
+          shouldBreak = callback(
+            sourceData.key,
+            targetData.key,
+            sourceData.attributes,
+            targetData.attributes,
+            edgeData.key,
+            edgeData.attributes,
+            edgeData.undirected
+          );
+
+          if (breakable && shouldBreak) return edgeData;
+
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (type !== 'directed') {
+      adj = sourceData.undirected;
+
+      for (neighbor in adj) {
+        if (assymetric && sourceData.key > neighbor) continue;
+
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+
+          if (targetData.key !== neighbor) targetData = edgeData.source;
+
+          hasEdges = true;
+          shouldBreak = callback(
+            sourceData.key,
+            targetData.key,
+            sourceData.attributes,
+            targetData.attributes,
+            edgeData.key,
+            edgeData.attributes,
+            edgeData.undirected
+          );
+
+          if (breakable && shouldBreak) return edgeData;
+
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (disconnectedNodes && !hasEdges) {
+      shouldBreak = callback(
+        sourceData.key,
+        null,
+        sourceData.attributes,
+        null,
+        null,
+        null,
+        null
+      );
+
+      if (breakable && shouldBreak) return null;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Graphology Serialization Utilities
+ * ===================================
+ *
+ * Collection of functions used by the graph serialization schemes.
+ */
+
+/**
+ * Formats internal node data into a serialized node.
+ *
+ * @param  {any}    key  - The node's key.
+ * @param  {object} data - Internal node's data.
+ * @return {array}       - The serialized node.
+ */
+function serializeNode(key, data) {
+  const serialized = {key};
+
+  if (!isEmpty(data.attributes))
+    serialized.attributes = assign({}, data.attributes);
+
+  return serialized;
+}
+
+/**
+ * Formats internal edge data into a serialized edge.
+ *
+ * @param  {any}    key  - The edge's key.
+ * @param  {object} data - Internal edge's data.
+ * @return {array}       - The serialized edge.
+ */
+function serializeEdge(key, data) {
+  const serialized = {
+    key,
+    source: data.source.key,
+    target: data.target.key
+  };
+
+  if (!isEmpty(data.attributes))
+    serialized.attributes = assign({}, data.attributes);
+
+  if (data.undirected) serialized.undirected = true;
+
+  return serialized;
+}
+
+/**
+ * Checks whether the given value is a serialized node.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+function validateSerializedNode(value) {
+  if (!isPlainObject(value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.'
+    );
+
+  if (!('key' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized node is missing its key.'
+    );
+
+  if (
+    'attributes' in value &&
+    (!isPlainObject(value.attributes) || value.attributes === null)
+  )
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.'
+    );
+}
+
+/**
+ * Checks whether the given value is a serialized edge.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+function validateSerializedEdge(value) {
+  if (!isPlainObject(value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.'
+    );
+
+  if (!('source' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized edge is missing its source.'
+    );
+
+  if (!('target' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized edge is missing its target.'
+    );
+
+  if (
+    'attributes' in value &&
+    (!isPlainObject(value.attributes) || value.attributes === null)
+  )
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.'
+    );
+
+  if ('undirected' in value && typeof value.undirected !== 'boolean')
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.'
+    );
+}
+
+/* eslint no-nested-ternary: 0 */
+
+/**
+ * Constants.
+ */
+const INSTANCE_ID = incrementalIdStartingFromRandomByte();
+
+/**
+ * Enums.
+ */
+const TYPES = new Set(['directed', 'undirected', 'mixed']);
+
+const EMITTER_PROPS = new Set([
+  'domain',
+  '_events',
+  '_eventsCount',
+  '_maxListeners'
+]);
+
+const EDGE_ADD_METHODS = [
+  {
+    name: verb => `${verb}Edge`,
+    generateKey: true
+  },
+  {
+    name: verb => `${verb}DirectedEdge`,
+    generateKey: true,
+    type: 'directed'
+  },
+  {
+    name: verb => `${verb}UndirectedEdge`,
+    generateKey: true,
+    type: 'undirected'
+  },
+  {
+    name: verb => `${verb}EdgeWithKey`
+  },
+  {
+    name: verb => `${verb}DirectedEdgeWithKey`,
+    type: 'directed'
+  },
+  {
+    name: verb => `${verb}UndirectedEdgeWithKey`,
+    type: 'undirected'
+  }
+];
+
+/**
+ * Default options.
+ */
+const DEFAULTS = {
+  allowSelfLoops: true,
+  multi: false,
+  type: 'mixed'
+};
+
+/**
+ * Abstract functions used by the Graph class for various methods.
+ */
+
+/**
+ * Internal method used to add a node to the given graph
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {any}     node            - The node's key.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {NodeData}                - Created node data.
+ */
+function addNode(graph, node, attributes) {
+  if (attributes && !isPlainObject(attributes))
+    throw new InvalidArgumentsGraphError(
+      `Graph.addNode: invalid attributes. Expecting an object but got "${attributes}"`
+    );
+
+  // String coercion
+  node = '' + node;
+  attributes = attributes || {};
+
+  if (graph._nodes.has(node))
+    throw new UsageGraphError(
+      `Graph.addNode: the "${node}" node already exist in the graph.`
+    );
+
+  const data = new graph.NodeDataClass(node, attributes);
+
+  // Adding the node to internal register
+  graph._nodes.set(node, data);
+
+  // Emitting
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes
+  });
+
+  return data;
+}
+
+/**
+ * Same as the above but without sanity checks because we call this in contexts
+ * where necessary checks were already done.
+ */
+function unsafeAddNode(graph, node, attributes) {
+  const data = new graph.NodeDataClass(node, attributes);
+
+  graph._nodes.set(node, data);
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes
+  });
+
+  return data;
+}
+
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+function addEdge(
+  graph,
+  name,
+  mustGenerateKey,
+  undirected,
+  edge,
+  source,
+  target,
+  attributes
+) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead.`
+    );
+
+  if (undirected && graph.type === 'directed')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead.`
+    );
+
+  if (attributes && !isPlainObject(attributes))
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"`
+    );
+
+  // Coercion of source & target:
+  source = '' + source;
+  target = '' + target;
+  attributes = attributes || {};
+
+  if (!graph.allowSelfLoops && source === target)
+    throw new UsageGraphError(
+      `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.`
+    );
+
+  const sourceData = graph._nodes.get(source),
+    targetData = graph._nodes.get(target);
+
+  if (!sourceData)
+    throw new NotFoundGraphError(
+      `Graph.${name}: source node "${source}" not found.`
+    );
+
+  if (!targetData)
+    throw new NotFoundGraphError(
+      `Graph.${name}: target node "${target}" not found.`
+    );
+
+  // Must the graph generate an id for this edge?
+  const eventData = {
+    key: null,
+    undirected,
+    source,
+    target,
+    attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge;
+
+    // Here, we have a key collision
+    if (graph._edges.has(edge))
+      throw new UsageGraphError(
+        `Graph.${name}: the "${edge}" edge already exists in the graph.`
+      );
+  }
+
+  // Here, we might have a source / target collision
+  if (
+    !graph.multi &&
+    (undirected
+      ? typeof sourceData.undirected[target] !== 'undefined'
+      : typeof sourceData.out[target] !== 'undefined')
+  ) {
+    throw new UsageGraphError(
+      `Graph.${name}: an edge linking "${source}" to "${target}" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option.`
+    );
+  }
+
+  // Storing some data
+  const edgeData = new EdgeData(
+    undirected,
+    edge,
+    sourceData,
+    targetData,
+    attributes
+  );
+
+  // Adding the edge to the internal register
+  graph._edges.set(edge, edgeData);
+
+  // Incrementing node degree counters
+  const isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  }
+
+  // Updating relevant index
+  if (graph.multi) edgeData.attachMulti();
+  else edgeData.attach();
+
+  if (undirected) graph._undirectedSize++;
+  else graph._directedSize++;
+
+  // Emitting
+  eventData.key = edge;
+
+  graph.emit('edgeAdded', eventData);
+
+  return edge;
+}
+
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @param  {boolean} [asUpdater]       - Are we updating or merging?
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+function mergeEdge(
+  graph,
+  name,
+  mustGenerateKey,
+  undirected,
+  edge,
+  source,
+  target,
+  attributes,
+  asUpdater
+) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead.`
+    );
+
+  if (undirected && graph.type === 'directed')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead.`
+    );
+
+  if (attributes) {
+    if (asUpdater) {
+      if (typeof attributes !== 'function')
+        throw new InvalidArgumentsGraphError(
+          `Graph.${name}: invalid updater function. Expecting a function but got "${attributes}"`
+        );
+    } else {
+      if (!isPlainObject(attributes))
+        throw new InvalidArgumentsGraphError(
+          `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"`
+        );
+    }
+  }
+
+  // Coercion of source & target:
+  source = '' + source;
+  target = '' + target;
+
+  let updater;
+
+  if (asUpdater) {
+    updater = attributes;
+    attributes = undefined;
+  }
+
+  if (!graph.allowSelfLoops && source === target)
+    throw new UsageGraphError(
+      `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.`
+    );
+
+  let sourceData = graph._nodes.get(source);
+  let targetData = graph._nodes.get(target);
+  let edgeData;
+
+  // Do we need to handle duplicate?
+  let alreadyExistingEdgeData;
+
+  if (!mustGenerateKey) {
+    edgeData = graph._edges.get(edge);
+
+    if (edgeData) {
+      // Here, we need to ensure, if the user gave a key, that source & target
+      // are consistent
+      if (edgeData.source.key !== source || edgeData.target.key !== target) {
+        // If source or target inconsistent
+        if (
+          !undirected ||
+          edgeData.source.key !== target ||
+          edgeData.target.key !== source
+        ) {
+          // If directed, or source/target aren't flipped
+          throw new UsageGraphError(
+            `Graph.${name}: inconsistency detected when attempting to merge the "${edge}" edge with "${source}" source & "${target}" target vs. ("${edgeData.source.key}", "${edgeData.target.key}").`
+          );
+        }
+      }
+
+      alreadyExistingEdgeData = edgeData;
+    }
+  }
+
+  // Here, we might have a source / target collision
+  if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+    alreadyExistingEdgeData = undirected
+      ? sourceData.undirected[target]
+      : sourceData.out[target];
+  }
+
+  // Handling duplicates
+  if (alreadyExistingEdgeData) {
+    const info = [alreadyExistingEdgeData.key, false, false, false];
+
+    // We can skip the attribute merging part if the user did not provide them
+    if (asUpdater ? !updater : !attributes) return info;
+
+    // Updating the attributes
+    if (asUpdater) {
+      const oldAttributes = alreadyExistingEdgeData.attributes;
+      alreadyExistingEdgeData.attributes = updater(oldAttributes);
+
+      graph.emit('edgeAttributesUpdated', {
+        type: 'replace',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes
+      });
+    }
+
+    // Merging the attributes
+    else {
+      assign(alreadyExistingEdgeData.attributes, attributes);
+
+      graph.emit('edgeAttributesUpdated', {
+        type: 'merge',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes,
+        data: attributes
+      });
+    }
+
+    return info;
+  }
+
+  attributes = attributes || {};
+
+  if (asUpdater && updater) attributes = updater(attributes);
+
+  // Must the graph generate an id for this edge?
+  const eventData = {
+    key: null,
+    undirected,
+    source,
+    target,
+    attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge;
+
+    // Here, we have a key collision
+    if (graph._edges.has(edge))
+      throw new UsageGraphError(
+        `Graph.${name}: the "${edge}" edge already exists in the graph.`
+      );
+  }
+
+  let sourceWasAdded = false;
+  let targetWasAdded = false;
+
+  if (!sourceData) {
+    sourceData = unsafeAddNode(graph, source, {});
+    sourceWasAdded = true;
+
+    if (source === target) {
+      targetData = sourceData;
+      targetWasAdded = true;
+    }
+  }
+  if (!targetData) {
+    targetData = unsafeAddNode(graph, target, {});
+    targetWasAdded = true;
+  }
+
+  // Storing some data
+  edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes);
+
+  // Adding the edge to the internal register
+  graph._edges.set(edge, edgeData);
+
+  // Incrementing node degree counters
+  const isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  }
+
+  // Updating relevant index
+  if (graph.multi) edgeData.attachMulti();
+  else edgeData.attach();
+
+  if (undirected) graph._undirectedSize++;
+  else graph._directedSize++;
+
+  // Emitting
+  eventData.key = edge;
+
+  graph.emit('edgeAdded', eventData);
+
+  return [edge, true, sourceWasAdded, targetWasAdded];
+}
+
+/**
+ * Internal method used to drop an edge.
+ *
+ * @param  {Graph}    graph    - Target graph.
+ * @param  {EdgeData} edgeData - Data of the edge to drop.
+ */
+function dropEdgeFromData(graph, edgeData) {
+  // Dropping the edge from the register
+  graph._edges.delete(edgeData.key);
+
+  // Updating related degrees
+  const {source: sourceData, target: targetData, attributes} = edgeData;
+
+  const undirected = edgeData.undirected;
+
+  const isSelfLoop = sourceData === targetData;
+
+  if (undirected) {
+    sourceData.undirectedDegree--;
+    targetData.undirectedDegree--;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount--;
+  } else {
+    sourceData.outDegree--;
+    targetData.inDegree--;
+
+    if (isSelfLoop) graph._directedSelfLoopCount--;
+  }
+
+  // Clearing index
+  if (graph.multi) edgeData.detachMulti();
+  else edgeData.detach();
+
+  if (undirected) graph._undirectedSize--;
+  else graph._directedSize--;
+
+  // Emitting
+  graph.emit('edgeDropped', {
+    key: edgeData.key,
+    attributes,
+    source: sourceData.key,
+    target: targetData.key,
+    undirected
+  });
+}
+
+/**
+ * Graph class
+ *
+ * @constructor
+ * @param  {object}  [options] - Options:
+ * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+ * @param  {string}    [type]           - Type of the graph.
+ * @param  {boolean}   [map]            - Allow references as keys?
+ * @param  {boolean}   [multi]          - Allow parallel edges?
+ *
+ * @throws {Error} - Will throw if the arguments are not valid.
+ */
+class Graph extends EventEmitter {
+  constructor(options) {
+    super();
+
+    //-- Solving options
+    options = assign({}, DEFAULTS, options);
+
+    // Enforcing options validity
+    if (typeof options.multi !== 'boolean')
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'multi' option. Expecting a boolean but got "${options.multi}".`
+      );
+
+    if (!TYPES.has(options.type))
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'type' option. Should be one of "mixed", "directed" or "undirected" but got "${options.type}".`
+      );
+
+    if (typeof options.allowSelfLoops !== 'boolean')
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got "${options.allowSelfLoops}".`
+      );
+
+    //-- Private properties
+
+    // Utilities
+    const NodeDataClass =
+      options.type === 'mixed'
+        ? MixedNodeData
+        : options.type === 'directed'
+        ? DirectedNodeData
+        : UndirectedNodeData;
+
+    privateProperty(this, 'NodeDataClass', NodeDataClass);
+
+    // Internal edge key generator
+
+    // NOTE: this internal generator produce keys that are strings
+    // composed of a weird prefix, an incremental instance id starting from
+    // a random byte and finally an internal instance incremental id.
+    // All this to avoid intra-frame and cross-frame adversarial inputs
+    // that can force a single #.addEdge call to degenerate into a O(n)
+    // available key search loop.
+
+    // It also ensures that automatically generated edge keys are unlikely
+    // to produce collisions with arbitrary keys given by users.
+    const instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+    let edgeId = 0;
+
+    const edgeKeyGenerator = () => {
+      let availableEdgeKey;
+
+      do {
+        availableEdgeKey = instancePrefix + edgeId++;
+      } while (this._edges.has(availableEdgeKey));
+
+      return availableEdgeKey;
+    };
+
+    // Indexes
+    privateProperty(this, '_attributes', {});
+    privateProperty(this, '_nodes', new Map());
+    privateProperty(this, '_edges', new Map());
+    privateProperty(this, '_directedSize', 0);
+    privateProperty(this, '_undirectedSize', 0);
+    privateProperty(this, '_directedSelfLoopCount', 0);
+    privateProperty(this, '_undirectedSelfLoopCount', 0);
+    privateProperty(this, '_edgeKeyGenerator', edgeKeyGenerator);
+
+    // Options
+    privateProperty(this, '_options', options);
+
+    // Emitter properties
+    EMITTER_PROPS.forEach(prop => privateProperty(this, prop, this[prop]));
+
+    //-- Properties readers
+    readOnlyProperty(this, 'order', () => this._nodes.size);
+    readOnlyProperty(this, 'size', () => this._edges.size);
+    readOnlyProperty(this, 'directedSize', () => this._directedSize);
+    readOnlyProperty(this, 'undirectedSize', () => this._undirectedSize);
+    readOnlyProperty(
+      this,
+      'selfLoopCount',
+      () => this._directedSelfLoopCount + this._undirectedSelfLoopCount
+    );
+    readOnlyProperty(
+      this,
+      'directedSelfLoopCount',
+      () => this._directedSelfLoopCount
+    );
+    readOnlyProperty(
+      this,
+      'undirectedSelfLoopCount',
+      () => this._undirectedSelfLoopCount
+    );
+    readOnlyProperty(this, 'multi', this._options.multi);
+    readOnlyProperty(this, 'type', this._options.type);
+    readOnlyProperty(this, 'allowSelfLoops', this._options.allowSelfLoops);
+    readOnlyProperty(this, 'implementation', () => 'graphology');
+  }
+
+  _resetInstanceCounters() {
+    this._directedSize = 0;
+    this._undirectedSize = 0;
+    this._directedSelfLoopCount = 0;
+    this._undirectedSelfLoopCount = 0;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Read
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning whether the given node is found in the graph.
+   *
+   * @param  {any}     node - The node.
+   * @return {boolean}
+   */
+  hasNode(node) {
+    return this._nodes.has('' + node);
+  }
+
+  /**
+   * Method returning whether the given directed edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasDirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'undirected') return false;
+
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      const edgeData = this._edges.get(edge);
+
+      return !!edgeData && !edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      const edges = nodeData.out[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning whether the given undirected edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasUndirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'directed') return false;
+
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      const edgeData = this._edges.get(edge);
+
+      return !!edgeData && edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      const edges = nodeData.undirected[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning whether the given edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasEdge(source, target) {
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      return this._edges.has(edge);
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      let edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+
+      if (!edges)
+        edges =
+          typeof nodeData.undirected !== 'undefined' &&
+          nodeData.undirected[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning the edge matching source & target in a directed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  directedEdge(source, target) {
+    if (this.type === 'undirected') return;
+
+    source = '' + source;
+    target = '' + target;
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.'
+      );
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.directedEdge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.directedEdge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData = (sourceData.out && sourceData.out[target]) || undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning the edge matching source & target in a undirected fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  undirectedEdge(source, target) {
+    if (this.type === 'directed') return;
+
+    source = '' + source;
+    target = '' + target;
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.'
+      );
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedEdge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.undirectedEdge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData =
+      (sourceData.undirected && sourceData.undirected[target]) || undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning the edge matching source & target in a mixed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  edge(source, target) {
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.'
+      );
+
+    source = '' + source;
+    target = '' + target;
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.edge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.edge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData =
+      (sourceData.out && sourceData.out[target]) ||
+      (sourceData.undirected && sourceData.undirected[target]) ||
+      undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning whether two nodes are directed neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areDirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areDirectedNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.in || neighbor in nodeData.out;
+  }
+
+  /**
+   * Method returning whether two nodes are out neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areOutNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areOutNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.out;
+  }
+
+  /**
+   * Method returning whether two nodes are in neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areInNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areInNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.in;
+  }
+
+  /**
+   * Method returning whether two nodes are undirected neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areUndirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areUndirectedNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return false;
+
+    return neighbor in nodeData.undirected;
+  }
+
+  /**
+   * Method returning whether two nodes are neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.in || neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether two nodes are inbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areInboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areInboundNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.in) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether two nodes are outbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areOutboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areOutboundNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning the given node's in degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.inDegree;
+  }
+
+  /**
+   * Method returning the given node's out degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.outDegree;
+  }
+
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  directedDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.directedDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.inDegree + nodeData.outDegree;
+  }
+
+  /**
+   * Method returning the given node's undirected degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  undirectedDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return 0;
+
+    return nodeData.undirectedDegree;
+  }
+
+  /**
+   * Method returning the given node's inbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inboundDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inboundDegree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's outbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outboundDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outboundDegree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  degree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.degree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's in degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.in[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.inDegree - loops;
+  }
+
+  /**
+   * Method returning the given node's out degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.out[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.outDegree - loops;
+  }
+
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  directedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.directedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.out[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.inDegree + nodeData.outDegree - loops * 2;
+  }
+
+  /**
+   * Method returning the given node's undirected degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  undirectedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return 0;
+
+    const self = nodeData.undirected[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.undirectedDegree - loops * 2;
+  }
+
+  /**
+   * Method returning the given node's inbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+
+      self = nodeData.out[node];
+      loops += self ? (this.multi ? self.size : 1) : 0;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given node's outbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+
+      self = nodeData.in[node];
+      loops += self ? (this.multi ? self.size : 1) : 0;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  degreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.degreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+
+      self = nodeData.out[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given edge's source.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's source.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  source(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.source: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source.key;
+  }
+
+  /**
+   * Method returning the given edge's target.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's target.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  target(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.target: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.target.key;
+  }
+
+  /**
+   * Method returning the given edge's extremities.
+   *
+   * @param  {any}   edge - The edge's key.
+   * @return {array}      - The edge's extremities.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  extremities(edge) {
+    edge = '' + edge;
+
+    const edgeData = this._edges.get(edge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.extremities: could not find the "${edge}" edge in the graph.`
+      );
+
+    return [edgeData.source.key, edgeData.target.key];
+  }
+
+  /**
+   * Given a node & an edge, returns the other extremity of the edge.
+   *
+   * @param  {any}   node - The node's key.
+   * @param  {any}   edge - The edge's key.
+   * @return {any}        - The related node.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph or if the
+   *                   edge & node are not related.
+   */
+  opposite(node, edge) {
+    node = '' + node;
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.opposite: could not find the "${edge}" edge in the graph.`
+      );
+
+    const source = data.source.key;
+    const target = data.target.key;
+
+    if (node === source) return target;
+    if (node === target) return source;
+
+    throw new NotFoundGraphError(
+      `Graph.opposite: the "${node}" node is not attached to the "${edge}" edge (${source}, ${target}).`
+    );
+  }
+
+  /**
+   * Returns whether the given edge has the given node as extremity.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @param  {any}     node - The node's key.
+   * @return {boolean}      - The related node.
+   *
+   * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+   */
+  hasExtremity(edge, node) {
+    edge = '' + edge;
+    node = '' + node;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.hasExtremity: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source.key === node || data.target.key === node;
+  }
+
+  /**
+   * Method returning whether the given edge is undirected.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isUndirected(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isUndirected: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.undirected;
+  }
+
+  /**
+   * Method returning whether the given edge is directed.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isDirected(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isDirected: could not find the "${edge}" edge in the graph.`
+      );
+
+    return !data.undirected;
+  }
+
+  /**
+   * Method returning whether the given edge is a self loop.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isSelfLoop(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isSelfLoop: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source === data.target;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Mutation
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to add a node to the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   *
+   * @throws {Error} - Will throw if the given node already exist.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   */
+  addNode(node, attributes) {
+    const nodeData = addNode(this, node, attributes);
+
+    return nodeData.key;
+  }
+
+  /**
+   * Method used to merge a node into the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   */
+  mergeNode(node, attributes) {
+    if (attributes && !isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.mergeNode: invalid attributes. Expecting an object but got "${attributes}"`
+      );
+
+    // String coercion
+    node = '' + node;
+    attributes = attributes || {};
+
+    // If the node already exists, we merge the attributes
+    let data = this._nodes.get(node);
+
+    if (data) {
+      if (attributes) {
+        assign(data.attributes, attributes);
+
+        this.emit('nodeAttributesUpdated', {
+          type: 'merge',
+          key: node,
+          attributes: data.attributes,
+          data: attributes
+        });
+      }
+      return [node, false];
+    }
+
+    data = new this.NodeDataClass(node, attributes);
+
+    // Adding the node to internal register
+    this._nodes.set(node, data);
+
+    // Emitting
+    this.emit('nodeAdded', {
+      key: node,
+      attributes
+    });
+
+    return [node, true];
+  }
+
+  /**
+   * Method used to add a node if it does not exist in the graph or else to
+   * update its attributes using a function.
+   *
+   * @param  {any}      node      - The node.
+   * @param  {function} [updater] - Optional updater function.
+   * @return {any}                - The node.
+   */
+  updateNode(node, updater) {
+    if (updater && typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.updateNode: invalid updater function. Expecting a function but got "${updater}"`
+      );
+
+    // String coercion
+    node = '' + node;
+
+    // If the node already exists, we update the attributes
+    let data = this._nodes.get(node);
+
+    if (data) {
+      if (updater) {
+        const oldAttributes = data.attributes;
+        data.attributes = updater(oldAttributes);
+
+        this.emit('nodeAttributesUpdated', {
+          type: 'replace',
+          key: node,
+          attributes: data.attributes
+        });
+      }
+      return [node, false];
+    }
+
+    const attributes = updater ? updater({}) : {};
+
+    data = new this.NodeDataClass(node, attributes);
+
+    // Adding the node to internal register
+    this._nodes.set(node, data);
+
+    // Emitting
+    this.emit('nodeAdded', {
+      key: node,
+      attributes
+    });
+
+    return [node, true];
+  }
+
+  /**
+   * Method used to drop a single node & all its attached edges from the graph.
+   *
+   * @param  {any}    node - The node.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the node doesn't exist.
+   */
+  dropNode(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.dropNode: could not find the "${node}" node in the graph.`
+      );
+
+    let edgeData;
+
+    // Removing attached edges
+    // NOTE: we could be faster here, but this is such a pain to maintain
+    if (this.type !== 'undirected') {
+      for (const neighbor in nodeData.out) {
+        edgeData = nodeData.out[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+
+      for (const neighbor in nodeData.in) {
+        edgeData = nodeData.in[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (this.type !== 'directed') {
+      for (const neighbor in nodeData.undirected) {
+        edgeData = nodeData.undirected[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    // Dropping the node from the register
+    this._nodes.delete(node);
+
+    // Emitting
+    this.emit('nodeDropped', {
+      key: node,
+      attributes: nodeData.attributes
+    });
+  }
+
+  /**
+   * Method used to drop a single edge from the graph.
+   *
+   * Arity 1:
+   * @param  {any}    edge - The edge.
+   *
+   * Arity 2:
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropEdge(edge) {
+    let edgeData;
+
+    if (arguments.length > 1) {
+      const source = '' + arguments[0];
+      const target = '' + arguments[1];
+
+      edgeData = getMatchingEdge(this, source, target, this.type);
+
+      if (!edgeData)
+        throw new NotFoundGraphError(
+          `Graph.dropEdge: could not find the "${source}" -> "${target}" edge in the graph.`
+        );
+    } else {
+      edge = '' + edge;
+
+      edgeData = this._edges.get(edge);
+
+      if (!edgeData)
+        throw new NotFoundGraphError(
+          `Graph.dropEdge: could not find the "${edge}" edge in the graph.`
+        );
+    }
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to drop a single directed edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropDirectedEdge(source, target) {
+    if (arguments.length < 2)
+      throw new UsageGraphError(
+        'Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.'
+      );
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.'
+      );
+
+    source = '' + source;
+    target = '' + target;
+
+    const edgeData = getMatchingEdge(this, source, target, 'directed');
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.dropDirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.`
+      );
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to drop a single undirected edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropUndirectedEdge(source, target) {
+    if (arguments.length < 2)
+      throw new UsageGraphError(
+        'Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.'
+      );
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.'
+      );
+
+    const edgeData = getMatchingEdge(this, source, target, 'undirected');
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.dropUndirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.`
+      );
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to remove every edge & every node from the graph.
+   *
+   * @return {Graph}
+   */
+  clear() {
+    // Clearing edges
+    this._edges.clear();
+
+    // Clearing nodes
+    this._nodes.clear();
+
+    // Reset counters
+    this._resetInstanceCounters();
+
+    // Emitting
+    this.emit('cleared');
+  }
+
+  /**
+   * Method used to remove every edge from the graph.
+   *
+   * @return {Graph}
+   */
+  clearEdges() {
+    // Clearing structure index
+    const iterator = this._nodes.values();
+
+    let step;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      step.value.clear();
+    }
+
+    // Clearing edges
+    this._edges.clear();
+
+    // Reset counters
+    this._resetInstanceCounters();
+
+    // Emitting
+    this.emit('edgesCleared');
+  }
+
+  /**---------------------------------------------------------------------------
+   * Attributes-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning the desired graph's attribute.
+   *
+   * @param  {string} name - Name of the attribute.
+   * @return {any}
+   */
+  getAttribute(name) {
+    return this._attributes[name];
+  }
+
+  /**
+   * Method returning the graph's attributes.
+   *
+   * @return {object}
+   */
+  getAttributes() {
+    return this._attributes;
+  }
+
+  /**
+   * Method returning whether the graph has the desired attribute.
+   *
+   * @param  {string}  name - Name of the attribute.
+   * @return {boolean}
+   */
+  hasAttribute(name) {
+    return this._attributes.hasOwnProperty(name);
+  }
+
+  /**
+   * Method setting a value for the desired graph's attribute.
+   *
+   * @param  {string}  name  - Name of the attribute.
+   * @param  {any}     value - Value for the attribute.
+   * @return {Graph}
+   */
+  setAttribute(name, value) {
+    this._attributes[name] = value;
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method using a function to update the desired graph's attribute's value.
+   *
+   * @param  {string}   name    - Name of the attribute.
+   * @param  {function} updater - Function use to update the attribute's value.
+   * @return {Graph}
+   */
+  updateAttribute(name, updater) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateAttribute: updater should be a function.'
+      );
+
+    const value = this._attributes[name];
+
+    this._attributes[name] = updater(value);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method removing the desired graph's attribute.
+   *
+   * @param  {string} name  - Name of the attribute.
+   * @return {Graph}
+   */
+  removeAttribute(name) {
+    delete this._attributes[name];
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'remove',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method replacing the graph's attributes.
+   *
+   * @param  {object} attributes - New attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  replaceAttributes(attributes) {
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        'Graph.replaceAttributes: provided attributes are not a plain object.'
+      );
+
+    this._attributes = attributes;
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'replace',
+      attributes: this._attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method merging the graph's attributes.
+   *
+   * @param  {object} attributes - Attributes to merge.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  mergeAttributes(attributes) {
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        'Graph.mergeAttributes: provided attributes are not a plain object.'
+      );
+
+    assign(this._attributes, attributes);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'merge',
+      attributes: this._attributes,
+      data: attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method updating the graph's attributes.
+   *
+   * @param  {function} updater - Function used to update the attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given updater is not a function.
+   */
+  updateAttributes(updater) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateAttributes: provided updater is not a function.'
+      );
+
+    this._attributes = updater(this._attributes);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'update',
+      attributes: this._attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method used to update each node's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  updateEachNodeAttributes(updater, hints) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachNodeAttributes: expecting an updater function.'
+      );
+
+    if (hints && !validateHints(hints))
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+    }
+
+    this.emit('eachNodeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+
+  /**
+   * Method used to update each edge's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  updateEachEdgeAttributes(updater, hints) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachEdgeAttributes: expecting an updater function.'
+      );
+
+    if (hints && !validateHints(hints))
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}'
+      );
+
+    const iterator = this._edges.values();
+
+    let step, edgeData, sourceData, targetData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      edgeData = step.value;
+      sourceData = edgeData.source;
+      targetData = edgeData.target;
+
+      edgeData.attributes = updater(
+        edgeData.key,
+        edgeData.attributes,
+        sourceData.key,
+        targetData.key,
+        sourceData.attributes,
+        targetData.attributes,
+        edgeData.undirected
+      );
+    }
+
+    this.emit('eachEdgeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+
+  /**---------------------------------------------------------------------------
+   * Iteration-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method iterating over the graph's adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  forEachAdjacencyEntry(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAdjacencyEntry: expecting a callback.'
+      );
+
+    forEachAdjacency(false, false, false, this, callback);
+  }
+  forEachAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.'
+      );
+
+    forEachAdjacency(false, false, true, this, callback);
+  }
+
+  /**
+   * Method iterating over the graph's assymetric adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  forEachAssymetricAdjacencyEntry(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAssymetricAdjacencyEntry: expecting a callback.'
+      );
+
+    forEachAdjacency(false, true, false, this, callback);
+  }
+  forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.'
+      );
+
+    forEachAdjacency(false, true, true, this, callback);
+  }
+
+  /**
+   * Method returning the list of the graph's nodes.
+   *
+   * @return {array} - The nodes.
+   */
+  nodes() {
+    if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+
+    return take(this._nodes.keys(), this._nodes.size);
+  }
+
+  /**
+   * Method iterating over the graph's nodes using the given callback.
+   *
+   * @param  {function}  callback - Callback (key, attributes, index).
+   */
+  forEachNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      callback(nodeData.key, nodeData.attributes);
+    }
+  }
+
+  /**
+   * Method iterating attempting to find a node matching the given predicate
+   * function.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  findNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.findNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+    }
+
+    return;
+  }
+
+  /**
+   * Method mapping nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  mapNodes(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.mapNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    const result = new Array(this.order);
+    let i = 0;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      result[i++] = callback(nodeData.key, nodeData.attributes);
+    }
+
+    return result;
+  }
+
+  /**
+   * Method returning whether some node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  someNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.someNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes)) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether all node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  everyNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.everyNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (!callback(nodeData.key, nodeData.attributes)) return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Method filtering nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  filterNodes(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.filterNodes: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    const result = [];
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes))
+        result.push(nodeData.key);
+    }
+
+    return result;
+  }
+
+  /**
+   * Method reducing nodes.
+   *
+   * @param  {function}  callback - Callback (accumulator, key, attributes).
+   */
+  reduceNodes(callback, initialValue) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.reduceNodes: expecting a callback.'
+      );
+
+    if (arguments.length < 2)
+      throw new InvalidArgumentsGraphError(
+        'Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.'
+      );
+
+    let accumulator = initialValue;
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+    }
+
+    return accumulator;
+  }
+
+  /**
+   * Method returning an iterator over the graph's node entries.
+   *
+   * @return {Iterator}
+   */
+  nodeEntries() {
+    const iterator = this._nodes.values();
+
+    return new Iterator(() => {
+      const step = iterator.next();
+
+      if (step.done) return step;
+
+      const data = step.value;
+
+      return {
+        value: {node: data.key, attributes: data.attributes},
+        done: false
+      };
+    });
+  }
+
+  /**---------------------------------------------------------------------------
+   * Serialization
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to export the whole graph.
+   *
+   * @return {object} - The serialized graph.
+   */
+  export() {
+    const nodes = new Array(this._nodes.size);
+
+    let i = 0;
+
+    this._nodes.forEach((data, key) => {
+      nodes[i++] = serializeNode(key, data);
+    });
+
+    const edges = new Array(this._edges.size);
+
+    i = 0;
+
+    this._edges.forEach((data, key) => {
+      edges[i++] = serializeEdge(key, data);
+    });
+
+    return {
+      options: {
+        type: this.type,
+        multi: this.multi,
+        allowSelfLoops: this.allowSelfLoops
+      },
+      attributes: this.getAttributes(),
+      nodes,
+      edges
+    };
+  }
+
+  /**
+   * Method used to import a serialized graph.
+   *
+   * @param  {object|Graph} data  - The serialized graph.
+   * @param  {boolean}      merge - Whether to merge data.
+   * @return {Graph}              - Returns itself for chaining.
+   */
+  import(data, merge = false) {
+    // Importing a Graph instance directly
+    if (isGraph(data)) {
+      // Nodes
+      data.forEachNode((n, a) => {
+        if (merge) this.mergeNode(n, a);
+        else this.addNode(n, a);
+      });
+
+      // Edges
+      data.forEachEdge((e, a, s, t, _sa, _ta, u) => {
+        if (merge) {
+          if (u) this.mergeUndirectedEdgeWithKey(e, s, t, a);
+          else this.mergeDirectedEdgeWithKey(e, s, t, a);
+        } else {
+          if (u) this.addUndirectedEdgeWithKey(e, s, t, a);
+          else this.addDirectedEdgeWithKey(e, s, t, a);
+        }
+      });
+
+      return this;
+    }
+
+    // Importing a serialized graph
+    if (!isPlainObject(data))
+      throw new InvalidArgumentsGraphError(
+        'Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.'
+      );
+
+    if (data.attributes) {
+      if (!isPlainObject(data.attributes))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid attributes. Expecting a plain object.'
+        );
+
+      if (merge) this.mergeAttributes(data.attributes);
+      else this.replaceAttributes(data.attributes);
+    }
+
+    let i, l, list, node, edge;
+
+    if (data.nodes) {
+      list = data.nodes;
+
+      if (!Array.isArray(list))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid nodes. Expecting an array.'
+        );
+
+      for (i = 0, l = list.length; i < l; i++) {
+        node = list[i];
+
+        // Validating
+        validateSerializedNode(node);
+
+        // Adding the node
+        const {key, attributes} = node;
+
+        if (merge) this.mergeNode(key, attributes);
+        else this.addNode(key, attributes);
+      }
+    }
+
+    if (data.edges) {
+      list = data.edges;
+
+      if (!Array.isArray(list))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid edges. Expecting an array.'
+        );
+
+      for (i = 0, l = list.length; i < l; i++) {
+        edge = list[i];
+
+        // Validating
+        validateSerializedEdge(edge);
+
+        // Adding the edge
+        const {source, target, attributes, undirected = false} = edge;
+
+        let method;
+
+        if ('key' in edge) {
+          method = merge
+            ? undirected
+              ? this.mergeUndirectedEdgeWithKey
+              : this.mergeDirectedEdgeWithKey
+            : undirected
+            ? this.addUndirectedEdgeWithKey
+            : this.addDirectedEdgeWithKey;
+
+          method.call(this, edge.key, source, target, attributes);
+        } else {
+          method = merge
+            ? undirected
+              ? this.mergeUndirectedEdge
+              : this.mergeDirectedEdge
+            : undirected
+            ? this.addUndirectedEdge
+            : this.addDirectedEdge;
+
+          method.call(this, source, target, attributes);
+        }
+      }
+    }
+
+    return this;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Utils
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning a null copy of the graph, i.e. a graph without nodes
+   * & edges but with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The null copy.
+   */
+  nullCopy(options) {
+    const graph = new Graph(assign({}, this._options, options));
+    graph.replaceAttributes(assign({}, this.getAttributes()));
+    return graph;
+  }
+
+  /**
+   * Method returning an empty copy of the graph, i.e. a graph without edges but
+   * with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The empty copy.
+   */
+  emptyCopy(options) {
+    const graph = this.nullCopy(options);
+
+    this._nodes.forEach((nodeData, key) => {
+      const attributes = assign({}, nodeData.attributes);
+
+      // NOTE: no need to emit events since user cannot access the instance yet
+      nodeData = new graph.NodeDataClass(key, attributes);
+      graph._nodes.set(key, nodeData);
+    });
+
+    return graph;
+  }
+
+  /**
+   * Method returning an exact copy of the graph.
+   *
+   * @param  {object} options - Upgrade options.
+   * @return {Graph}          - The copy.
+   */
+  copy(options) {
+    options = options || {};
+
+    if (
+      typeof options.type === 'string' &&
+      options.type !== this.type &&
+      options.type !== 'mixed'
+    )
+      throw new UsageGraphError(
+        `Graph.copy: cannot create an incompatible copy from "${this.type}" type to "${options.type}" because this would mean losing information about the current graph.`
+      );
+
+    if (
+      typeof options.multi === 'boolean' &&
+      options.multi !== this.multi &&
+      options.multi !== true
+    )
+      throw new UsageGraphError(
+        'Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.'
+      );
+
+    if (
+      typeof options.allowSelfLoops === 'boolean' &&
+      options.allowSelfLoops !== this.allowSelfLoops &&
+      options.allowSelfLoops !== true
+    )
+      throw new UsageGraphError(
+        'Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.'
+      );
+
+    const graph = this.emptyCopy(options);
+
+    const iterator = this._edges.values();
+
+    let step, edgeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      edgeData = step.value;
+
+      // NOTE: no need to emit events since user cannot access the instance yet
+      addEdge(
+        graph,
+        'copy',
+        false,
+        edgeData.undirected,
+        edgeData.key,
+        edgeData.source.key,
+        edgeData.target.key,
+        assign({}, edgeData.attributes)
+      );
+    }
+
+    return graph;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Known methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used by JavaScript to perform JSON serialization.
+   *
+   * @return {object} - The serialized graph.
+   */
+  toJSON() {
+    return this.export();
+  }
+
+  /**
+   * Method returning [object Graph].
+   */
+  toString() {
+    return '[object Graph]';
+  }
+
+  /**
+   * Method used internally by node's console to display a custom object.
+   *
+   * @return {object} - Formatted object representation of the graph.
+   */
+  inspect() {
+    const nodes = {};
+    this._nodes.forEach((data, key) => {
+      nodes[key] = data.attributes;
+    });
+
+    const edges = {},
+      multiIndex = {};
+
+    this._edges.forEach((data, key) => {
+      const direction = data.undirected ? '--' : '->';
+
+      let label = '';
+
+      let source = data.source.key;
+      let target = data.target.key;
+      let tmp;
+
+      if (data.undirected && source > target) {
+        tmp = source;
+        source = target;
+        target = tmp;
+      }
+
+      const desc = `(${source})${direction}(${target})`;
+
+      if (!key.startsWith('geid_')) {
+        label += `[${key}]: `;
+      } else if (this.multi) {
+        if (typeof multiIndex[desc] === 'undefined') {
+          multiIndex[desc] = 0;
+        } else {
+          multiIndex[desc]++;
+        }
+
+        label += `${multiIndex[desc]}. `;
+      }
+
+      label += desc;
+
+      edges[label] = data.attributes;
+    });
+
+    const dummy = {};
+
+    for (const k in this) {
+      if (
+        this.hasOwnProperty(k) &&
+        !EMITTER_PROPS.has(k) &&
+        typeof this[k] !== 'function' &&
+        typeof k !== 'symbol'
+      )
+        dummy[k] = this[k];
+    }
+
+    dummy.attributes = this._attributes;
+    dummy.nodes = nodes;
+    dummy.edges = edges;
+
+    privateProperty(dummy, 'constructor', this.constructor);
+
+    return dummy;
+  }
+}
+
+/**
+ * Attaching methods to the prototype.
+ *
+ * Here, we are attaching a wide variety of methods to the Graph class'
+ * prototype when those are very numerous and when their creation is
+ * abstracted.
+ */
+
+/**
+ * Attaching custom inspect method for node >= 10.
+ */
+if (typeof Symbol !== 'undefined')
+  Graph.prototype[Symbol.for('nodejs.util.inspect.custom')] =
+    Graph.prototype.inspect;
+
+/**
+ * Related to edge addition.
+ */
+EDGE_ADD_METHODS.forEach(method => {
+  ['add', 'merge', 'update'].forEach(verb => {
+    const name = method.name(verb);
+    const fn = verb === 'add' ? addEdge : mergeEdge;
+
+    if (method.generateKey) {
+      Graph.prototype[name] = function (source, target, attributes) {
+        return fn(
+          this,
+          name,
+          true,
+          (method.type || this.type) === 'undirected',
+          null,
+          source,
+          target,
+          attributes,
+          verb === 'update'
+        );
+      };
+    } else {
+      Graph.prototype[name] = function (edge, source, target, attributes) {
+        return fn(
+          this,
+          name,
+          false,
+          (method.type || this.type) === 'undirected',
+          edge,
+          source,
+          target,
+          attributes,
+          verb === 'update'
+        );
+      };
+    }
+  });
+});
+
+/**
+ * Attributes-related.
+ */
+attachNodeAttributesMethods(Graph);
+attachEdgeAttributesMethods(Graph);
+
+/**
+ * Edge iteration-related.
+ */
+attachEdgeIterationMethods(Graph);
+
+/**
+ * Neighbor iteration-related.
+ */
+attachNeighborIterationMethods(Graph);
+
+/**
+ * Graphology Helper Classes
+ * ==========================
+ *
+ * Building some higher-order classes instantiating the graph with
+ * predefinite options.
+ */
+
+/**
+ * Alternative constructors.
+ */
+class DirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'directed'}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== false)
+      throw new InvalidArgumentsGraphError(
+        'DirectedGraph.from: inconsistent indication that the graph should be multi in given options!'
+      );
+
+    if (finalOptions.type !== 'directed')
+      throw new InvalidArgumentsGraphError(
+        'DirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class UndirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'undirected'}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== false)
+      throw new InvalidArgumentsGraphError(
+        'UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!'
+      );
+
+    if (finalOptions.type !== 'undirected')
+      throw new InvalidArgumentsGraphError(
+        'UndirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiDirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'directed', multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    if (finalOptions.type !== 'directed')
+      throw new InvalidArgumentsGraphError(
+        'MultiDirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiUndirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'undirected', multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    if (finalOptions.type !== 'undirected')
+      throw new InvalidArgumentsGraphError(
+        'MultiUndirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+
+/**
+ * Attaching static #.from method to each of the constructors.
+ */
+function attachStaticFromMethod(Class) {
+  /**
+   * Builds a graph from serialized data or another graph's data.
+   *
+   * @param  {Graph|SerializedGraph} data      - Hydratation data.
+   * @param  {object}                [options] - Options.
+   * @return {Class}
+   */
+  Class.from = function (data, options) {
+    // Merging given options with serialized ones
+    const finalOptions = assign({}, data.options, options);
+
+    const instance = new Class(finalOptions);
+    instance.import(data);
+
+    return instance;
+  };
+}
+
+attachStaticFromMethod(Graph);
+attachStaticFromMethod(DirectedGraph);
+attachStaticFromMethod(UndirectedGraph);
+attachStaticFromMethod(MultiGraph);
+attachStaticFromMethod(MultiDirectedGraph);
+attachStaticFromMethod(MultiUndirectedGraph);
+
+Graph.Graph = Graph;
+Graph.DirectedGraph = DirectedGraph;
+Graph.UndirectedGraph = UndirectedGraph;
+Graph.MultiGraph = MultiGraph;
+Graph.MultiDirectedGraph = MultiDirectedGraph;
+Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+
+Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+Graph.NotFoundGraphError = NotFoundGraphError;
+Graph.UsageGraphError = UsageGraphError;
+
+/**
+ * Graphology ESM Endoint
+ * =======================
+ *
+ * Endpoint for ESM modules consumers.
+ */
+
+export { DirectedGraph, Graph, InvalidArgumentsGraphError, MultiDirectedGraph, MultiGraph, MultiUndirectedGraph, NotFoundGraphError, UndirectedGraph, UsageGraphError, Graph as default };
+//# sourceMappingURL=graphology.esm.js.map
diff --git a/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.js b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3d07b30a7f3c952015811f39d73c699d8c064d5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.js
@@ -0,0 +1,6125 @@
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.graphology = factory());
+})(this, (function () { 'use strict';
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+      return typeof obj;
+    } : function (obj) {
+      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+    }, _typeof(obj);
+  }
+
+  function _inheritsLoose(subClass, superClass) {
+    subClass.prototype = Object.create(superClass.prototype);
+    subClass.prototype.constructor = subClass;
+
+    _setPrototypeOf(subClass, superClass);
+  }
+
+  function _getPrototypeOf(o) {
+    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
+      return o.__proto__ || Object.getPrototypeOf(o);
+    };
+    return _getPrototypeOf(o);
+  }
+
+  function _setPrototypeOf(o, p) {
+    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
+      o.__proto__ = p;
+      return o;
+    };
+
+    return _setPrototypeOf(o, p);
+  }
+
+  function _isNativeReflectConstruct() {
+    if (typeof Reflect === "undefined" || !Reflect.construct) return false;
+    if (Reflect.construct.sham) return false;
+    if (typeof Proxy === "function") return true;
+
+    try {
+      Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  function _construct(Parent, args, Class) {
+    if (_isNativeReflectConstruct()) {
+      _construct = Reflect.construct;
+    } else {
+      _construct = function _construct(Parent, args, Class) {
+        var a = [null];
+        a.push.apply(a, args);
+        var Constructor = Function.bind.apply(Parent, a);
+        var instance = new Constructor();
+        if (Class) _setPrototypeOf(instance, Class.prototype);
+        return instance;
+      };
+    }
+
+    return _construct.apply(null, arguments);
+  }
+
+  function _isNativeFunction(fn) {
+    return Function.toString.call(fn).indexOf("[native code]") !== -1;
+  }
+
+  function _wrapNativeSuper(Class) {
+    var _cache = typeof Map === "function" ? new Map() : undefined;
+
+    _wrapNativeSuper = function _wrapNativeSuper(Class) {
+      if (Class === null || !_isNativeFunction(Class)) return Class;
+
+      if (typeof Class !== "function") {
+        throw new TypeError("Super expression must either be null or a function");
+      }
+
+      if (typeof _cache !== "undefined") {
+        if (_cache.has(Class)) return _cache.get(Class);
+
+        _cache.set(Class, Wrapper);
+      }
+
+      function Wrapper() {
+        return _construct(Class, arguments, _getPrototypeOf(this).constructor);
+      }
+
+      Wrapper.prototype = Object.create(Class.prototype, {
+        constructor: {
+          value: Wrapper,
+          enumerable: false,
+          writable: true,
+          configurable: true
+        }
+      });
+      return _setPrototypeOf(Wrapper, Class);
+    };
+
+    return _wrapNativeSuper(Class);
+  }
+
+  function _assertThisInitialized(self) {
+    if (self === void 0) {
+      throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+    }
+
+    return self;
+  }
+
+  /**
+   * Graphology Utilities
+   * =====================
+   *
+   * Collection of helpful functions used by the implementation.
+   */
+
+  /**
+   * Object.assign-like polyfill.
+   *
+   * @param  {object} target       - First object.
+   * @param  {object} [...objects] - Objects to merge.
+   * @return {object}
+   */
+  function assignPolyfill() {
+    var target = arguments[0];
+
+    for (var i = 1, l = arguments.length; i < l; i++) {
+      if (!arguments[i]) continue;
+
+      for (var k in arguments[i]) {
+        target[k] = arguments[i][k];
+      }
+    }
+
+    return target;
+  }
+
+  var assign = assignPolyfill;
+  if (typeof Object.assign === 'function') assign = Object.assign;
+  /**
+   * Function returning the first matching edge for given path.
+   * Note: this function does not check the existence of source & target. This
+   * must be performed by the caller.
+   *
+   * @param  {Graph}  graph  - Target graph.
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+   * @return {string|null}
+   */
+
+  function getMatchingEdge(graph, source, target, type) {
+    var sourceData = graph._nodes.get(source);
+
+    var edge = null;
+    if (!sourceData) return edge;
+
+    if (type === 'mixed') {
+      edge = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target];
+    } else if (type === 'directed') {
+      edge = sourceData.out && sourceData.out[target];
+    } else {
+      edge = sourceData.undirected && sourceData.undirected[target];
+    }
+
+    return edge;
+  }
+  /**
+   * Checks whether the given value is a Graph implementation instance.
+   *
+   * @param  {mixed}   value - Target value.
+   * @return {boolean}
+   */
+
+  function isGraph(value) {
+    return value !== null && _typeof(value) === 'object' && typeof value.addUndirectedEdgeWithKey === 'function' && typeof value.dropNode === 'function';
+  }
+  /**
+   * Checks whether the given value is a plain object.
+   *
+   * @param  {mixed}   value - Target value.
+   * @return {boolean}
+   */
+
+  function isPlainObject(value) {
+    return _typeof(value) === 'object' && value !== null && value.constructor === Object;
+  }
+  /**
+   * Checks whether the given object is empty.
+   *
+   * @param  {object}  o - Target Object.
+   * @return {boolean}
+   */
+
+  function isEmpty(o) {
+    var k;
+
+    for (k in o) {
+      return false;
+    }
+
+    return true;
+  }
+  /**
+   * Creates a "private" property for the given member name by concealing it
+   * using the `enumerable` option.
+   *
+   * @param {object} target - Target object.
+   * @param {string} name   - Member name.
+   */
+
+  function privateProperty(target, name, value) {
+    Object.defineProperty(target, name, {
+      enumerable: false,
+      configurable: false,
+      writable: true,
+      value: value
+    });
+  }
+  /**
+   * Creates a read-only property for the given member name & the given getter.
+   *
+   * @param {object}   target - Target object.
+   * @param {string}   name   - Member name.
+   * @param {mixed}    value  - The attached getter or fixed value.
+   */
+
+  function readOnlyProperty(target, name, value) {
+    var descriptor = {
+      enumerable: true,
+      configurable: true
+    };
+
+    if (typeof value === 'function') {
+      descriptor.get = value;
+    } else {
+      descriptor.value = value;
+      descriptor.writable = false;
+    }
+
+    Object.defineProperty(target, name, descriptor);
+  }
+  /**
+   * Returns whether the given object constitute valid hints.
+   *
+   * @param {object} hints - Target object.
+   */
+
+  function validateHints(hints) {
+    if (!isPlainObject(hints)) return false;
+    if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+    return true;
+  }
+  /**
+   * Creates a function generating incremental ids for edges.
+   *
+   * @return {function}
+   */
+
+  function incrementalIdStartingFromRandomByte() {
+    var i = Math.floor(Math.random() * 256) & 0xff;
+    return function () {
+      return i++;
+    };
+  }
+
+  var events = {exports: {}};
+
+  var R = typeof Reflect === 'object' ? Reflect : null;
+  var ReflectApply = R && typeof R.apply === 'function' ? R.apply : function ReflectApply(target, receiver, args) {
+    return Function.prototype.apply.call(target, receiver, args);
+  };
+  var ReflectOwnKeys;
+
+  if (R && typeof R.ownKeys === 'function') {
+    ReflectOwnKeys = R.ownKeys;
+  } else if (Object.getOwnPropertySymbols) {
+    ReflectOwnKeys = function ReflectOwnKeys(target) {
+      return Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));
+    };
+  } else {
+    ReflectOwnKeys = function ReflectOwnKeys(target) {
+      return Object.getOwnPropertyNames(target);
+    };
+  }
+
+  function ProcessEmitWarning(warning) {
+    if (console && console.warn) console.warn(warning);
+  }
+
+  var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
+    return value !== value;
+  };
+
+  function EventEmitter() {
+    EventEmitter.init.call(this);
+  }
+
+  events.exports = EventEmitter;
+  events.exports.once = once; // Backwards-compat with node 0.10.x
+
+  EventEmitter.EventEmitter = EventEmitter;
+  EventEmitter.prototype._events = undefined;
+  EventEmitter.prototype._eventsCount = 0;
+  EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are
+  // added to it. This is a useful default which helps finding memory leaks.
+
+  var defaultMaxListeners = 10;
+
+  function checkListener(listener) {
+    if (typeof listener !== 'function') {
+      throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
+    }
+  }
+
+  Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
+    enumerable: true,
+    get: function () {
+      return defaultMaxListeners;
+    },
+    set: function (arg) {
+      if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
+        throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
+      }
+
+      defaultMaxListeners = arg;
+    }
+  });
+
+  EventEmitter.init = function () {
+    if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
+      this._events = Object.create(null);
+      this._eventsCount = 0;
+    }
+
+    this._maxListeners = this._maxListeners || undefined;
+  }; // Obviously not all Emitters should be limited to 10. This function allows
+  // that to be increased. Set to zero for unlimited.
+
+
+  EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+    if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+      throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
+    }
+
+    this._maxListeners = n;
+    return this;
+  };
+
+  function _getMaxListeners(that) {
+    if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners;
+    return that._maxListeners;
+  }
+
+  EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
+    return _getMaxListeners(this);
+  };
+
+  EventEmitter.prototype.emit = function emit(type) {
+    var args = [];
+
+    for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
+
+    var doError = type === 'error';
+    var events = this._events;
+    if (events !== undefined) doError = doError && events.error === undefined;else if (!doError) return false; // If there is no 'error' event listener then throw.
+
+    if (doError) {
+      var er;
+      if (args.length > 0) er = args[0];
+
+      if (er instanceof Error) {
+        // Note: The comments on the `throw` lines are intentional, they show
+        // up in Node's output if this results in an unhandled exception.
+        throw er; // Unhandled 'error' event
+      } // At least give some kind of context to the user
+
+
+      var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
+      err.context = er;
+      throw err; // Unhandled 'error' event
+    }
+
+    var handler = events[type];
+    if (handler === undefined) return false;
+
+    if (typeof handler === 'function') {
+      ReflectApply(handler, this, args);
+    } else {
+      var len = handler.length;
+      var listeners = arrayClone(handler, len);
+
+      for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args);
+    }
+
+    return true;
+  };
+
+  function _addListener(target, type, listener, prepend) {
+    var m;
+    var events;
+    var existing;
+    checkListener(listener);
+    events = target._events;
+
+    if (events === undefined) {
+      events = target._events = Object.create(null);
+      target._eventsCount = 0;
+    } else {
+      // To avoid recursion in the case that type === "newListener"! Before
+      // adding it to the listeners, first emit "newListener".
+      if (events.newListener !== undefined) {
+        target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the
+        // this._events to be assigned to a new object
+
+        events = target._events;
+      }
+
+      existing = events[type];
+    }
+
+    if (existing === undefined) {
+      // Optimize the case of one listener. Don't need the extra array object.
+      existing = events[type] = listener;
+      ++target._eventsCount;
+    } else {
+      if (typeof existing === 'function') {
+        // Adding the second element, need to change to array.
+        existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append.
+      } else if (prepend) {
+        existing.unshift(listener);
+      } else {
+        existing.push(listener);
+      } // Check for listener leak
+
+
+      m = _getMaxListeners(target);
+
+      if (m > 0 && existing.length > m && !existing.warned) {
+        existing.warned = true; // No error code for this since it is a Warning
+        // eslint-disable-next-line no-restricted-syntax
+
+        var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + String(type) + ' listeners ' + 'added. Use emitter.setMaxListeners() to ' + 'increase limit');
+        w.name = 'MaxListenersExceededWarning';
+        w.emitter = target;
+        w.type = type;
+        w.count = existing.length;
+        ProcessEmitWarning(w);
+      }
+    }
+
+    return target;
+  }
+
+  EventEmitter.prototype.addListener = function addListener(type, listener) {
+    return _addListener(this, type, listener, false);
+  };
+
+  EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+  EventEmitter.prototype.prependListener = function prependListener(type, listener) {
+    return _addListener(this, type, listener, true);
+  };
+
+  function onceWrapper() {
+    if (!this.fired) {
+      this.target.removeListener(this.type, this.wrapFn);
+      this.fired = true;
+      if (arguments.length === 0) return this.listener.call(this.target);
+      return this.listener.apply(this.target, arguments);
+    }
+  }
+
+  function _onceWrap(target, type, listener) {
+    var state = {
+      fired: false,
+      wrapFn: undefined,
+      target: target,
+      type: type,
+      listener: listener
+    };
+    var wrapped = onceWrapper.bind(state);
+    wrapped.listener = listener;
+    state.wrapFn = wrapped;
+    return wrapped;
+  }
+
+  EventEmitter.prototype.once = function once(type, listener) {
+    checkListener(listener);
+    this.on(type, _onceWrap(this, type, listener));
+    return this;
+  };
+
+  EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) {
+    checkListener(listener);
+    this.prependListener(type, _onceWrap(this, type, listener));
+    return this;
+  }; // Emits a 'removeListener' event if and only if the listener was removed.
+
+
+  EventEmitter.prototype.removeListener = function removeListener(type, listener) {
+    var list, events, position, i, originalListener;
+    checkListener(listener);
+    events = this._events;
+    if (events === undefined) return this;
+    list = events[type];
+    if (list === undefined) return this;
+
+    if (list === listener || list.listener === listener) {
+      if (--this._eventsCount === 0) this._events = Object.create(null);else {
+        delete events[type];
+        if (events.removeListener) this.emit('removeListener', type, list.listener || listener);
+      }
+    } else if (typeof list !== 'function') {
+      position = -1;
+
+      for (i = list.length - 1; i >= 0; i--) {
+        if (list[i] === listener || list[i].listener === listener) {
+          originalListener = list[i].listener;
+          position = i;
+          break;
+        }
+      }
+
+      if (position < 0) return this;
+      if (position === 0) list.shift();else {
+        spliceOne(list, position);
+      }
+      if (list.length === 1) events[type] = list[0];
+      if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener);
+    }
+
+    return this;
+  };
+
+  EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+  EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
+    var listeners, events, i;
+    events = this._events;
+    if (events === undefined) return this; // not listening for removeListener, no need to emit
+
+    if (events.removeListener === undefined) {
+      if (arguments.length === 0) {
+        this._events = Object.create(null);
+        this._eventsCount = 0;
+      } else if (events[type] !== undefined) {
+        if (--this._eventsCount === 0) this._events = Object.create(null);else delete events[type];
+      }
+
+      return this;
+    } // emit removeListener for all listeners on all events
+
+
+    if (arguments.length === 0) {
+      var keys = Object.keys(events);
+      var key;
+
+      for (i = 0; i < keys.length; ++i) {
+        key = keys[i];
+        if (key === 'removeListener') continue;
+        this.removeAllListeners(key);
+      }
+
+      this.removeAllListeners('removeListener');
+      this._events = Object.create(null);
+      this._eventsCount = 0;
+      return this;
+    }
+
+    listeners = events[type];
+
+    if (typeof listeners === 'function') {
+      this.removeListener(type, listeners);
+    } else if (listeners !== undefined) {
+      // LIFO order
+      for (i = listeners.length - 1; i >= 0; i--) {
+        this.removeListener(type, listeners[i]);
+      }
+    }
+
+    return this;
+  };
+
+  function _listeners(target, type, unwrap) {
+    var events = target._events;
+    if (events === undefined) return [];
+    var evlistener = events[type];
+    if (evlistener === undefined) return [];
+    if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener];
+    return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
+  }
+
+  EventEmitter.prototype.listeners = function listeners(type) {
+    return _listeners(this, type, true);
+  };
+
+  EventEmitter.prototype.rawListeners = function rawListeners(type) {
+    return _listeners(this, type, false);
+  };
+
+  EventEmitter.listenerCount = function (emitter, type) {
+    if (typeof emitter.listenerCount === 'function') {
+      return emitter.listenerCount(type);
+    } else {
+      return listenerCount.call(emitter, type);
+    }
+  };
+
+  EventEmitter.prototype.listenerCount = listenerCount;
+
+  function listenerCount(type) {
+    var events = this._events;
+
+    if (events !== undefined) {
+      var evlistener = events[type];
+
+      if (typeof evlistener === 'function') {
+        return 1;
+      } else if (evlistener !== undefined) {
+        return evlistener.length;
+      }
+    }
+
+    return 0;
+  }
+
+  EventEmitter.prototype.eventNames = function eventNames() {
+    return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
+  };
+
+  function arrayClone(arr, n) {
+    var copy = new Array(n);
+
+    for (var i = 0; i < n; ++i) copy[i] = arr[i];
+
+    return copy;
+  }
+
+  function spliceOne(list, index) {
+    for (; index + 1 < list.length; index++) list[index] = list[index + 1];
+
+    list.pop();
+  }
+
+  function unwrapListeners(arr) {
+    var ret = new Array(arr.length);
+
+    for (var i = 0; i < ret.length; ++i) {
+      ret[i] = arr[i].listener || arr[i];
+    }
+
+    return ret;
+  }
+
+  function once(emitter, name) {
+    return new Promise(function (resolve, reject) {
+      function errorListener(err) {
+        emitter.removeListener(name, resolver);
+        reject(err);
+      }
+
+      function resolver() {
+        if (typeof emitter.removeListener === 'function') {
+          emitter.removeListener('error', errorListener);
+        }
+
+        resolve([].slice.call(arguments));
+      }
+      eventTargetAgnosticAddListener(emitter, name, resolver, {
+        once: true
+      });
+
+      if (name !== 'error') {
+        addErrorHandlerIfEventEmitter(emitter, errorListener, {
+          once: true
+        });
+      }
+    });
+  }
+
+  function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
+    if (typeof emitter.on === 'function') {
+      eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
+    }
+  }
+
+  function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
+    if (typeof emitter.on === 'function') {
+      if (flags.once) {
+        emitter.once(name, listener);
+      } else {
+        emitter.on(name, listener);
+      }
+    } else if (typeof emitter.addEventListener === 'function') {
+      // EventTarget does not have `error` event semantics like Node
+      // EventEmitters, we do not listen for `error` events here.
+      emitter.addEventListener(name, function wrapListener(arg) {
+        // IE does not have builtin `{ once: true }` support so we
+        // have to do it manually.
+        if (flags.once) {
+          emitter.removeEventListener(name, wrapListener);
+        }
+
+        listener(arg);
+      });
+    } else {
+      throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
+    }
+  }
+
+  /**
+   * Obliterator Iterator Class
+   * ===========================
+   *
+   * Simple class representing the library's iterators.
+   */
+  /**
+   * Iterator class.
+   *
+   * @constructor
+   * @param {function} next - Next function.
+   */
+
+  function Iterator$2(next) {
+    if (typeof next !== 'function') throw new Error('obliterator/iterator: expecting a function!');
+    this.next = next;
+  }
+  /**
+   * If symbols are supported, we add `next` to `Symbol.iterator`.
+   */
+
+
+  if (typeof Symbol !== 'undefined') Iterator$2.prototype[Symbol.iterator] = function () {
+    return this;
+  };
+  /**
+   * Returning an iterator of the given values.
+   *
+   * @param  {any...} values - Values.
+   * @return {Iterator}
+   */
+
+  Iterator$2.of = function () {
+    var args = arguments,
+        l = args.length,
+        i = 0;
+    return new Iterator$2(function () {
+      if (i >= l) return {
+        done: true
+      };
+      return {
+        done: false,
+        value: args[i++]
+      };
+    });
+  };
+  /**
+   * Returning an empty iterator.
+   *
+   * @return {Iterator}
+   */
+
+
+  Iterator$2.empty = function () {
+    var iterator = new Iterator$2(function () {
+      return {
+        done: true
+      };
+    });
+    return iterator;
+  };
+  /**
+   * Returning an iterator over the given indexed sequence.
+   *
+   * @param  {string|Array} sequence - Target sequence.
+   * @return {Iterator}
+   */
+
+
+  Iterator$2.fromSequence = function (sequence) {
+    var i = 0,
+        l = sequence.length;
+    return new Iterator$2(function () {
+      if (i >= l) return {
+        done: true
+      };
+      return {
+        done: false,
+        value: sequence[i++]
+      };
+    });
+  };
+  /**
+   * Returning whether the given value is an iterator.
+   *
+   * @param  {any} value - Value.
+   * @return {boolean}
+   */
+
+
+  Iterator$2.is = function (value) {
+    if (value instanceof Iterator$2) return true;
+    return typeof value === 'object' && value !== null && typeof value.next === 'function';
+  };
+  /**
+   * Exporting.
+   */
+
+
+  var iterator = Iterator$2;
+
+  var support$1 = {};
+
+  support$1.ARRAY_BUFFER_SUPPORT = typeof ArrayBuffer !== 'undefined';
+  support$1.SYMBOL_SUPPORT = typeof Symbol !== 'undefined';
+
+  /**
+   * Obliterator Iter Function
+   * ==========================
+   *
+   * Function coercing values to an iterator. It can be quite useful when needing
+   * to handle iterables and iterators the same way.
+   */
+  var Iterator$1 = iterator;
+  var support = support$1;
+  var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+  var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+  function iterOrNull(target) {
+    // Indexed sequence
+    if (typeof target === 'string' || Array.isArray(target) || ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(target)) return Iterator$1.fromSequence(target); // Invalid value
+
+    if (typeof target !== 'object' || target === null) return null; // Iterable
+
+    if (SYMBOL_SUPPORT && typeof target[Symbol.iterator] === 'function') return target[Symbol.iterator](); // Iterator duck-typing
+
+    if (typeof target.next === 'function') return target; // Invalid object
+
+    return null;
+  }
+
+  var iter$2 = function iter(target) {
+    var iterator = iterOrNull(target);
+    if (!iterator) throw new Error('obliterator: target is not iterable nor a valid iterator.');
+    return iterator;
+  };
+
+  /* eslint no-constant-condition: 0 */
+  /**
+   * Obliterator Take Function
+   * ==========================
+   *
+   * Function taking n or every value of the given iterator and returns them
+   * into an array.
+   */
+
+  var iter$1 = iter$2;
+  /**
+   * Take.
+   *
+   * @param  {Iterable} iterable - Target iterable.
+   * @param  {number}   [n]      - Optional number of items to take.
+   * @return {array}
+   */
+
+  var take = function take(iterable, n) {
+    var l = arguments.length > 1 ? n : Infinity,
+        array = l !== Infinity ? new Array(l) : [],
+        step,
+        i = 0;
+    var iterator = iter$1(iterable);
+
+    while (true) {
+      if (i === l) return array;
+      step = iterator.next();
+
+      if (step.done) {
+        if (i !== n) array.length = i;
+        return array;
+      }
+
+      array[i++] = step.value;
+    }
+  };
+
+  /**
+   * Graphology Custom Errors
+   * =========================
+   *
+   * Defining custom errors for ease of use & easy unit tests across
+   * implementations (normalized typology rather than relying on error
+   * messages to check whether the correct error was found).
+   */
+  var GraphError = /*#__PURE__*/function (_Error) {
+    _inheritsLoose(GraphError, _Error);
+
+    function GraphError(message) {
+      var _this;
+
+      _this = _Error.call(this) || this;
+      _this.name = 'GraphError';
+      _this.message = message;
+      return _this;
+    }
+
+    return GraphError;
+  }( /*#__PURE__*/_wrapNativeSuper(Error));
+  var InvalidArgumentsGraphError = /*#__PURE__*/function (_GraphError) {
+    _inheritsLoose(InvalidArgumentsGraphError, _GraphError);
+
+    function InvalidArgumentsGraphError(message) {
+      var _this2;
+
+      _this2 = _GraphError.call(this, message) || this;
+      _this2.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this2), InvalidArgumentsGraphError.prototype.constructor);
+      return _this2;
+    }
+
+    return InvalidArgumentsGraphError;
+  }(GraphError);
+  var NotFoundGraphError = /*#__PURE__*/function (_GraphError2) {
+    _inheritsLoose(NotFoundGraphError, _GraphError2);
+
+    function NotFoundGraphError(message) {
+      var _this3;
+
+      _this3 = _GraphError2.call(this, message) || this;
+      _this3.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this3), NotFoundGraphError.prototype.constructor);
+      return _this3;
+    }
+
+    return NotFoundGraphError;
+  }(GraphError);
+  var UsageGraphError = /*#__PURE__*/function (_GraphError3) {
+    _inheritsLoose(UsageGraphError, _GraphError3);
+
+    function UsageGraphError(message) {
+      var _this4;
+
+      _this4 = _GraphError3.call(this, message) || this;
+      _this4.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this4), UsageGraphError.prototype.constructor);
+      return _this4;
+    }
+
+    return UsageGraphError;
+  }(GraphError);
+
+  /**
+   * Graphology Internal Data Classes
+   * =================================
+   *
+   * Internal classes hopefully reduced to structs by engines & storing
+   * necessary information for nodes & edges.
+   *
+   * Note that those classes don't rely on the `class` keyword to avoid some
+   * cruft introduced by most of ES2015 transpilers.
+   */
+
+  /**
+   * MixedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+  function MixedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  MixedNodeData.prototype.clear = function () {
+    // Degrees
+    this.inDegree = 0;
+    this.outDegree = 0;
+    this.undirectedDegree = 0; // Indices
+
+    this["in"] = {};
+    this.out = {};
+    this.undirected = {};
+  };
+  /**
+   * DirectedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+
+
+  function DirectedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  DirectedNodeData.prototype.clear = function () {
+    // Degrees
+    this.inDegree = 0;
+    this.outDegree = 0; // Indices
+
+    this["in"] = {};
+    this.out = {};
+  };
+  /**
+   * UndirectedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+
+
+  function UndirectedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  UndirectedNodeData.prototype.clear = function () {
+    // Degrees
+    this.undirectedDegree = 0; // Indices
+
+    this.undirected = {};
+  };
+  /**
+   * EdgeData class.
+   *
+   * @constructor
+   * @param {boolean} undirected   - Whether the edge is undirected.
+   * @param {string}  string       - The edge's key.
+   * @param {string}  source       - Source of the edge.
+   * @param {string}  target       - Target of the edge.
+   * @param {object}  attributes   - Edge's attributes.
+   */
+
+
+  function EdgeData(undirected, key, source, target, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.undirected = undirected; // Extremities
+
+    this.source = source;
+    this.target = target;
+  }
+
+  EdgeData.prototype.attach = function () {
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected';
+    var source = this.source.key;
+    var target = this.target.key; // Handling source
+
+    this.source[outKey][target] = this;
+    if (this.undirected && source === target) return; // Handling target
+
+    this.target[inKey][source] = this;
+  };
+
+  EdgeData.prototype.attachMulti = function () {
+    var outKey = 'out';
+    var inKey = 'in';
+    var source = this.source.key;
+    var target = this.target.key;
+    if (this.undirected) outKey = inKey = 'undirected'; // Handling source
+
+    var adj = this.source[outKey];
+    var head = adj[target];
+
+    if (typeof head === 'undefined') {
+      adj[target] = this; // Self-loop optimization
+
+      if (!(this.undirected && source === target)) {
+        // Handling target
+        this.target[inKey][source] = this;
+      }
+
+      return;
+    } // Prepending to doubly-linked list
+
+
+    head.previous = this;
+    this.next = head; // Pointing to new head
+    // NOTE: use mutating swap later to avoid lookup?
+
+    adj[target] = this;
+    this.target[inKey][source] = this;
+  };
+
+  EdgeData.prototype.detach = function () {
+    var source = this.source.key;
+    var target = this.target.key;
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected';
+    delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+    delete this.target[inKey][source];
+  };
+
+  EdgeData.prototype.detachMulti = function () {
+    var source = this.source.key;
+    var target = this.target.key;
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list
+
+    if (this.previous === undefined) {
+      // We are dealing with the head
+      // Should we delete the adjacency entry because it is now empty?
+      if (this.next === undefined) {
+        delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+        delete this.target[inKey][source];
+      } else {
+        // Detaching
+        this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method
+
+        this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop
+
+        this.target[inKey][source] = this.next;
+      }
+    } else {
+      // We are dealing with another list node
+      this.previous.next = this.next; // If not last
+
+      if (this.next !== undefined) {
+        this.next.previous = this.previous;
+      }
+    }
+  };
+
+  /**
+   * Graphology Node Attributes methods
+   * ===================================
+   */
+  var NODE = 0;
+  var SOURCE = 1;
+  var TARGET = 2;
+  var OPPOSITE = 3;
+
+  function findRelevantNodeData(graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2) {
+    var nodeData, edgeData, arg1, arg2;
+    nodeOrEdge = '' + nodeOrEdge;
+
+    if (mode === NODE) {
+      nodeData = graph._nodes.get(nodeOrEdge);
+      if (!nodeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" node in the graph."));
+      arg1 = nameOrEdge;
+      arg2 = add1;
+    } else if (mode === OPPOSITE) {
+      nameOrEdge = '' + nameOrEdge;
+      edgeData = graph._edges.get(nameOrEdge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nameOrEdge, "\" edge in the graph."));
+      var source = edgeData.source.key;
+      var target = edgeData.target.key;
+
+      if (nodeOrEdge === source) {
+        nodeData = edgeData.target;
+      } else if (nodeOrEdge === target) {
+        nodeData = edgeData.source;
+      } else {
+        throw new NotFoundGraphError("Graph.".concat(method, ": the \"").concat(nodeOrEdge, "\" node is not attached to the \"").concat(nameOrEdge, "\" edge (").concat(source, ", ").concat(target, ")."));
+      }
+
+      arg1 = add1;
+      arg2 = add2;
+    } else {
+      edgeData = graph._edges.get(nodeOrEdge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" edge in the graph."));
+
+      if (mode === SOURCE) {
+        nodeData = edgeData.source;
+      } else {
+        nodeData = edgeData.target;
+      }
+
+      arg1 = nameOrEdge;
+      arg2 = add1;
+    }
+
+    return [nodeData, arg1, arg2];
+  }
+
+  function attachNodeAttributeGetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData[0],
+          name = _findRelevantNodeData[1];
+
+      return data.attributes[name];
+    };
+  }
+
+  function attachNodeAttributesGetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+      var _findRelevantNodeData2 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge),
+          data = _findRelevantNodeData2[0];
+
+      return data.attributes;
+    };
+  }
+
+  function attachNodeAttributeChecker(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData3 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData3[0],
+          name = _findRelevantNodeData3[1];
+
+      return data.attributes.hasOwnProperty(name);
+    };
+  }
+
+  function attachNodeAttributeSetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+      var _findRelevantNodeData4 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+          data = _findRelevantNodeData4[0],
+          name = _findRelevantNodeData4[1],
+          value = _findRelevantNodeData4[2];
+
+      data.attributes[name] = value; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributeUpdater(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+      var _findRelevantNodeData5 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+          data = _findRelevantNodeData5[0],
+          name = _findRelevantNodeData5[1],
+          updater = _findRelevantNodeData5[2];
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+      var attributes = data.attributes;
+      var value = updater(attributes[name]);
+      attributes[name] = value; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributeRemover(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData6 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData6[0],
+          name = _findRelevantNodeData6[1];
+
+      delete data.attributes[name]; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'remove',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesReplacer(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData7 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData7[0],
+          attributes = _findRelevantNodeData7[1];
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      data.attributes = attributes; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'replace',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesMerger(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData8 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData8[0],
+          attributes = _findRelevantNodeData8[1];
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      assign(data.attributes, attributes); // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'merge',
+        attributes: data.attributes,
+        data: attributes
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesUpdater(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData9 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData9[0],
+          updater = _findRelevantNodeData9[1];
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+      data.attributes = updater(data.attributes); // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'update',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * List of methods to attach.
+   */
+
+
+  var NODE_ATTRIBUTES_METHODS = [{
+    name: function name(element) {
+      return "get".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeGetter
+  }, {
+    name: function name(element) {
+      return "get".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesGetter
+  }, {
+    name: function name(element) {
+      return "has".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeChecker
+  }, {
+    name: function name(element) {
+      return "set".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeSetter
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeUpdater
+  }, {
+    name: function name(element) {
+      return "remove".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeRemover
+  }, {
+    name: function name(element) {
+      return "replace".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesReplacer
+  }, {
+    name: function name(element) {
+      return "merge".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesMerger
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesUpdater
+  }];
+  /**
+   * Attach every attributes-related methods to a Graph class.
+   *
+   * @param {function} Graph - Target class.
+   */
+
+  function attachNodeAttributesMethods(Graph) {
+    NODE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+      var name = _ref.name,
+          attacher = _ref.attacher;
+      // For nodes
+      attacher(Graph, name('Node'), NODE); // For sources
+
+      attacher(Graph, name('Source'), SOURCE); // For targets
+
+      attacher(Graph, name('Target'), TARGET); // For opposites
+
+      attacher(Graph, name('Opposite'), OPPOSITE);
+    });
+  }
+
+  /**
+   * Graphology Edge Attributes methods
+   * ===================================
+   */
+  /**
+   * Attach an attribute getter method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+  function attachEdgeAttributeGetter(Class, method, type) {
+    /**
+     * Get the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {mixed}          - The attribute's value.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes[name];
+    };
+  }
+  /**
+   * Attach an attributes getter method onto the provided class.
+   *
+   * @param {function} Class       - Target class.
+   * @param {string}   method      - Method name.
+   * @param {string}   type        - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesGetter(Class, method, type) {
+    /**
+     * Retrieves all the target element's attributes.
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     *
+     * @return {object}          - The element's attributes.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 1) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + arguments[1];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes;
+    };
+  }
+  /**
+   * Attach an attribute checker method onto the provided class.
+   *
+   * @param {function} Class       - Target class.
+   * @param {string}   method      - Method name.
+   * @param {string}   type        - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeChecker(Class, method, type) {
+    /**
+     * Checks whether the desired attribute is set for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes.hasOwnProperty(name);
+    };
+  }
+  /**
+   * Attach an attribute setter method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeSetter(Class, method, type) {
+    /**
+     * Set the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     * @param  {mixed}  value   - New attribute value.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     * @param  {mixed}  value   - New attribute value.
+     *
+     * @return {Graph}          - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name, value) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 3) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        value = arguments[3];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      data.attributes[name] = value; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute updater method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeUpdater(Class, method, type) {
+    /**
+     * Update the desired attribute for the given element (node or edge) using
+     * the provided function.
+     *
+     * Arity 2:
+     * @param  {any}      element - Target element.
+     * @param  {string}   name    - Attribute's name.
+     * @param  {function} updater - Updater function.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}      source  - Source element.
+     * @param  {any}      target  - Target element.
+     * @param  {string}   name    - Attribute's name.
+     * @param  {function} updater - Updater function.
+     *
+     * @return {Graph}            - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name, updater) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 3) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        updater = arguments[3];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+      data.attributes[name] = updater(data.attributes[name]); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute remover method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeRemover(Class, method, type) {
+    /**
+     * Remove the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {Graph}          - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      delete data.attributes[name]; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'remove',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute replacer method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesReplacer(Class, method, type) {
+    /**
+     * Replace the attributes for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element    - Target element.
+     * @param  {object} attributes - New attributes.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source     - Source element.
+     * @param  {any}     target     - Target element.
+     * @param  {object}  attributes - New attributes.
+     *
+     * @return {Graph}              - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, attributes) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + attributes;
+        attributes = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      data.attributes = attributes; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'replace',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute merger method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesMerger(Class, method, type) {
+    /**
+     * Merge the attributes for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element    - Target element.
+     * @param  {object} attributes - Attributes to merge.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source     - Source element.
+     * @param  {any}     target     - Target element.
+     * @param  {object}  attributes - Attributes to merge.
+     *
+     * @return {Graph}              - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, attributes) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + attributes;
+        attributes = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      assign(data.attributes, attributes); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'merge',
+        attributes: data.attributes,
+        data: attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute updater method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesUpdater(Class, method, type) {
+    /**
+     * Update the attributes of the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}      element - Target element.
+     * @param  {function} updater - Updater function.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}      source  - Source element.
+     * @param  {any}      target  - Target element.
+     * @param  {function} updater - Updater function.
+     *
+     * @return {Graph}            - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, updater) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + updater;
+        updater = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+      data.attributes = updater(data.attributes); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'update',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * List of methods to attach.
+   */
+
+
+  var EDGE_ATTRIBUTES_METHODS = [{
+    name: function name(element) {
+      return "get".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeGetter
+  }, {
+    name: function name(element) {
+      return "get".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesGetter
+  }, {
+    name: function name(element) {
+      return "has".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeChecker
+  }, {
+    name: function name(element) {
+      return "set".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeSetter
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeUpdater
+  }, {
+    name: function name(element) {
+      return "remove".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeRemover
+  }, {
+    name: function name(element) {
+      return "replace".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesReplacer
+  }, {
+    name: function name(element) {
+      return "merge".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesMerger
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesUpdater
+  }];
+  /**
+   * Attach every attributes-related methods to a Graph class.
+   *
+   * @param {function} Graph - Target class.
+   */
+
+  function attachEdgeAttributesMethods(Graph) {
+    EDGE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+      var name = _ref.name,
+          attacher = _ref.attacher;
+      // For edges
+      attacher(Graph, name('Edge'), 'mixed'); // For directed edges
+
+      attacher(Graph, name('DirectedEdge'), 'directed'); // For undirected edges
+
+      attacher(Graph, name('UndirectedEdge'), 'undirected');
+    });
+  }
+
+  /**
+   * Obliterator Chain Function
+   * ===========================
+   *
+   * Variadic function combining the given iterables.
+   */
+  var Iterator = iterator;
+  var iter = iter$2;
+  /**
+   * Chain.
+   *
+   * @param  {...Iterator} iterables - Target iterables.
+   * @return {Iterator}
+   */
+
+  var chain = function chain() {
+    var iterables = arguments;
+    var current = null;
+    var i = -1;
+    /* eslint-disable no-constant-condition */
+
+    return new Iterator(function next() {
+      var step = null;
+
+      do {
+        if (current === null) {
+          i++;
+          if (i >= iterables.length) return {
+            done: true
+          };
+          current = iter(iterables[i]);
+        }
+
+        step = current.next();
+
+        if (step.done === true) {
+          current = null;
+          continue;
+        }
+
+        break;
+      } while (true);
+
+      return step;
+    });
+  };
+
+  /**
+   * Graphology Edge Iteration
+   * ==========================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over a
+   * graph's edges.
+   */
+  /**
+   * Definitions.
+   */
+
+  var EDGES_ITERATION = [{
+    name: 'edges',
+    type: 'mixed'
+  }, {
+    name: 'inEdges',
+    type: 'directed',
+    direction: 'in'
+  }, {
+    name: 'outEdges',
+    type: 'directed',
+    direction: 'out'
+  }, {
+    name: 'inboundEdges',
+    type: 'mixed',
+    direction: 'in'
+  }, {
+    name: 'outboundEdges',
+    type: 'mixed',
+    direction: 'out'
+  }, {
+    name: 'directedEdges',
+    type: 'directed'
+  }, {
+    name: 'undirectedEdges',
+    type: 'undirected'
+  }];
+  /**
+   * Function iterating over edges from the given object to match one of them.
+   *
+   * @param {object}   object   - Target object.
+   * @param {function} callback - Function to call.
+   */
+
+  function forEachSimple(breakable, object, callback, avoid) {
+    var shouldBreak = false;
+
+    for (var k in object) {
+      if (k === avoid) continue;
+      var edgeData = object[k];
+      shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+    }
+
+    return;
+  }
+
+  function forEachMulti(breakable, object, callback, avoid) {
+    var edgeData, source, target;
+    var shouldBreak = false;
+
+    for (var k in object) {
+      if (k === avoid) continue;
+      edgeData = object[k];
+
+      do {
+        source = edgeData.source;
+        target = edgeData.target;
+        shouldBreak = callback(edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected);
+        if (breakable && shouldBreak) return edgeData.key;
+        edgeData = edgeData.next;
+      } while (edgeData !== undefined);
+    }
+
+    return;
+  }
+  /**
+   * Function returning an iterator over edges from the given object.
+   *
+   * @param  {object}   object - Target object.
+   * @return {Iterator}
+   */
+
+
+  function createIterator(object, avoid) {
+    var keys = Object.keys(object);
+    var l = keys.length;
+    var edgeData;
+    var i = 0;
+    return new iterator(function next() {
+      do {
+        if (!edgeData) {
+          if (i >= l) return {
+            done: true
+          };
+          var k = keys[i++];
+
+          if (k === avoid) {
+            edgeData = undefined;
+            continue;
+          }
+
+          edgeData = object[k];
+        } else {
+          edgeData = edgeData.next;
+        }
+      } while (!edgeData);
+
+      return {
+        done: false,
+        value: {
+          edge: edgeData.key,
+          attributes: edgeData.attributes,
+          source: edgeData.source.key,
+          target: edgeData.target.key,
+          sourceAttributes: edgeData.source.attributes,
+          targetAttributes: edgeData.target.attributes,
+          undirected: edgeData.undirected
+        }
+      };
+    });
+  }
+  /**
+   * Function iterating over the egdes from the object at given key to match
+   * one of them.
+   *
+   * @param {object}   object   - Target object.
+   * @param {mixed}    k        - Neighbor key.
+   * @param {function} callback - Callback to use.
+   */
+
+
+  function forEachForKeySimple(breakable, object, k, callback) {
+    var edgeData = object[k];
+    if (!edgeData) return;
+    var sourceData = edgeData.source;
+    var targetData = edgeData.target;
+    if (callback(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected) && breakable) return edgeData.key;
+  }
+
+  function forEachForKeyMulti(breakable, object, k, callback) {
+    var edgeData = object[k];
+    if (!edgeData) return;
+    var shouldBreak = false;
+
+    do {
+      shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+
+    return;
+  }
+  /**
+   * Function returning an iterator over the egdes from the object at given key.
+   *
+   * @param  {object}   object   - Target object.
+   * @param  {mixed}    k        - Neighbor key.
+   * @return {Iterator}
+   */
+
+
+  function createIteratorForKey(object, k) {
+    var edgeData = object[k];
+
+    if (edgeData.next !== undefined) {
+      return new iterator(function () {
+        if (!edgeData) return {
+          done: true
+        };
+        var value = {
+          edge: edgeData.key,
+          attributes: edgeData.attributes,
+          source: edgeData.source.key,
+          target: edgeData.target.key,
+          sourceAttributes: edgeData.source.attributes,
+          targetAttributes: edgeData.target.attributes,
+          undirected: edgeData.undirected
+        };
+        edgeData = edgeData.next;
+        return {
+          done: false,
+          value: value
+        };
+      });
+    }
+
+    return iterator.of({
+      edge: edgeData.key,
+      attributes: edgeData.attributes,
+      source: edgeData.source.key,
+      target: edgeData.target.key,
+      sourceAttributes: edgeData.source.attributes,
+      targetAttributes: edgeData.target.attributes,
+      undirected: edgeData.undirected
+    });
+  }
+  /**
+   * Function creating an array of edges for the given type.
+   *
+   * @param  {Graph}   graph - Target Graph instance.
+   * @param  {string}  type  - Type of edges to retrieve.
+   * @return {array}         - Array of edges.
+   */
+
+
+  function createEdgeArray(graph, type) {
+    if (graph.size === 0) return [];
+
+    if (type === 'mixed' || type === graph.type) {
+      if (typeof Array.from === 'function') return Array.from(graph._edges.keys());
+      return take(graph._edges.keys(), graph._edges.size);
+    }
+
+    var size = type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+    var list = new Array(size),
+        mask = type === 'undirected';
+
+    var iterator = graph._edges.values();
+
+    var i = 0;
+    var step, data;
+
+    while (step = iterator.next(), step.done !== true) {
+      data = step.value;
+      if (data.undirected === mask) list[i++] = data.key;
+    }
+
+    return list;
+  }
+  /**
+   * Function iterating over a graph's edges using a callback to match one of
+   * them.
+   *
+   * @param  {Graph}    graph    - Target Graph instance.
+   * @param  {string}   type     - Type of edges to retrieve.
+   * @param  {function} callback - Function to call.
+   */
+
+
+  function forEachEdge(breakable, graph, type, callback) {
+    if (graph.size === 0) return;
+    var shouldFilter = type !== 'mixed' && type !== graph.type;
+    var mask = type === 'undirected';
+    var step, data;
+    var shouldBreak = false;
+
+    var iterator = graph._edges.values();
+
+    while (step = iterator.next(), step.done !== true) {
+      data = step.value;
+      if (shouldFilter && data.undirected !== mask) continue;
+      var _data = data,
+          key = _data.key,
+          attributes = _data.attributes,
+          source = _data.source,
+          target = _data.target;
+      shouldBreak = callback(key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected);
+      if (breakable && shouldBreak) return key;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an iterator of edges for the given type.
+   *
+   * @param  {Graph}    graph - Target Graph instance.
+   * @param  {string}   type  - Type of edges to retrieve.
+   * @return {Iterator}
+   */
+
+
+  function createEdgeIterator(graph, type) {
+    if (graph.size === 0) return iterator.empty();
+    var shouldFilter = type !== 'mixed' && type !== graph.type;
+    var mask = type === 'undirected';
+
+    var iterator$1 = graph._edges.values();
+
+    return new iterator(function next() {
+      var step, data; // eslint-disable-next-line no-constant-condition
+
+      while (true) {
+        step = iterator$1.next();
+        if (step.done) return step;
+        data = step.value;
+        if (shouldFilter && data.undirected !== mask) continue;
+        break;
+      }
+
+      var value = {
+        edge: data.key,
+        attributes: data.attributes,
+        source: data.source.key,
+        target: data.target.key,
+        sourceAttributes: data.source.attributes,
+        targetAttributes: data.target.attributes,
+        undirected: data.undirected
+      };
+      return {
+        value: value,
+        done: false
+      };
+    });
+  }
+  /**
+   * Function iterating over a node's edges using a callback to match one of them.
+   *
+   * @param  {boolean}  multi     - Whether the graph is multi or not.
+   * @param  {string}   type      - Type of edges to retrieve.
+   * @param  {string}   direction - In or out?
+   * @param  {any}      nodeData  - Target node's data.
+   * @param  {function} callback  - Function to call.
+   */
+
+
+  function forEachEdgeForNode(breakable, multi, type, direction, nodeData, callback) {
+    var fn = multi ? forEachMulti : forEachSimple;
+    var found;
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        found = fn(breakable, nodeData["in"], callback);
+        if (breakable && found) return found;
+      }
+
+      if (direction !== 'in') {
+        found = fn(breakable, nodeData.out, callback, !direction ? nodeData.key : undefined);
+        if (breakable && found) return found;
+      }
+    }
+
+    if (type !== 'directed') {
+      found = fn(breakable, nodeData.undirected, callback);
+      if (breakable && found) return found;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of edges for the given type & the given node.
+   *
+   * @param  {boolean} multi     - Whether the graph is multi or not.
+   * @param  {string}  type      - Type of edges to retrieve.
+   * @param  {string}  direction - In or out?
+   * @param  {any}     nodeData  - Target node's data.
+   * @return {array}             - Array of edges.
+   */
+
+
+  function createEdgeArrayForNode(multi, type, direction, nodeData) {
+    var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+    forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+      edges.push(key);
+    });
+    return edges;
+  }
+  /**
+   * Function iterating over a node's edges using a callback.
+   *
+   * @param  {string}   type      - Type of edges to retrieve.
+   * @param  {string}   direction - In or out?
+   * @param  {any}      nodeData  - Target node's data.
+   * @return {Iterator}
+   */
+
+
+  function createEdgeIteratorForNode(type, direction, nodeData) {
+    var iterator$1 = iterator.empty();
+
+    if (type !== 'undirected') {
+      if (direction !== 'out' && typeof nodeData["in"] !== 'undefined') iterator$1 = chain(iterator$1, createIterator(nodeData["in"]));
+      if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator$1 = chain(iterator$1, createIterator(nodeData.out, !direction ? nodeData.key : undefined));
+    }
+
+    if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+      iterator$1 = chain(iterator$1, createIterator(nodeData.undirected));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function iterating over edges for the given path using a callback to match
+   * one of them.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {boolean}  multi      - Whether the graph is multi.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {string}   target     - Target node.
+   * @param  {function} callback   - Function to call.
+   */
+
+
+  function forEachEdgeForPath(breakable, type, multi, direction, sourceData, target, callback) {
+    var fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+    var found;
+
+    if (type !== 'undirected') {
+      if (typeof sourceData["in"] !== 'undefined' && direction !== 'out') {
+        found = fn(breakable, sourceData["in"], target, callback);
+        if (breakable && found) return found;
+      }
+
+      if (typeof sourceData.out !== 'undefined' && direction !== 'in' && (direction || sourceData.key !== target)) {
+        found = fn(breakable, sourceData.out, target, callback);
+        if (breakable && found) return found;
+      }
+    }
+
+    if (type !== 'directed') {
+      if (typeof sourceData.undirected !== 'undefined') {
+        found = fn(breakable, sourceData.undirected, target, callback);
+        if (breakable && found) return found;
+      }
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of edges for the given path.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {boolean}  multi      - Whether the graph is multi.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {any}      target     - Target node.
+   * @return {array}               - Array of edges.
+   */
+
+
+  function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+    var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+    forEachEdgeForPath(false, type, multi, direction, sourceData, target, function (key) {
+      edges.push(key);
+    });
+    return edges;
+  }
+  /**
+   * Function returning an iterator over edges for the given path.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {string}   target     - Target node.
+   * @param  {function} callback   - Function to call.
+   */
+
+
+  function createEdgeIteratorForPath(type, direction, sourceData, target) {
+    var iterator$1 = iterator.empty();
+
+    if (type !== 'undirected') {
+      if (typeof sourceData["in"] !== 'undefined' && direction !== 'out' && target in sourceData["in"]) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData["in"], target));
+      if (typeof sourceData.out !== 'undefined' && direction !== 'in' && target in sourceData.out && (direction || sourceData.key !== target)) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData.out, target));
+    }
+
+    if (type !== 'directed') {
+      if (typeof sourceData.undirected !== 'undefined' && target in sourceData.undirected) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData.undirected, target));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function attaching an edge array creator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachEdgeArrayCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    /**
+     * Function returning an array of certain edges.
+     *
+     * Arity 0: Return all the relevant edges.
+     *
+     * Arity 1: Return all of a node's relevant edges.
+     * @param  {any}   node   - Target node.
+     *
+     * Arity 2: Return the relevant edges across the given path.
+     * @param  {any}   source - Source node.
+     * @param  {any}   target - Target node.
+     *
+     * @return {array|number} - The edges or the number of edges.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[name] = function (source, target) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+      if (!arguments.length) return createEdgeArray(this, type);
+
+      if (arguments.length === 1) {
+        source = '' + source;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+        return createEdgeArrayForNode(this.multi, type === 'mixed' ? this.type : type, direction, nodeData);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return createEdgeArrayForPath(type, this.multi, direction, sourceData, target);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+    };
+  }
+  /**
+   * Function attaching a edge callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachForEachEdge(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over the graph's relevant edges by applying the given
+     * callback.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[forEachName] = function (source, target, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+      if (arguments.length === 1) {
+        callback = source;
+        return forEachEdge(false, this, type, callback);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        callback = target;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+        // TODO: maybe attach the sub method to the instance dynamically?
+
+        return forEachEdgeForNode(false, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+      }
+
+      if (arguments.length === 3) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return forEachEdgeForPath(false, type, this.multi, direction, sourceData, target, callback);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(forEachName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+    };
+    /**
+     * Function mapping the graph's relevant edges by applying the given
+     * callback.
+     *
+     * Arity 1: Map all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Map all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Map the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[mapName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      var result; // We know the result length beforehand
+
+      if (args.length === 0) {
+        var length = 0;
+        if (type !== 'directed') length += this.undirectedSize;
+        if (type !== 'undirected') length += this.directedSize;
+        result = new Array(length);
+        var i = 0;
+        args.push(function (e, ea, s, t, sa, ta, u) {
+          result[i++] = callback(e, ea, s, t, sa, ta, u);
+        });
+      } // We don't know the result length beforehand
+      // TODO: we can in some instances of simple graphs, knowing degree
+      else {
+        result = [];
+        args.push(function (e, ea, s, t, sa, ta, u) {
+          result.push(callback(e, ea, s, t, sa, ta, u));
+        });
+      }
+
+      this[forEachName].apply(this, args);
+      return result;
+    };
+    /**
+     * Function filtering the graph's relevant edges using the provided predicate
+     * function.
+     *
+     * Arity 1: Filter all the relevant edges.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * Arity 2: Filter all of a node's relevant edges.
+     * @param  {any}      node      - Target node.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * Arity 3: Filter the relevant edges across the given path.
+     * @param  {any}      source    - Source node.
+     * @param  {any}      target    - Target node.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[filterName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      var result = [];
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+      });
+      this[forEachName].apply(this, args);
+      return result;
+    };
+    /**
+     * Function reducing the graph's relevant edges using the provided accumulator
+     * function.
+     *
+     * Arity 1: Reduce all the relevant edges.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * Arity 2: Reduce all of a node's relevant edges.
+     * @param  {any}      node         - Target node.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * Arity 3: Reduce the relevant edges across the given path.
+     * @param  {any}      source       - Source node.
+     * @param  {any}      target       - Target node.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[reduceName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+
+      if (args.length < 2 || args.length > 4) {
+        throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(args.length, ")."));
+      }
+
+      if (typeof args[args.length - 1] === 'function' && typeof args[args.length - 2] !== 'function') {
+        throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+      }
+
+      var callback;
+      var initialValue;
+
+      if (args.length === 2) {
+        callback = args[0];
+        initialValue = args[1];
+        args = [];
+      } else if (args.length === 3) {
+        callback = args[1];
+        initialValue = args[2];
+        args = [args[0]];
+      } else if (args.length === 4) {
+        callback = args[2];
+        initialValue = args[3];
+        args = [args[0], args[1]];
+      }
+
+      var accumulator = initialValue;
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+      });
+      this[forEachName].apply(this, args);
+      return accumulator;
+    };
+  }
+  /**
+   * Function attaching a breakable edge callback iterator method to the Graph
+   * prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachFindEdge(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over the graph's relevant edges in order to match
+     * one of them using the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[findEdgeName] = function (source, target, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return false;
+
+      if (arguments.length === 1) {
+        callback = source;
+        return forEachEdge(true, this, type, callback);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        callback = target;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findEdgeName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+        // TODO: maybe attach the sub method to the instance dynamically?
+
+        return forEachEdgeForNode(true, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+      }
+
+      if (arguments.length === 3) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return forEachEdgeForPath(true, type, this.multi, direction, sourceData, target, callback);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(findEdgeName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+    };
+    /**
+     * Function iterating over the graph's relevant edges in order to assert
+     * whether any one of them matches the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+    Class.prototype[someName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        return callback(e, ea, s, t, sa, ta, u);
+      });
+      var found = this[findEdgeName].apply(this, args);
+      if (found) return true;
+      return false;
+    };
+    /**
+     * Function iterating over the graph's relevant edges in order to assert
+     * whether all of them matche the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+    Class.prototype[everyName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        return !callback(e, ea, s, t, sa, ta, u);
+      });
+      var found = this[findEdgeName].apply(this, args);
+      if (found) return false;
+      return true;
+    };
+  }
+  /**
+   * Function attaching an edge iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachEdgeIteratorCreator(Class, description) {
+    var originalName = description.name,
+        type = description.type,
+        direction = description.direction;
+    var name = originalName.slice(0, -1) + 'Entries';
+    /**
+     * Function returning an iterator over the graph's edges.
+     *
+     * Arity 0: Iterate over all the relevant edges.
+     *
+     * Arity 1: Iterate over all of a node's relevant edges.
+     * @param  {any}   node   - Target node.
+     *
+     * Arity 2: Iterate over the relevant edges across the given path.
+     * @param  {any}   source - Source node.
+     * @param  {any}   target - Target node.
+     *
+     * @return {array|number} - The edges or the number of edges.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[name] = function (source, target) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return iterator.empty();
+      if (!arguments.length) return createEdgeIterator(this, type);
+
+      if (arguments.length === 1) {
+        source = '' + source;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+        return createEdgeIteratorForNode(type, direction, sourceData);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target;
+
+        var _sourceData = this._nodes.get(source);
+
+        if (!_sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return createEdgeIteratorForPath(type, direction, _sourceData, target);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+    };
+  }
+  /**
+   * Function attaching every edge iteration method to the Graph class.
+   *
+   * @param {function} Graph - Graph class.
+   */
+
+
+  function attachEdgeIterationMethods(Graph) {
+    EDGES_ITERATION.forEach(function (description) {
+      attachEdgeArrayCreator(Graph, description);
+      attachForEachEdge(Graph, description);
+      attachFindEdge(Graph, description);
+      attachEdgeIteratorCreator(Graph, description);
+    });
+  }
+
+  /**
+   * Graphology Neighbor Iteration
+   * ==============================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over
+   * neighbors.
+   */
+  /**
+   * Definitions.
+   */
+
+  var NEIGHBORS_ITERATION = [{
+    name: 'neighbors',
+    type: 'mixed'
+  }, {
+    name: 'inNeighbors',
+    type: 'directed',
+    direction: 'in'
+  }, {
+    name: 'outNeighbors',
+    type: 'directed',
+    direction: 'out'
+  }, {
+    name: 'inboundNeighbors',
+    type: 'mixed',
+    direction: 'in'
+  }, {
+    name: 'outboundNeighbors',
+    type: 'mixed',
+    direction: 'out'
+  }, {
+    name: 'directedNeighbors',
+    type: 'directed'
+  }, {
+    name: 'undirectedNeighbors',
+    type: 'undirected'
+  }];
+  /**
+   * Helpers.
+   */
+
+  function CompositeSetWrapper() {
+    this.A = null;
+    this.B = null;
+  }
+
+  CompositeSetWrapper.prototype.wrap = function (set) {
+    if (this.A === null) this.A = set;else if (this.B === null) this.B = set;
+  };
+
+  CompositeSetWrapper.prototype.has = function (key) {
+    if (this.A !== null && key in this.A) return true;
+    if (this.B !== null && key in this.B) return true;
+    return false;
+  };
+  /**
+   * Function iterating over the given node's relevant neighbors to match
+   * one of them using a predicated function.
+   *
+   * @param  {string}   type      - Type of neighbors.
+   * @param  {string}   direction - Direction.
+   * @param  {any}      nodeData  - Target node's data.
+   * @param  {function} callback  - Callback to use.
+   */
+
+
+  function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+    for (var k in object) {
+      var edgeData = object[k];
+      var sourceData = edgeData.source;
+      var targetData = edgeData.target;
+      var neighborData = sourceData === nodeData ? targetData : sourceData;
+      if (visited && visited.has(neighborData.key)) continue;
+      var shouldBreak = callback(neighborData.key, neighborData.attributes);
+      if (breakable && shouldBreak) return neighborData.key;
+    }
+
+    return;
+  }
+
+  function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return forEachInObjectOnce(breakable, null, nodeData, nodeData.undirected, callback);
+      if (typeof direction === 'string') return forEachInObjectOnce(breakable, null, nodeData, nodeData[direction], callback);
+    } // Else we need to keep a set of neighbors not to return duplicates
+    // We cheat by querying the other adjacencies
+
+
+    var visited = new CompositeSetWrapper();
+    var found;
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        found = forEachInObjectOnce(breakable, null, nodeData, nodeData["in"], callback);
+        if (breakable && found) return found;
+        visited.wrap(nodeData["in"]);
+      }
+
+      if (direction !== 'in') {
+        found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.out, callback);
+        if (breakable && found) return found;
+        visited.wrap(nodeData.out);
+      }
+    }
+
+    if (type !== 'directed') {
+      found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.undirected, callback);
+      if (breakable && found) return found;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of relevant neighbors for the given node.
+   *
+   * @param  {string}       type      - Type of neighbors.
+   * @param  {string}       direction - Direction.
+   * @param  {any}          nodeData  - Target node's data.
+   * @return {Array}                  - The list of neighbors.
+   */
+
+
+  function createNeighborArrayForNode(type, direction, nodeData) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return Object.keys(nodeData.undirected);
+      if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+    }
+
+    var neighbors = [];
+    forEachNeighbor(false, type, direction, nodeData, function (key) {
+      neighbors.push(key);
+    });
+    return neighbors;
+  }
+  /**
+   * Function returning an iterator over the given node's relevant neighbors.
+   *
+   * @param  {string}   type      - Type of neighbors.
+   * @param  {string}   direction - Direction.
+   * @param  {any}      nodeData  - Target node's data.
+   * @return {Iterator}
+   */
+
+
+  function createDedupedObjectIterator(visited, nodeData, object) {
+    var keys = Object.keys(object);
+    var l = keys.length;
+    var i = 0;
+    return new iterator(function next() {
+      var neighborData = null;
+
+      do {
+        if (i >= l) {
+          if (visited) visited.wrap(object);
+          return {
+            done: true
+          };
+        }
+
+        var edgeData = object[keys[i++]];
+        var sourceData = edgeData.source;
+        var targetData = edgeData.target;
+        neighborData = sourceData === nodeData ? targetData : sourceData;
+
+        if (visited && visited.has(neighborData.key)) {
+          neighborData = null;
+          continue;
+        }
+      } while (neighborData === null);
+
+      return {
+        done: false,
+        value: {
+          neighbor: neighborData.key,
+          attributes: neighborData.attributes
+        }
+      };
+    });
+  }
+
+  function createNeighborIterator(type, direction, nodeData) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+      if (typeof direction === 'string') return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+    }
+
+    var iterator$1 = iterator.empty(); // Else we need to keep a set of neighbors not to return duplicates
+    // We cheat by querying the other adjacencies
+
+    var visited = new CompositeSetWrapper();
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData["in"]));
+      }
+
+      if (direction !== 'in') {
+        iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData.out));
+      }
+    }
+
+    if (type !== 'directed') {
+      iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData.undirected));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function attaching a neighbors array creator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachNeighborArrayCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    /**
+     * Function returning an array of certain neighbors.
+     *
+     * @param  {any}   node   - Target node.
+     * @return {array} - The neighbors of neighbors.
+     *
+     * @throws {Error} - Will throw if node is not found in the graph.
+     */
+
+    Class.prototype[name] = function (node) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return createNeighborArrayForNode(type === 'mixed' ? this.type : type, direction, nodeData);
+    };
+  }
+  /**
+   * Function attaching a neighbors callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachForEachNeighbor(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over all the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[forEachName] = function (node, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      forEachNeighbor(false, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    };
+    /**
+     * Function mapping the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[mapName] = function (node, callback) {
+      // TODO: optimize when size is known beforehand
+      var result = [];
+      this[forEachName](node, function (n, a) {
+        result.push(callback(n, a));
+      });
+      return result;
+    };
+    /**
+     * Function filtering the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[filterName] = function (node, callback) {
+      var result = [];
+      this[forEachName](node, function (n, a) {
+        if (callback(n, a)) result.push(n);
+      });
+      return result;
+    };
+    /**
+     * Function reducing the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[reduceName] = function (node, callback, initialValue) {
+      if (arguments.length < 3) throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+      var accumulator = initialValue;
+      this[forEachName](node, function (n, a) {
+        accumulator = callback(accumulator, n, a);
+      });
+      return accumulator;
+    };
+  }
+  /**
+   * Function attaching a breakable neighbors callback iterator method to the
+   * Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachFindNeighbor(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+    var findName = 'find' + capitalizedSingular;
+    /**
+     * Function iterating over all the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[findName] = function (node, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return forEachNeighbor(true, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    };
+    /**
+     * Function iterating over all the relevant neighbors to find if any of them
+     * matches the given predicate.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var someName = 'some' + capitalizedSingular;
+
+    Class.prototype[someName] = function (node, callback) {
+      var found = this[findName](node, callback);
+      if (found) return true;
+      return false;
+    };
+    /**
+     * Function iterating over all the relevant neighbors to find if all of them
+     * matche the given predicate.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var everyName = 'every' + capitalizedSingular;
+
+    Class.prototype[everyName] = function (node, callback) {
+      var found = this[findName](node, function (n, a) {
+        return !callback(n, a);
+      });
+      if (found) return false;
+      return true;
+    };
+  }
+  /**
+   * Function attaching a neighbors callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachNeighborIteratorCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    /**
+     * Function returning an iterator over all the relevant neighbors.
+     *
+     * @param  {any}      node     - Target node.
+     * @return {Iterator}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[iteratorName] = function (node) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return iterator.empty();
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(iteratorName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return createNeighborIterator(type === 'mixed' ? this.type : type, direction, nodeData);
+    };
+  }
+  /**
+   * Function attaching every neighbor iteration method to the Graph class.
+   *
+   * @param {function} Graph - Graph class.
+   */
+
+
+  function attachNeighborIterationMethods(Graph) {
+    NEIGHBORS_ITERATION.forEach(function (description) {
+      attachNeighborArrayCreator(Graph, description);
+      attachForEachNeighbor(Graph, description);
+      attachFindNeighbor(Graph, description);
+      attachNeighborIteratorCreator(Graph, description);
+    });
+  }
+
+  /**
+   * Graphology Adjacency Iteration
+   * ===============================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over a
+   * graph's adjacency.
+   */
+
+  /**
+   * Function iterating over a simple graph's adjacency using a callback.
+   *
+   * @param {boolean}  breakable         - Can we break?
+   * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+   * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+   * @param {Graph}    graph             - Target Graph instance.
+   * @param {callback} function          - Iteration callback.
+   */
+  function forEachAdjacency(breakable, assymetric, disconnectedNodes, graph, callback) {
+    var iterator = graph._nodes.values();
+
+    var type = graph.type;
+    var step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+    while (step = iterator.next(), step.done !== true) {
+      var hasEdges = false;
+      sourceData = step.value;
+
+      if (type !== 'undirected') {
+        adj = sourceData.out;
+
+        for (neighbor in adj) {
+          edgeData = adj[neighbor];
+
+          do {
+            targetData = edgeData.target;
+            hasEdges = true;
+            shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+            if (breakable && shouldBreak) return edgeData;
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (type !== 'directed') {
+        adj = sourceData.undirected;
+
+        for (neighbor in adj) {
+          if (assymetric && sourceData.key > neighbor) continue;
+          edgeData = adj[neighbor];
+
+          do {
+            targetData = edgeData.target;
+            if (targetData.key !== neighbor) targetData = edgeData.source;
+            hasEdges = true;
+            shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+            if (breakable && shouldBreak) return edgeData;
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (disconnectedNodes && !hasEdges) {
+        shouldBreak = callback(sourceData.key, null, sourceData.attributes, null, null, null, null);
+        if (breakable && shouldBreak) return null;
+      }
+    }
+
+    return;
+  }
+
+  /**
+   * Graphology Serialization Utilities
+   * ===================================
+   *
+   * Collection of functions used by the graph serialization schemes.
+   */
+  /**
+   * Formats internal node data into a serialized node.
+   *
+   * @param  {any}    key  - The node's key.
+   * @param  {object} data - Internal node's data.
+   * @return {array}       - The serialized node.
+   */
+
+  function serializeNode(key, data) {
+    var serialized = {
+      key: key
+    };
+    if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+    return serialized;
+  }
+  /**
+   * Formats internal edge data into a serialized edge.
+   *
+   * @param  {any}    key  - The edge's key.
+   * @param  {object} data - Internal edge's data.
+   * @return {array}       - The serialized edge.
+   */
+
+  function serializeEdge(key, data) {
+    var serialized = {
+      key: key,
+      source: data.source.key,
+      target: data.target.key
+    };
+    if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+    if (data.undirected) serialized.undirected = true;
+    return serialized;
+  }
+  /**
+   * Checks whether the given value is a serialized node.
+   *
+   * @param  {mixed} value - Target value.
+   * @return {string|null}
+   */
+
+  function validateSerializedNode(value) {
+    if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');
+    if (!('key' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized node is missing its key.');
+    if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+  }
+  /**
+   * Checks whether the given value is a serialized edge.
+   *
+   * @param  {mixed} value - Target value.
+   * @return {string|null}
+   */
+
+  function validateSerializedEdge(value) {
+    if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');
+    if (!('source' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its source.');
+    if (!('target' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its target.');
+    if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+    if ('undirected' in value && typeof value.undirected !== 'boolean') throw new InvalidArgumentsGraphError('Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.');
+  }
+
+  /**
+   * Constants.
+   */
+
+  var INSTANCE_ID = incrementalIdStartingFromRandomByte();
+  /**
+   * Enums.
+   */
+
+  var TYPES = new Set(['directed', 'undirected', 'mixed']);
+  var EMITTER_PROPS = new Set(['domain', '_events', '_eventsCount', '_maxListeners']);
+  var EDGE_ADD_METHODS = [{
+    name: function name(verb) {
+      return "".concat(verb, "Edge");
+    },
+    generateKey: true
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "DirectedEdge");
+    },
+    generateKey: true,
+    type: 'directed'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "UndirectedEdge");
+    },
+    generateKey: true,
+    type: 'undirected'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "EdgeWithKey");
+    }
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "DirectedEdgeWithKey");
+    },
+    type: 'directed'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "UndirectedEdgeWithKey");
+    },
+    type: 'undirected'
+  }];
+  /**
+   * Default options.
+   */
+
+  var DEFAULTS = {
+    allowSelfLoops: true,
+    multi: false,
+    type: 'mixed'
+  };
+  /**
+   * Abstract functions used by the Graph class for various methods.
+   */
+
+  /**
+   * Internal method used to add a node to the given graph
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {any}     node            - The node's key.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @return {NodeData}                - Created node data.
+   */
+
+  function _addNode(graph, node, attributes) {
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.addNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+    node = '' + node;
+    attributes = attributes || {};
+    if (graph._nodes.has(node)) throw new UsageGraphError("Graph.addNode: the \"".concat(node, "\" node already exist in the graph."));
+    var data = new graph.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    graph._nodes.set(node, data); // Emitting
+
+
+    graph.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return data;
+  }
+  /**
+   * Same as the above but without sanity checks because we call this in contexts
+   * where necessary checks were already done.
+   */
+
+
+  function unsafeAddNode(graph, node, attributes) {
+    var data = new graph.NodeDataClass(node, attributes);
+
+    graph._nodes.set(node, data);
+
+    graph.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return data;
+  }
+  /**
+   * Internal method used to add an arbitrary edge to the given graph.
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {string}  name            - Name of the child method for errors.
+   * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+   * @param  {boolean} undirected      - Whether the edge is undirected.
+   * @param  {any}     edge            - The edge's key.
+   * @param  {any}     source          - The source node.
+   * @param  {any}     target          - The target node.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @return {any}                     - The edge.
+   *
+   * @throws {Error} - Will throw if the graph is of the wrong type.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   * @throws {Error} - Will throw if the edge already exist.
+   */
+
+
+  function addEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes) {
+    // Checking validity of operation
+    if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));
+    if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\"")); // Coercion of source & target:
+
+    source = '' + source;
+    target = '' + target;
+    attributes = attributes || {};
+    if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+    var sourceData = graph._nodes.get(source),
+        targetData = graph._nodes.get(target);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": source node \"").concat(source, "\" not found."));
+    if (!targetData) throw new NotFoundGraphError("Graph.".concat(name, ": target node \"").concat(target, "\" not found.")); // Must the graph generate an id for this edge?
+
+    var eventData = {
+      key: null,
+      undirected: undirected,
+      source: source,
+      target: target,
+      attributes: attributes
+    };
+
+    if (mustGenerateKey) {
+      // NOTE: in this case we can guarantee that the key does not already
+      // exist and is already correctly casted as a string
+      edge = graph._edgeKeyGenerator();
+    } else {
+      // Coercion of edge key
+      edge = '' + edge; // Here, we have a key collision
+
+      if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+    } // Here, we might have a source / target collision
+
+
+    if (!graph.multi && (undirected ? typeof sourceData.undirected[target] !== 'undefined' : typeof sourceData.out[target] !== 'undefined')) {
+      throw new UsageGraphError("Graph.".concat(name, ": an edge linking \"").concat(source, "\" to \"").concat(target, "\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));
+    } // Storing some data
+
+
+    var edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+    graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+    var isSelfLoop = source === target;
+
+    if (undirected) {
+      sourceData.undirectedDegree++;
+      targetData.undirectedDegree++;
+      if (isSelfLoop) graph._undirectedSelfLoopCount++;
+    } else {
+      sourceData.outDegree++;
+      targetData.inDegree++;
+      if (isSelfLoop) graph._directedSelfLoopCount++;
+    } // Updating relevant index
+
+
+    if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+    if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+    eventData.key = edge;
+    graph.emit('edgeAdded', eventData);
+    return edge;
+  }
+  /**
+   * Internal method used to add an arbitrary edge to the given graph.
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {string}  name            - Name of the child method for errors.
+   * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+   * @param  {boolean} undirected      - Whether the edge is undirected.
+   * @param  {any}     edge            - The edge's key.
+   * @param  {any}     source          - The source node.
+   * @param  {any}     target          - The target node.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @param  {boolean} [asUpdater]       - Are we updating or merging?
+   * @return {any}                     - The edge.
+   *
+   * @throws {Error} - Will throw if the graph is of the wrong type.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   * @throws {Error} - Will throw if the edge already exist.
+   */
+
+
+  function mergeEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes, asUpdater) {
+    // Checking validity of operation
+    if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));
+    if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));
+
+    if (attributes) {
+      if (asUpdater) {
+        if (typeof attributes !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid updater function. Expecting a function but got \"").concat(attributes, "\""));
+      } else {
+        if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\""));
+      }
+    } // Coercion of source & target:
+
+
+    source = '' + source;
+    target = '' + target;
+    var updater;
+
+    if (asUpdater) {
+      updater = attributes;
+      attributes = undefined;
+    }
+
+    if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+    var sourceData = graph._nodes.get(source);
+
+    var targetData = graph._nodes.get(target);
+
+    var edgeData; // Do we need to handle duplicate?
+
+    var alreadyExistingEdgeData;
+
+    if (!mustGenerateKey) {
+      edgeData = graph._edges.get(edge);
+
+      if (edgeData) {
+        // Here, we need to ensure, if the user gave a key, that source & target
+        // are consistent
+        if (edgeData.source.key !== source || edgeData.target.key !== target) {
+          // If source or target inconsistent
+          if (!undirected || edgeData.source.key !== target || edgeData.target.key !== source) {
+            // If directed, or source/target aren't flipped
+            throw new UsageGraphError("Graph.".concat(name, ": inconsistency detected when attempting to merge the \"").concat(edge, "\" edge with \"").concat(source, "\" source & \"").concat(target, "\" target vs. (\"").concat(edgeData.source.key, "\", \"").concat(edgeData.target.key, "\")."));
+          }
+        }
+
+        alreadyExistingEdgeData = edgeData;
+      }
+    } // Here, we might have a source / target collision
+
+
+    if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+      alreadyExistingEdgeData = undirected ? sourceData.undirected[target] : sourceData.out[target];
+    } // Handling duplicates
+
+
+    if (alreadyExistingEdgeData) {
+      var info = [alreadyExistingEdgeData.key, false, false, false]; // We can skip the attribute merging part if the user did not provide them
+
+      if (asUpdater ? !updater : !attributes) return info; // Updating the attributes
+
+      if (asUpdater) {
+        var oldAttributes = alreadyExistingEdgeData.attributes;
+        alreadyExistingEdgeData.attributes = updater(oldAttributes);
+        graph.emit('edgeAttributesUpdated', {
+          type: 'replace',
+          key: alreadyExistingEdgeData.key,
+          attributes: alreadyExistingEdgeData.attributes
+        });
+      } // Merging the attributes
+      else {
+        assign(alreadyExistingEdgeData.attributes, attributes);
+        graph.emit('edgeAttributesUpdated', {
+          type: 'merge',
+          key: alreadyExistingEdgeData.key,
+          attributes: alreadyExistingEdgeData.attributes,
+          data: attributes
+        });
+      }
+
+      return info;
+    }
+
+    attributes = attributes || {};
+    if (asUpdater && updater) attributes = updater(attributes); // Must the graph generate an id for this edge?
+
+    var eventData = {
+      key: null,
+      undirected: undirected,
+      source: source,
+      target: target,
+      attributes: attributes
+    };
+
+    if (mustGenerateKey) {
+      // NOTE: in this case we can guarantee that the key does not already
+      // exist and is already correctly casted as a string
+      edge = graph._edgeKeyGenerator();
+    } else {
+      // Coercion of edge key
+      edge = '' + edge; // Here, we have a key collision
+
+      if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+    }
+
+    var sourceWasAdded = false;
+    var targetWasAdded = false;
+
+    if (!sourceData) {
+      sourceData = unsafeAddNode(graph, source, {});
+      sourceWasAdded = true;
+
+      if (source === target) {
+        targetData = sourceData;
+        targetWasAdded = true;
+      }
+    }
+
+    if (!targetData) {
+      targetData = unsafeAddNode(graph, target, {});
+      targetWasAdded = true;
+    } // Storing some data
+
+
+    edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+    graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+    var isSelfLoop = source === target;
+
+    if (undirected) {
+      sourceData.undirectedDegree++;
+      targetData.undirectedDegree++;
+      if (isSelfLoop) graph._undirectedSelfLoopCount++;
+    } else {
+      sourceData.outDegree++;
+      targetData.inDegree++;
+      if (isSelfLoop) graph._directedSelfLoopCount++;
+    } // Updating relevant index
+
+
+    if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+    if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+    eventData.key = edge;
+    graph.emit('edgeAdded', eventData);
+    return [edge, true, sourceWasAdded, targetWasAdded];
+  }
+  /**
+   * Internal method used to drop an edge.
+   *
+   * @param  {Graph}    graph    - Target graph.
+   * @param  {EdgeData} edgeData - Data of the edge to drop.
+   */
+
+
+  function dropEdgeFromData(graph, edgeData) {
+    // Dropping the edge from the register
+    graph._edges["delete"](edgeData.key); // Updating related degrees
+
+
+    var sourceData = edgeData.source,
+        targetData = edgeData.target,
+        attributes = edgeData.attributes;
+    var undirected = edgeData.undirected;
+    var isSelfLoop = sourceData === targetData;
+
+    if (undirected) {
+      sourceData.undirectedDegree--;
+      targetData.undirectedDegree--;
+      if (isSelfLoop) graph._undirectedSelfLoopCount--;
+    } else {
+      sourceData.outDegree--;
+      targetData.inDegree--;
+      if (isSelfLoop) graph._directedSelfLoopCount--;
+    } // Clearing index
+
+
+    if (graph.multi) edgeData.detachMulti();else edgeData.detach();
+    if (undirected) graph._undirectedSize--;else graph._directedSize--; // Emitting
+
+    graph.emit('edgeDropped', {
+      key: edgeData.key,
+      attributes: attributes,
+      source: sourceData.key,
+      target: targetData.key,
+      undirected: undirected
+    });
+  }
+  /**
+   * Graph class
+   *
+   * @constructor
+   * @param  {object}  [options] - Options:
+   * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+   * @param  {string}    [type]           - Type of the graph.
+   * @param  {boolean}   [map]            - Allow references as keys?
+   * @param  {boolean}   [multi]          - Allow parallel edges?
+   *
+   * @throws {Error} - Will throw if the arguments are not valid.
+   */
+
+
+  var Graph = /*#__PURE__*/function (_EventEmitter) {
+    _inheritsLoose(Graph, _EventEmitter);
+
+    function Graph(options) {
+      var _this;
+
+      _this = _EventEmitter.call(this) || this; //-- Solving options
+
+      options = assign({}, DEFAULTS, options); // Enforcing options validity
+
+      if (typeof options.multi !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(options.multi, "\"."));
+      if (!TYPES.has(options.type)) throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'type' option. Should be one of \"mixed\", \"directed\" or \"undirected\" but got \"".concat(options.type, "\"."));
+      if (typeof options.allowSelfLoops !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(options.allowSelfLoops, "\".")); //-- Private properties
+      // Utilities
+
+      var NodeDataClass = options.type === 'mixed' ? MixedNodeData : options.type === 'directed' ? DirectedNodeData : UndirectedNodeData;
+      privateProperty(_assertThisInitialized(_this), 'NodeDataClass', NodeDataClass); // Internal edge key generator
+      // NOTE: this internal generator produce keys that are strings
+      // composed of a weird prefix, an incremental instance id starting from
+      // a random byte and finally an internal instance incremental id.
+      // All this to avoid intra-frame and cross-frame adversarial inputs
+      // that can force a single #.addEdge call to degenerate into a O(n)
+      // available key search loop.
+      // It also ensures that automatically generated edge keys are unlikely
+      // to produce collisions with arbitrary keys given by users.
+
+      var instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+      var edgeId = 0;
+
+      var edgeKeyGenerator = function edgeKeyGenerator() {
+        var availableEdgeKey;
+
+        do {
+          availableEdgeKey = instancePrefix + edgeId++;
+        } while (_this._edges.has(availableEdgeKey));
+
+        return availableEdgeKey;
+      }; // Indexes
+
+
+      privateProperty(_assertThisInitialized(_this), '_attributes', {});
+      privateProperty(_assertThisInitialized(_this), '_nodes', new Map());
+      privateProperty(_assertThisInitialized(_this), '_edges', new Map());
+      privateProperty(_assertThisInitialized(_this), '_directedSize', 0);
+      privateProperty(_assertThisInitialized(_this), '_undirectedSize', 0);
+      privateProperty(_assertThisInitialized(_this), '_directedSelfLoopCount', 0);
+      privateProperty(_assertThisInitialized(_this), '_undirectedSelfLoopCount', 0);
+      privateProperty(_assertThisInitialized(_this), '_edgeKeyGenerator', edgeKeyGenerator); // Options
+
+      privateProperty(_assertThisInitialized(_this), '_options', options); // Emitter properties
+
+      EMITTER_PROPS.forEach(function (prop) {
+        return privateProperty(_assertThisInitialized(_this), prop, _this[prop]);
+      }); //-- Properties readers
+
+      readOnlyProperty(_assertThisInitialized(_this), 'order', function () {
+        return _this._nodes.size;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'size', function () {
+        return _this._edges.size;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'directedSize', function () {
+        return _this._directedSize;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'undirectedSize', function () {
+        return _this._undirectedSize;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'selfLoopCount', function () {
+        return _this._directedSelfLoopCount + _this._undirectedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'directedSelfLoopCount', function () {
+        return _this._directedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'undirectedSelfLoopCount', function () {
+        return _this._undirectedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'multi', _this._options.multi);
+      readOnlyProperty(_assertThisInitialized(_this), 'type', _this._options.type);
+      readOnlyProperty(_assertThisInitialized(_this), 'allowSelfLoops', _this._options.allowSelfLoops);
+      readOnlyProperty(_assertThisInitialized(_this), 'implementation', function () {
+        return 'graphology';
+      });
+      return _this;
+    }
+
+    var _proto = Graph.prototype;
+
+    _proto._resetInstanceCounters = function _resetInstanceCounters() {
+      this._directedSize = 0;
+      this._undirectedSize = 0;
+      this._directedSelfLoopCount = 0;
+      this._undirectedSelfLoopCount = 0;
+    }
+    /**---------------------------------------------------------------------------
+     * Read
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning whether the given node is found in the graph.
+     *
+     * @param  {any}     node - The node.
+     * @return {boolean}
+     */
+    ;
+
+    _proto.hasNode = function hasNode(node) {
+      return this._nodes.has('' + node);
+    }
+    /**
+     * Method returning whether the given directed edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasDirectedEdge = function hasDirectedEdge(source, target) {
+      // Early termination
+      if (this.type === 'undirected') return false;
+
+      if (arguments.length === 1) {
+        var edge = '' + source;
+
+        var edgeData = this._edges.get(edge);
+
+        return !!edgeData && !edgeData.undirected;
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = nodeData.out[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning whether the given undirected edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasUndirectedEdge = function hasUndirectedEdge(source, target) {
+      // Early termination
+      if (this.type === 'directed') return false;
+
+      if (arguments.length === 1) {
+        var edge = '' + source;
+
+        var edgeData = this._edges.get(edge);
+
+        return !!edgeData && edgeData.undirected;
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = nodeData.undirected[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning whether the given edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasEdge = function hasEdge(source, target) {
+      if (arguments.length === 1) {
+        var edge = '' + source;
+        return this._edges.has(edge);
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+        if (!edges) edges = typeof nodeData.undirected !== 'undefined' && nodeData.undirected[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning the edge matching source & target in a directed fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.directedEdge = function directedEdge(source, target) {
+      if (this.type === 'undirected') return;
+      source = '' + source;
+      target = '' + target;
+      if (this.multi) throw new UsageGraphError('Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.');
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.out && sourceData.out[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning the edge matching source & target in a undirected fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.undirectedEdge = function undirectedEdge(source, target) {
+      if (this.type === 'directed') return;
+      source = '' + source;
+      target = '' + target;
+      if (this.multi) throw new UsageGraphError('Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.');
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.undirected && sourceData.undirected[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning the edge matching source & target in a mixed fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.edge = function edge(source, target) {
+      if (this.multi) throw new UsageGraphError('Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.');
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning whether two nodes are directed neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areDirectedNeighbors = function areDirectedNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areDirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData["in"] || neighbor in nodeData.out;
+    }
+    /**
+     * Method returning whether two nodes are out neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areOutNeighbors = function areOutNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areOutNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData.out;
+    }
+    /**
+     * Method returning whether two nodes are in neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areInNeighbors = function areInNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areInNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData["in"];
+    }
+    /**
+     * Method returning whether two nodes are undirected neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areUndirectedNeighbors = function areUndirectedNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areUndirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return false;
+      return neighbor in nodeData.undirected;
+    }
+    /**
+     * Method returning whether two nodes are neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areNeighbors = function areNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData["in"] || neighbor in nodeData.out) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether two nodes are inbound neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areInboundNeighbors = function areInboundNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areInboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData["in"]) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether two nodes are outbound neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areOutboundNeighbors = function areOutboundNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areOutboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData.out) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning the given node's in degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inDegree = function inDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.inDegree;
+    }
+    /**
+     * Method returning the given node's out degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outDegree = function outDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.outDegree;
+    }
+    /**
+     * Method returning the given node's directed degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.directedDegree = function directedDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.directedDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.inDegree + nodeData.outDegree;
+    }
+    /**
+     * Method returning the given node's undirected degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.undirectedDegree = function undirectedDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return 0;
+      return nodeData.undirectedDegree;
+    }
+    /**
+     * Method returning the given node's inbound degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's inbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inboundDegree = function inboundDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's outbound degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's outbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outboundDegree = function outboundDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.outDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's directed degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.degree = function degree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.degree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree + nodeData.outDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's in degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inDegreeWithoutSelfLoops = function inDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData["in"][node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.inDegree - loops;
+    }
+    /**
+     * Method returning the given node's out degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outDegreeWithoutSelfLoops = function outDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData.out[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.outDegree - loops;
+    }
+    /**
+     * Method returning the given node's directed degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.directedDegreeWithoutSelfLoops = function directedDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.directedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData.out[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.inDegree + nodeData.outDegree - loops * 2;
+    }
+    /**
+     * Method returning the given node's undirected degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.undirectedDegreeWithoutSelfLoops = function undirectedDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return 0;
+      var self = nodeData.undirected[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.undirectedDegree - loops * 2;
+    }
+    /**
+     * Method returning the given node's inbound degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's inbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inboundDegreeWithoutSelfLoops = function inboundDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree;
+        self = nodeData.out[node];
+        loops += self ? this.multi ? self.size : 1 : 0;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given node's outbound degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's outbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outboundDegreeWithoutSelfLoops = function outboundDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.outDegree;
+        self = nodeData["in"][node];
+        loops += self ? this.multi ? self.size : 1 : 0;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given node's directed degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.degreeWithoutSelfLoops = function degreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.degreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree + nodeData.outDegree;
+        self = nodeData.out[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given edge's source.
+     *
+     * @param  {any} edge - The edge's key.
+     * @return {any}      - The edge's source.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.source = function source(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.source: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source.key;
+    }
+    /**
+     * Method returning the given edge's target.
+     *
+     * @param  {any} edge - The edge's key.
+     * @return {any}      - The edge's target.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.target = function target(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.target: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.target.key;
+    }
+    /**
+     * Method returning the given edge's extremities.
+     *
+     * @param  {any}   edge - The edge's key.
+     * @return {array}      - The edge's extremities.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.extremities = function extremities(edge) {
+      edge = '' + edge;
+
+      var edgeData = this._edges.get(edge);
+
+      if (!edgeData) throw new NotFoundGraphError("Graph.extremities: could not find the \"".concat(edge, "\" edge in the graph."));
+      return [edgeData.source.key, edgeData.target.key];
+    }
+    /**
+     * Given a node & an edge, returns the other extremity of the edge.
+     *
+     * @param  {any}   node - The node's key.
+     * @param  {any}   edge - The edge's key.
+     * @return {any}        - The related node.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph or if the
+     *                   edge & node are not related.
+     */
+    ;
+
+    _proto.opposite = function opposite(node, edge) {
+      node = '' + node;
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.opposite: could not find the \"".concat(edge, "\" edge in the graph."));
+      var source = data.source.key;
+      var target = data.target.key;
+      if (node === source) return target;
+      if (node === target) return source;
+      throw new NotFoundGraphError("Graph.opposite: the \"".concat(node, "\" node is not attached to the \"").concat(edge, "\" edge (").concat(source, ", ").concat(target, ")."));
+    }
+    /**
+     * Returns whether the given edge has the given node as extremity.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @param  {any}     node - The node's key.
+     * @return {boolean}      - The related node.
+     *
+     * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+     */
+    ;
+
+    _proto.hasExtremity = function hasExtremity(edge, node) {
+      edge = '' + edge;
+      node = '' + node;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.hasExtremity: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source.key === node || data.target.key === node;
+    }
+    /**
+     * Method returning whether the given edge is undirected.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isUndirected = function isUndirected(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isUndirected: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.undirected;
+    }
+    /**
+     * Method returning whether the given edge is directed.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isDirected = function isDirected(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isDirected: could not find the \"".concat(edge, "\" edge in the graph."));
+      return !data.undirected;
+    }
+    /**
+     * Method returning whether the given edge is a self loop.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isSelfLoop = function isSelfLoop(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isSelfLoop: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source === data.target;
+    }
+    /**---------------------------------------------------------------------------
+     * Mutation
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used to add a node to the graph.
+     *
+     * @param  {any}    node         - The node.
+     * @param  {object} [attributes] - Optional attributes.
+     * @return {any}                 - The node.
+     *
+     * @throws {Error} - Will throw if the given node already exist.
+     * @throws {Error} - Will throw if the given attributes are not an object.
+     */
+    ;
+
+    _proto.addNode = function addNode(node, attributes) {
+      var nodeData = _addNode(this, node, attributes);
+
+      return nodeData.key;
+    }
+    /**
+     * Method used to merge a node into the graph.
+     *
+     * @param  {any}    node         - The node.
+     * @param  {object} [attributes] - Optional attributes.
+     * @return {any}                 - The node.
+     */
+    ;
+
+    _proto.mergeNode = function mergeNode(node, attributes) {
+      if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.mergeNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+      node = '' + node;
+      attributes = attributes || {}; // If the node already exists, we merge the attributes
+
+      var data = this._nodes.get(node);
+
+      if (data) {
+        if (attributes) {
+          assign(data.attributes, attributes);
+          this.emit('nodeAttributesUpdated', {
+            type: 'merge',
+            key: node,
+            attributes: data.attributes,
+            data: attributes
+          });
+        }
+
+        return [node, false];
+      }
+
+      data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+      this._nodes.set(node, data); // Emitting
+
+
+      this.emit('nodeAdded', {
+        key: node,
+        attributes: attributes
+      });
+      return [node, true];
+    }
+    /**
+     * Method used to add a node if it does not exist in the graph or else to
+     * update its attributes using a function.
+     *
+     * @param  {any}      node      - The node.
+     * @param  {function} [updater] - Optional updater function.
+     * @return {any}                - The node.
+     */
+    ;
+
+    _proto.updateNode = function updateNode(node, updater) {
+      if (updater && typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.updateNode: invalid updater function. Expecting a function but got \"".concat(updater, "\"")); // String coercion
+
+      node = '' + node; // If the node already exists, we update the attributes
+
+      var data = this._nodes.get(node);
+
+      if (data) {
+        if (updater) {
+          var oldAttributes = data.attributes;
+          data.attributes = updater(oldAttributes);
+          this.emit('nodeAttributesUpdated', {
+            type: 'replace',
+            key: node,
+            attributes: data.attributes
+          });
+        }
+
+        return [node, false];
+      }
+
+      var attributes = updater ? updater({}) : {};
+      data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+      this._nodes.set(node, data); // Emitting
+
+
+      this.emit('nodeAdded', {
+        key: node,
+        attributes: attributes
+      });
+      return [node, true];
+    }
+    /**
+     * Method used to drop a single node & all its attached edges from the graph.
+     *
+     * @param  {any}    node - The node.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the node doesn't exist.
+     */
+    ;
+
+    _proto.dropNode = function dropNode(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.dropNode: could not find the \"".concat(node, "\" node in the graph."));
+      var edgeData; // Removing attached edges
+      // NOTE: we could be faster here, but this is such a pain to maintain
+
+      if (this.type !== 'undirected') {
+        for (var neighbor in nodeData.out) {
+          edgeData = nodeData.out[neighbor];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+
+        for (var _neighbor in nodeData["in"]) {
+          edgeData = nodeData["in"][_neighbor];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (this.type !== 'directed') {
+        for (var _neighbor2 in nodeData.undirected) {
+          edgeData = nodeData.undirected[_neighbor2];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      } // Dropping the node from the register
+
+
+      this._nodes["delete"](node); // Emitting
+
+
+      this.emit('nodeDropped', {
+        key: node,
+        attributes: nodeData.attributes
+      });
+    }
+    /**
+     * Method used to drop a single edge from the graph.
+     *
+     * Arity 1:
+     * @param  {any}    edge - The edge.
+     *
+     * Arity 2:
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropEdge = function dropEdge(edge) {
+      var edgeData;
+
+      if (arguments.length > 1) {
+        var source = '' + arguments[0];
+        var target = '' + arguments[1];
+        edgeData = getMatchingEdge(this, source, target, this.type);
+        if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      } else {
+        edge = '' + edge;
+        edgeData = this._edges.get(edge);
+        if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(edge, "\" edge in the graph."));
+      }
+
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to drop a single directed edge from the graph.
+     *
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropDirectedEdge = function dropDirectedEdge(source, target) {
+      if (arguments.length < 2) throw new UsageGraphError('Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+      if (this.multi) throw new UsageGraphError('Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+      source = '' + source;
+      target = '' + target;
+      var edgeData = getMatchingEdge(this, source, target, 'directed');
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropDirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to drop a single undirected edge from the graph.
+     *
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropUndirectedEdge = function dropUndirectedEdge(source, target) {
+      if (arguments.length < 2) throw new UsageGraphError('Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+      if (this.multi) throw new UsageGraphError('Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+      var edgeData = getMatchingEdge(this, source, target, 'undirected');
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropUndirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to remove every edge & every node from the graph.
+     *
+     * @return {Graph}
+     */
+    ;
+
+    _proto.clear = function clear() {
+      // Clearing edges
+      this._edges.clear(); // Clearing nodes
+
+
+      this._nodes.clear(); // Reset counters
+
+
+      this._resetInstanceCounters(); // Emitting
+
+
+      this.emit('cleared');
+    }
+    /**
+     * Method used to remove every edge from the graph.
+     *
+     * @return {Graph}
+     */
+    ;
+
+    _proto.clearEdges = function clearEdges() {
+      // Clearing structure index
+      var iterator = this._nodes.values();
+
+      var step;
+
+      while (step = iterator.next(), step.done !== true) {
+        step.value.clear();
+      } // Clearing edges
+
+
+      this._edges.clear(); // Reset counters
+
+
+      this._resetInstanceCounters(); // Emitting
+
+
+      this.emit('edgesCleared');
+    }
+    /**---------------------------------------------------------------------------
+     * Attributes-related methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning the desired graph's attribute.
+     *
+     * @param  {string} name - Name of the attribute.
+     * @return {any}
+     */
+    ;
+
+    _proto.getAttribute = function getAttribute(name) {
+      return this._attributes[name];
+    }
+    /**
+     * Method returning the graph's attributes.
+     *
+     * @return {object}
+     */
+    ;
+
+    _proto.getAttributes = function getAttributes() {
+      return this._attributes;
+    }
+    /**
+     * Method returning whether the graph has the desired attribute.
+     *
+     * @param  {string}  name - Name of the attribute.
+     * @return {boolean}
+     */
+    ;
+
+    _proto.hasAttribute = function hasAttribute(name) {
+      return this._attributes.hasOwnProperty(name);
+    }
+    /**
+     * Method setting a value for the desired graph's attribute.
+     *
+     * @param  {string}  name  - Name of the attribute.
+     * @param  {any}     value - Value for the attribute.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.setAttribute = function setAttribute(name, value) {
+      this._attributes[name] = value; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'set',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method using a function to update the desired graph's attribute's value.
+     *
+     * @param  {string}   name    - Name of the attribute.
+     * @param  {function} updater - Function use to update the attribute's value.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.updateAttribute = function updateAttribute(name, updater) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttribute: updater should be a function.');
+      var value = this._attributes[name];
+      this._attributes[name] = updater(value); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'set',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method removing the desired graph's attribute.
+     *
+     * @param  {string} name  - Name of the attribute.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.removeAttribute = function removeAttribute(name) {
+      delete this._attributes[name]; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'remove',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method replacing the graph's attributes.
+     *
+     * @param  {object} attributes - New attributes.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given attributes are not a plain object.
+     */
+    ;
+
+    _proto.replaceAttributes = function replaceAttributes(attributes) {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.replaceAttributes: provided attributes are not a plain object.');
+      this._attributes = attributes; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'replace',
+        attributes: this._attributes
+      });
+      return this;
+    }
+    /**
+     * Method merging the graph's attributes.
+     *
+     * @param  {object} attributes - Attributes to merge.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given attributes are not a plain object.
+     */
+    ;
+
+    _proto.mergeAttributes = function mergeAttributes(attributes) {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.mergeAttributes: provided attributes are not a plain object.');
+      assign(this._attributes, attributes); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'merge',
+        attributes: this._attributes,
+        data: attributes
+      });
+      return this;
+    }
+    /**
+     * Method updating the graph's attributes.
+     *
+     * @param  {function} updater - Function used to update the attributes.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given updater is not a function.
+     */
+    ;
+
+    _proto.updateAttributes = function updateAttributes(updater) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttributes: provided updater is not a function.');
+      this._attributes = updater(this._attributes); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'update',
+        attributes: this._attributes
+      });
+      return this;
+    }
+    /**
+     * Method used to update each node's attributes using the given function.
+     *
+     * @param {function}  updater - Updater function to use.
+     * @param {object}    [hints] - Optional hints.
+     */
+    ;
+
+    _proto.updateEachNodeAttributes = function updateEachNodeAttributes(updater, hints) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: expecting an updater function.');
+      if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+      }
+
+      this.emit('eachNodeAttributesUpdated', {
+        hints: hints ? hints : null
+      });
+    }
+    /**
+     * Method used to update each edge's attributes using the given function.
+     *
+     * @param {function}  updater - Updater function to use.
+     * @param {object}    [hints] - Optional hints.
+     */
+    ;
+
+    _proto.updateEachEdgeAttributes = function updateEachEdgeAttributes(updater, hints) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: expecting an updater function.');
+      if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+      var iterator = this._edges.values();
+
+      var step, edgeData, sourceData, targetData;
+
+      while (step = iterator.next(), step.done !== true) {
+        edgeData = step.value;
+        sourceData = edgeData.source;
+        targetData = edgeData.target;
+        edgeData.attributes = updater(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected);
+      }
+
+      this.emit('eachEdgeAttributesUpdated', {
+        hints: hints ? hints : null
+      });
+    }
+    /**---------------------------------------------------------------------------
+     * Iteration-related methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method iterating over the graph's adjacency using the given callback.
+     *
+     * @param  {function}  callback - Callback to use.
+     */
+    ;
+
+    _proto.forEachAdjacencyEntry = function forEachAdjacencyEntry(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntry: expecting a callback.');
+      forEachAdjacency(false, false, false, this, callback);
+    };
+
+    _proto.forEachAdjacencyEntryWithOrphans = function forEachAdjacencyEntryWithOrphans(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.');
+      forEachAdjacency(false, false, true, this, callback);
+    }
+    /**
+     * Method iterating over the graph's assymetric adjacency using the given callback.
+     *
+     * @param  {function}  callback - Callback to use.
+     */
+    ;
+
+    _proto.forEachAssymetricAdjacencyEntry = function forEachAssymetricAdjacencyEntry(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntry: expecting a callback.');
+      forEachAdjacency(false, true, false, this, callback);
+    };
+
+    _proto.forEachAssymetricAdjacencyEntryWithOrphans = function forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.');
+      forEachAdjacency(false, true, true, this, callback);
+    }
+    /**
+     * Method returning the list of the graph's nodes.
+     *
+     * @return {array} - The nodes.
+     */
+    ;
+
+    _proto.nodes = function nodes() {
+      if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+      return take(this._nodes.keys(), this._nodes.size);
+    }
+    /**
+     * Method iterating over the graph's nodes using the given callback.
+     *
+     * @param  {function}  callback - Callback (key, attributes, index).
+     */
+    ;
+
+    _proto.forEachNode = function forEachNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        callback(nodeData.key, nodeData.attributes);
+      }
+    }
+    /**
+     * Method iterating attempting to find a node matching the given predicate
+     * function.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.findNode = function findNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.findNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+      }
+
+      return;
+    }
+    /**
+     * Method mapping nodes.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.mapNodes = function mapNodes(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.mapNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+      var result = new Array(this.order);
+      var i = 0;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        result[i++] = callback(nodeData.key, nodeData.attributes);
+      }
+
+      return result;
+    }
+    /**
+     * Method returning whether some node verify the given predicate.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.someNode = function someNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.someNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether all node verify the given predicate.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.everyNode = function everyNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.everyNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (!callback(nodeData.key, nodeData.attributes)) return false;
+      }
+
+      return true;
+    }
+    /**
+     * Method filtering nodes.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.filterNodes = function filterNodes(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.filterNodes: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+      var result = [];
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) result.push(nodeData.key);
+      }
+
+      return result;
+    }
+    /**
+     * Method reducing nodes.
+     *
+     * @param  {function}  callback - Callback (accumulator, key, attributes).
+     */
+    ;
+
+    _proto.reduceNodes = function reduceNodes(callback, initialValue) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.reduceNodes: expecting a callback.');
+      if (arguments.length < 2) throw new InvalidArgumentsGraphError('Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.');
+      var accumulator = initialValue;
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+      }
+
+      return accumulator;
+    }
+    /**
+     * Method returning an iterator over the graph's node entries.
+     *
+     * @return {Iterator}
+     */
+    ;
+
+    _proto.nodeEntries = function nodeEntries() {
+      var iterator$1 = this._nodes.values();
+
+      return new iterator(function () {
+        var step = iterator$1.next();
+        if (step.done) return step;
+        var data = step.value;
+        return {
+          value: {
+            node: data.key,
+            attributes: data.attributes
+          },
+          done: false
+        };
+      });
+    }
+    /**---------------------------------------------------------------------------
+     * Serialization
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used to export the whole graph.
+     *
+     * @return {object} - The serialized graph.
+     */
+    ;
+
+    _proto["export"] = function _export() {
+      var nodes = new Array(this._nodes.size);
+      var i = 0;
+
+      this._nodes.forEach(function (data, key) {
+        nodes[i++] = serializeNode(key, data);
+      });
+
+      var edges = new Array(this._edges.size);
+      i = 0;
+
+      this._edges.forEach(function (data, key) {
+        edges[i++] = serializeEdge(key, data);
+      });
+
+      return {
+        options: {
+          type: this.type,
+          multi: this.multi,
+          allowSelfLoops: this.allowSelfLoops
+        },
+        attributes: this.getAttributes(),
+        nodes: nodes,
+        edges: edges
+      };
+    }
+    /**
+     * Method used to import a serialized graph.
+     *
+     * @param  {object|Graph} data  - The serialized graph.
+     * @param  {boolean}      merge - Whether to merge data.
+     * @return {Graph}              - Returns itself for chaining.
+     */
+    ;
+
+    _proto["import"] = function _import(data) {
+      var _this2 = this;
+
+      var merge = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+      // Importing a Graph instance directly
+      if (isGraph(data)) {
+        // Nodes
+        data.forEachNode(function (n, a) {
+          if (merge) _this2.mergeNode(n, a);else _this2.addNode(n, a);
+        }); // Edges
+
+        data.forEachEdge(function (e, a, s, t, _sa, _ta, u) {
+          if (merge) {
+            if (u) _this2.mergeUndirectedEdgeWithKey(e, s, t, a);else _this2.mergeDirectedEdgeWithKey(e, s, t, a);
+          } else {
+            if (u) _this2.addUndirectedEdgeWithKey(e, s, t, a);else _this2.addDirectedEdgeWithKey(e, s, t, a);
+          }
+        });
+        return this;
+      } // Importing a serialized graph
+
+
+      if (!isPlainObject(data)) throw new InvalidArgumentsGraphError('Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.');
+
+      if (data.attributes) {
+        if (!isPlainObject(data.attributes)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Expecting a plain object.');
+        if (merge) this.mergeAttributes(data.attributes);else this.replaceAttributes(data.attributes);
+      }
+
+      var i, l, list, node, edge;
+
+      if (data.nodes) {
+        list = data.nodes;
+        if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid nodes. Expecting an array.');
+
+        for (i = 0, l = list.length; i < l; i++) {
+          node = list[i]; // Validating
+
+          validateSerializedNode(node); // Adding the node
+
+          var _node = node,
+              key = _node.key,
+              attributes = _node.attributes;
+          if (merge) this.mergeNode(key, attributes);else this.addNode(key, attributes);
+        }
+      }
+
+      if (data.edges) {
+        list = data.edges;
+        if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid edges. Expecting an array.');
+
+        for (i = 0, l = list.length; i < l; i++) {
+          edge = list[i]; // Validating
+
+          validateSerializedEdge(edge); // Adding the edge
+
+          var _edge = edge,
+              source = _edge.source,
+              target = _edge.target,
+              _attributes = _edge.attributes,
+              _edge$undirected = _edge.undirected,
+              undirected = _edge$undirected === void 0 ? false : _edge$undirected;
+          var method = void 0;
+
+          if ('key' in edge) {
+            method = merge ? undirected ? this.mergeUndirectedEdgeWithKey : this.mergeDirectedEdgeWithKey : undirected ? this.addUndirectedEdgeWithKey : this.addDirectedEdgeWithKey;
+            method.call(this, edge.key, source, target, _attributes);
+          } else {
+            method = merge ? undirected ? this.mergeUndirectedEdge : this.mergeDirectedEdge : undirected ? this.addUndirectedEdge : this.addDirectedEdge;
+            method.call(this, source, target, _attributes);
+          }
+        }
+      }
+
+      return this;
+    }
+    /**---------------------------------------------------------------------------
+     * Utils
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning a null copy of the graph, i.e. a graph without nodes
+     * & edges but with the exact same options.
+     *
+     * @param  {object} options - Options to merge with the current ones.
+     * @return {Graph}          - The null copy.
+     */
+    ;
+
+    _proto.nullCopy = function nullCopy(options) {
+      var graph = new Graph(assign({}, this._options, options));
+      graph.replaceAttributes(assign({}, this.getAttributes()));
+      return graph;
+    }
+    /**
+     * Method returning an empty copy of the graph, i.e. a graph without edges but
+     * with the exact same options.
+     *
+     * @param  {object} options - Options to merge with the current ones.
+     * @return {Graph}          - The empty copy.
+     */
+    ;
+
+    _proto.emptyCopy = function emptyCopy(options) {
+      var graph = this.nullCopy(options);
+
+      this._nodes.forEach(function (nodeData, key) {
+        var attributes = assign({}, nodeData.attributes); // NOTE: no need to emit events since user cannot access the instance yet
+
+        nodeData = new graph.NodeDataClass(key, attributes);
+
+        graph._nodes.set(key, nodeData);
+      });
+
+      return graph;
+    }
+    /**
+     * Method returning an exact copy of the graph.
+     *
+     * @param  {object} options - Upgrade options.
+     * @return {Graph}          - The copy.
+     */
+    ;
+
+    _proto.copy = function copy(options) {
+      options = options || {};
+      if (typeof options.type === 'string' && options.type !== this.type && options.type !== 'mixed') throw new UsageGraphError("Graph.copy: cannot create an incompatible copy from \"".concat(this.type, "\" type to \"").concat(options.type, "\" because this would mean losing information about the current graph."));
+      if (typeof options.multi === 'boolean' && options.multi !== this.multi && options.multi !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.');
+      if (typeof options.allowSelfLoops === 'boolean' && options.allowSelfLoops !== this.allowSelfLoops && options.allowSelfLoops !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.');
+      var graph = this.emptyCopy(options);
+
+      var iterator = this._edges.values();
+
+      var step, edgeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        edgeData = step.value; // NOTE: no need to emit events since user cannot access the instance yet
+
+        addEdge(graph, 'copy', false, edgeData.undirected, edgeData.key, edgeData.source.key, edgeData.target.key, assign({}, edgeData.attributes));
+      }
+
+      return graph;
+    }
+    /**---------------------------------------------------------------------------
+     * Known methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used by JavaScript to perform JSON serialization.
+     *
+     * @return {object} - The serialized graph.
+     */
+    ;
+
+    _proto.toJSON = function toJSON() {
+      return this["export"]();
+    }
+    /**
+     * Method returning [object Graph].
+     */
+    ;
+
+    _proto.toString = function toString() {
+      return '[object Graph]';
+    }
+    /**
+     * Method used internally by node's console to display a custom object.
+     *
+     * @return {object} - Formatted object representation of the graph.
+     */
+    ;
+
+    _proto.inspect = function inspect() {
+      var _this3 = this;
+
+      var nodes = {};
+
+      this._nodes.forEach(function (data, key) {
+        nodes[key] = data.attributes;
+      });
+
+      var edges = {},
+          multiIndex = {};
+
+      this._edges.forEach(function (data, key) {
+        var direction = data.undirected ? '--' : '->';
+        var label = '';
+        var source = data.source.key;
+        var target = data.target.key;
+        var tmp;
+
+        if (data.undirected && source > target) {
+          tmp = source;
+          source = target;
+          target = tmp;
+        }
+
+        var desc = "(".concat(source, ")").concat(direction, "(").concat(target, ")");
+
+        if (!key.startsWith('geid_')) {
+          label += "[".concat(key, "]: ");
+        } else if (_this3.multi) {
+          if (typeof multiIndex[desc] === 'undefined') {
+            multiIndex[desc] = 0;
+          } else {
+            multiIndex[desc]++;
+          }
+
+          label += "".concat(multiIndex[desc], ". ");
+        }
+
+        label += desc;
+        edges[label] = data.attributes;
+      });
+
+      var dummy = {};
+
+      for (var k in this) {
+        if (this.hasOwnProperty(k) && !EMITTER_PROPS.has(k) && typeof this[k] !== 'function' && _typeof(k) !== 'symbol') dummy[k] = this[k];
+      }
+
+      dummy.attributes = this._attributes;
+      dummy.nodes = nodes;
+      dummy.edges = edges;
+      privateProperty(dummy, 'constructor', this.constructor);
+      return dummy;
+    };
+
+    return Graph;
+  }(events.exports.EventEmitter);
+  if (typeof Symbol !== 'undefined') Graph.prototype[Symbol["for"]('nodejs.util.inspect.custom')] = Graph.prototype.inspect;
+  /**
+   * Related to edge addition.
+   */
+
+  EDGE_ADD_METHODS.forEach(function (method) {
+    ['add', 'merge', 'update'].forEach(function (verb) {
+      var name = method.name(verb);
+      var fn = verb === 'add' ? addEdge : mergeEdge;
+
+      if (method.generateKey) {
+        Graph.prototype[name] = function (source, target, attributes) {
+          return fn(this, name, true, (method.type || this.type) === 'undirected', null, source, target, attributes, verb === 'update');
+        };
+      } else {
+        Graph.prototype[name] = function (edge, source, target, attributes) {
+          return fn(this, name, false, (method.type || this.type) === 'undirected', edge, source, target, attributes, verb === 'update');
+        };
+      }
+    });
+  });
+  /**
+   * Attributes-related.
+   */
+
+  attachNodeAttributesMethods(Graph);
+  attachEdgeAttributesMethods(Graph);
+  /**
+   * Edge iteration-related.
+   */
+
+  attachEdgeIterationMethods(Graph);
+  /**
+   * Neighbor iteration-related.
+   */
+
+  attachNeighborIterationMethods(Graph);
+
+  /**
+   * Alternative constructors.
+   */
+
+  var DirectedGraph = /*#__PURE__*/function (_Graph) {
+    _inheritsLoose(DirectedGraph, _Graph);
+
+    function DirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'directed'
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+      if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph.call(this, finalOptions) || this;
+    }
+
+    return DirectedGraph;
+  }(Graph);
+
+  var UndirectedGraph = /*#__PURE__*/function (_Graph2) {
+    _inheritsLoose(UndirectedGraph, _Graph2);
+
+    function UndirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'undirected'
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+      if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph2.call(this, finalOptions) || this;
+    }
+
+    return UndirectedGraph;
+  }(Graph);
+
+  var MultiGraph = /*#__PURE__*/function (_Graph3) {
+    _inheritsLoose(MultiGraph, _Graph3);
+
+    function MultiGraph(options) {
+      var finalOptions = assign({
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiGraph.from: inconsistent indication that the graph should be simple in given options!');
+      return _Graph3.call(this, finalOptions) || this;
+    }
+
+    return MultiGraph;
+  }(Graph);
+
+  var MultiDirectedGraph = /*#__PURE__*/function (_Graph4) {
+    _inheritsLoose(MultiDirectedGraph, _Graph4);
+
+    function MultiDirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'directed',
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+      if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph4.call(this, finalOptions) || this;
+    }
+
+    return MultiDirectedGraph;
+  }(Graph);
+
+  var MultiUndirectedGraph = /*#__PURE__*/function (_Graph5) {
+    _inheritsLoose(MultiUndirectedGraph, _Graph5);
+
+    function MultiUndirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'undirected',
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+      if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph5.call(this, finalOptions) || this;
+    }
+
+    return MultiUndirectedGraph;
+  }(Graph);
+  /**
+   * Attaching static #.from method to each of the constructors.
+   */
+
+
+  function attachStaticFromMethod(Class) {
+    /**
+     * Builds a graph from serialized data or another graph's data.
+     *
+     * @param  {Graph|SerializedGraph} data      - Hydratation data.
+     * @param  {object}                [options] - Options.
+     * @return {Class}
+     */
+    Class.from = function (data, options) {
+      // Merging given options with serialized ones
+      var finalOptions = assign({}, data.options, options);
+      var instance = new Class(finalOptions);
+      instance["import"](data);
+      return instance;
+    };
+  }
+
+  attachStaticFromMethod(Graph);
+  attachStaticFromMethod(DirectedGraph);
+  attachStaticFromMethod(UndirectedGraph);
+  attachStaticFromMethod(MultiGraph);
+  attachStaticFromMethod(MultiDirectedGraph);
+  attachStaticFromMethod(MultiUndirectedGraph);
+  Graph.Graph = Graph;
+  Graph.DirectedGraph = DirectedGraph;
+  Graph.UndirectedGraph = UndirectedGraph;
+  Graph.MultiGraph = MultiGraph;
+  Graph.MultiDirectedGraph = MultiDirectedGraph;
+  Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+  Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+  Graph.NotFoundGraphError = NotFoundGraphError;
+  Graph.UsageGraphError = UsageGraphError;
+
+  /**
+   * Graphology CommonJS Endoint
+   * ============================
+   *
+   * Endpoint for CommonJS modules consumers.
+   */
+
+  return Graph;
+
+}));
+//# sourceMappingURL=graphology.umd.js.map
diff --git a/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.min.js b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..9cb5b8ff230cba8bec64c51bb01df2574dae2cb7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/dist/graphology.umd.min.js
@@ -0,0 +1,2 @@
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).graphology=e()}(this,(function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,r(t,e)}function n(t){return n=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)},n(t)}function r(t,e){return r=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},r(t,e)}function i(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}function o(t,e,n){return o=i()?Reflect.construct:function(t,e,n){var i=[null];i.push.apply(i,e);var o=new(Function.bind.apply(t,i));return n&&r(o,n.prototype),o},o.apply(null,arguments)}function a(t){var e="function"==typeof Map?new Map:void 0;return a=function(t){if(null===t||(i=t,-1===Function.toString.call(i).indexOf("[native code]")))return t;var i;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,a)}function a(){return o(t,arguments,n(this).constructor)}return a.prototype=Object.create(t.prototype,{constructor:{value:a,enumerable:!1,writable:!0,configurable:!0}}),r(a,t)},a(t)}function u(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var c=function(){for(var t=arguments[0],e=1,n=arguments.length;e<n;e++)if(arguments[e])for(var r in arguments[e])t[r]=arguments[e][r];return t};function s(t,e,n,r){var i=t._nodes.get(e),o=null;return i?o="mixed"===r?i.out&&i.out[n]||i.undirected&&i.undirected[n]:"directed"===r?i.out&&i.out[n]:i.undirected&&i.undirected[n]:o}function d(e){return null!==e&&"object"===t(e)&&"function"==typeof e.addUndirectedEdgeWithKey&&"function"==typeof e.dropNode}function h(e){return"object"===t(e)&&null!==e&&e.constructor===Object}function p(t){var e;for(e in t)return!1;return!0}function f(t,e,n){Object.defineProperty(t,e,{enumerable:!1,configurable:!1,writable:!0,value:n})}function l(t,e,n){var r={enumerable:!0,configurable:!0};"function"==typeof n?r.get=n:(r.value=n,r.writable=!1),Object.defineProperty(t,e,r)}function g(t){return!!h(t)&&!(t.attributes&&!Array.isArray(t.attributes))}"function"==typeof Object.assign&&(c=Object.assign);var y,w={exports:{}},v="object"==typeof Reflect?Reflect:null,b=v&&"function"==typeof v.apply?v.apply:function(t,e,n){return Function.prototype.apply.call(t,e,n)};y=v&&"function"==typeof v.ownKeys?v.ownKeys:Object.getOwnPropertySymbols?function(t){return Object.getOwnPropertyNames(t).concat(Object.getOwnPropertySymbols(t))}:function(t){return Object.getOwnPropertyNames(t)};var m=Number.isNaN||function(t){return t!=t};function k(){k.init.call(this)}w.exports=k,w.exports.once=function(t,e){return new Promise((function(n,r){function i(n){t.removeListener(e,o),r(n)}function o(){"function"==typeof t.removeListener&&t.removeListener("error",i),n([].slice.call(arguments))}N(t,e,o,{once:!0}),"error"!==e&&function(t,e,n){"function"==typeof t.on&&N(t,"error",e,n)}(t,i,{once:!0})}))},k.EventEmitter=k,k.prototype._events=void 0,k.prototype._eventsCount=0,k.prototype._maxListeners=void 0;var _=10;function G(t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t)}function x(t){return void 0===t._maxListeners?k.defaultMaxListeners:t._maxListeners}function E(t,e,n,r){var i,o,a,u;if(G(n),void 0===(o=t._events)?(o=t._events=Object.create(null),t._eventsCount=0):(void 0!==o.newListener&&(t.emit("newListener",e,n.listener?n.listener:n),o=t._events),a=o[e]),void 0===a)a=o[e]=n,++t._eventsCount;else if("function"==typeof a?a=o[e]=r?[n,a]:[a,n]:r?a.unshift(n):a.push(n),(i=x(t))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(e)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=t,c.type=e,c.count=a.length,u=c,console&&console.warn&&console.warn(u)}return t}function A(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function S(t,e,n){var r={fired:!1,wrapFn:void 0,target:t,type:e,listener:n},i=A.bind(r);return i.listener=n,r.wrapFn=i,i}function D(t,e,n){var r=t._events;if(void 0===r)return[];var i=r[e];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(t){for(var e=new Array(t.length),n=0;n<e.length;++n)e[n]=t[n].listener||t[n];return e}(i):U(i,i.length)}function L(t){var e=this._events;if(void 0!==e){var n=e[t];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function U(t,e){for(var n=new Array(e),r=0;r<e;++r)n[r]=t[r];return n}function N(t,e,n,r){if("function"==typeof t.on)r.once?t.once(e,n):t.on(e,n);else{if("function"!=typeof t.addEventListener)throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type '+typeof t);t.addEventListener(e,(function i(o){r.once&&t.removeEventListener(e,i),n(o)}))}}function j(t){if("function"!=typeof t)throw new Error("obliterator/iterator: expecting a function!");this.next=t}Object.defineProperty(k,"defaultMaxListeners",{enumerable:!0,get:function(){return _},set:function(t){if("number"!=typeof t||t<0||m(t))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+t+".");_=t}}),k.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},k.prototype.setMaxListeners=function(t){if("number"!=typeof t||t<0||m(t))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+t+".");return this._maxListeners=t,this},k.prototype.getMaxListeners=function(){return x(this)},k.prototype.emit=function(t){for(var e=[],n=1;n<arguments.length;n++)e.push(arguments[n]);var r="error"===t,i=this._events;if(void 0!==i)r=r&&void 0===i.error;else if(!r)return!1;if(r){var o;if(e.length>0&&(o=e[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var u=i[t];if(void 0===u)return!1;if("function"==typeof u)b(u,this,e);else{var c=u.length,s=U(u,c);for(n=0;n<c;++n)b(s[n],this,e)}return!0},k.prototype.addListener=function(t,e){return E(this,t,e,!1)},k.prototype.on=k.prototype.addListener,k.prototype.prependListener=function(t,e){return E(this,t,e,!0)},k.prototype.once=function(t,e){return G(e),this.on(t,S(this,t,e)),this},k.prototype.prependOnceListener=function(t,e){return G(e),this.prependListener(t,S(this,t,e)),this},k.prototype.removeListener=function(t,e){var n,r,i,o,a;if(G(e),void 0===(r=this._events))return this;if(void 0===(n=r[t]))return this;if(n===e||n.listener===e)0==--this._eventsCount?this._events=Object.create(null):(delete r[t],r.removeListener&&this.emit("removeListener",t,n.listener||e));else if("function"!=typeof n){for(i=-1,o=n.length-1;o>=0;o--)if(n[o]===e||n[o].listener===e){a=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():function(t,e){for(;e+1<t.length;e++)t[e]=t[e+1];t.pop()}(n,i),1===n.length&&(r[t]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",t,a||e)}return this},k.prototype.off=k.prototype.removeListener,k.prototype.removeAllListeners=function(t){var e,n,r;if(void 0===(n=this._events))return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[t]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[t]),this;if(0===arguments.length){var i,o=Object.keys(n);for(r=0;r<o.length;++r)"removeListener"!==(i=o[r])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(e=n[t]))this.removeListener(t,e);else if(void 0!==e)for(r=e.length-1;r>=0;r--)this.removeListener(t,e[r]);return this},k.prototype.listeners=function(t){return D(this,t,!0)},k.prototype.rawListeners=function(t){return D(this,t,!1)},k.listenerCount=function(t,e){return"function"==typeof t.listenerCount?t.listenerCount(e):L.call(t,e)},k.prototype.listenerCount=L,k.prototype.eventNames=function(){return this._eventsCount>0?y(this._events):[]},"undefined"!=typeof Symbol&&(j.prototype[Symbol.iterator]=function(){return this}),j.of=function(){var t=arguments,e=t.length,n=0;return new j((function(){return n>=e?{done:!0}:{done:!1,value:t[n++]}}))},j.empty=function(){return new j((function(){return{done:!0}}))},j.fromSequence=function(t){var e=0,n=t.length;return new j((function(){return e>=n?{done:!0}:{done:!1,value:t[e++]}}))},j.is=function(t){return t instanceof j||"object"==typeof t&&null!==t&&"function"==typeof t.next};var O=j,C={};C.ARRAY_BUFFER_SUPPORT="undefined"!=typeof ArrayBuffer,C.SYMBOL_SUPPORT="undefined"!=typeof Symbol;var z=O,M=C,W=M.ARRAY_BUFFER_SUPPORT,P=M.SYMBOL_SUPPORT;var R=function(t){var e=function(t){return"string"==typeof t||Array.isArray(t)||W&&ArrayBuffer.isView(t)?z.fromSequence(t):"object"!=typeof t||null===t?null:P&&"function"==typeof t[Symbol.iterator]?t[Symbol.iterator]():"function"==typeof t.next?t:null}(t);if(!e)throw new Error("obliterator: target is not iterable nor a valid iterator.");return e},K=R,T=function(t,e){for(var n,r=arguments.length>1?e:1/0,i=r!==1/0?new Array(r):[],o=0,a=K(t);;){if(o===r)return i;if((n=a.next()).done)return o!==e&&(i.length=o),i;i[o++]=n.value}},B=function(t){function n(e){var n;return(n=t.call(this)||this).name="GraphError",n.message=e,n}return e(n,t),n}(a(Error)),F=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="InvalidArgumentsGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B),I=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="NotFoundGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B),Y=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="UsageGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B);function q(t,e){this.key=t,this.attributes=e,this.clear()}function J(t,e){this.key=t,this.attributes=e,this.clear()}function V(t,e){this.key=t,this.attributes=e,this.clear()}function H(t,e,n,r,i){this.key=e,this.attributes=i,this.undirected=t,this.source=n,this.target=r}q.prototype.clear=function(){this.inDegree=0,this.outDegree=0,this.undirectedDegree=0,this.in={},this.out={},this.undirected={}},J.prototype.clear=function(){this.inDegree=0,this.outDegree=0,this.in={},this.out={}},V.prototype.clear=function(){this.undirectedDegree=0,this.undirected={}},H.prototype.attach=function(){var t="out",e="in";this.undirected&&(t=e="undirected");var n=this.source.key,r=this.target.key;this.source[t][r]=this,this.undirected&&n===r||(this.target[e][n]=this)},H.prototype.attachMulti=function(){var t="out",e="in",n=this.source.key,r=this.target.key;this.undirected&&(t=e="undirected");var i=this.source[t],o=i[r];if(void 0===o)return i[r]=this,void(this.undirected&&n===r||(this.target[e][n]=this));o.previous=this,this.next=o,i[r]=this,this.target[e][n]=this},H.prototype.detach=function(){var t=this.source.key,e=this.target.key,n="out",r="in";this.undirected&&(n=r="undirected"),delete this.source[n][e],delete this.target[r][t]},H.prototype.detachMulti=function(){var t=this.source.key,e=this.target.key,n="out",r="in";this.undirected&&(n=r="undirected"),void 0===this.previous?void 0===this.next?(delete this.source[n][e],delete this.target[r][t]):(this.next.previous=void 0,this.source[n][e]=this.next,this.target[r][t]=this.next):(this.previous.next=this.next,void 0!==this.next&&(this.next.previous=this.previous))};function Q(t,e,n,r,i,o,a){var u,c,s,d;if(r=""+r,0===n){if(!(u=t._nodes.get(r)))throw new I("Graph.".concat(e,': could not find the "').concat(r,'" node in the graph.'));s=i,d=o}else if(3===n){if(i=""+i,!(c=t._edges.get(i)))throw new I("Graph.".concat(e,': could not find the "').concat(i,'" edge in the graph.'));var h=c.source.key,p=c.target.key;if(r===h)u=c.target;else{if(r!==p)throw new I("Graph.".concat(e,': the "').concat(r,'" node is not attached to the "').concat(i,'" edge (').concat(h,", ").concat(p,")."));u=c.source}s=o,d=a}else{if(!(c=t._edges.get(r)))throw new I("Graph.".concat(e,': could not find the "').concat(r,'" edge in the graph.'));u=1===n?c.source:c.target,s=i,d=o}return[u,s,d]}var X=[{name:function(t){return"get".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return a.attributes[u]}}},{name:function(t){return"get".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){return Q(this,e,n,t,r)[0].attributes}}},{name:function(t){return"has".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return a.attributes.hasOwnProperty(u)}}},{name:function(t){return"set".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i,o){var a=Q(this,e,n,t,r,i,o),u=a[0],c=a[1],s=a[2];return u.attributes[c]=s,this.emit("nodeAttributesUpdated",{key:u.key,type:"set",attributes:u.attributes,name:c}),this}}},{name:function(t){return"update".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i,o){var a=Q(this,e,n,t,r,i,o),u=a[0],c=a[1],s=a[2];if("function"!=typeof s)throw new F("Graph.".concat(e,": updater should be a function."));var d=u.attributes,h=s(d[c]);return d[c]=h,this.emit("nodeAttributesUpdated",{key:u.key,type:"set",attributes:u.attributes,name:c}),this}}},{name:function(t){return"remove".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return delete a.attributes[u],this.emit("nodeAttributesUpdated",{key:a.key,type:"remove",attributes:a.attributes,name:u}),this}}},{name:function(t){return"replace".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if(!h(u))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return a.attributes=u,this.emit("nodeAttributesUpdated",{key:a.key,type:"replace",attributes:a.attributes}),this}}},{name:function(t){return"merge".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if(!h(u))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return c(a.attributes,u),this.emit("nodeAttributesUpdated",{key:a.key,type:"merge",attributes:a.attributes,data:u}),this}}},{name:function(t){return"update".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if("function"!=typeof u)throw new F("Graph.".concat(e,": provided updater is not a function."));return a.attributes=u(a.attributes),this.emit("nodeAttributesUpdated",{key:a.key,type:"update",attributes:a.attributes}),this}}}];var Z=[{name:function(t){return"get".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return i.attributes[r]}}},{name:function(t){return"get".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t){var r;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>1){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var i=""+t,o=""+arguments[1];if(!(r=s(this,i,o,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(i,'" - "').concat(o,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(r=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return r.attributes}}},{name:function(t){return"has".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return i.attributes.hasOwnProperty(r)}}},{name:function(t){return"set".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>3){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var a=""+t,u=""+r;if(r=arguments[2],i=arguments[3],!(o=s(this,a,u,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(a,'" - "').concat(u,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(o=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return o.attributes[r]=i,this.emit("edgeAttributesUpdated",{key:o.key,type:"set",attributes:o.attributes,name:r}),this}}},{name:function(t){return"update".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>3){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var a=""+t,u=""+r;if(r=arguments[2],i=arguments[3],!(o=s(this,a,u,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(a,'" - "').concat(u,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(o=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if("function"!=typeof i)throw new F("Graph.".concat(e,": updater should be a function."));return o.attributes[r]=i(o.attributes[r]),this.emit("edgeAttributesUpdated",{key:o.key,type:"set",attributes:o.attributes,name:r}),this}}},{name:function(t){return"remove".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return delete i.attributes[r],this.emit("edgeAttributesUpdated",{key:i.key,type:"remove",attributes:i.attributes,name:r}),this}}},{name:function(t){return"replace".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if(!h(r))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return i.attributes=r,this.emit("edgeAttributesUpdated",{key:i.key,type:"replace",attributes:i.attributes}),this}}},{name:function(t){return"merge".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if(!h(r))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return c(i.attributes,r),this.emit("edgeAttributesUpdated",{key:i.key,type:"merge",attributes:i.attributes,data:r}),this}}},{name:function(t){return"update".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if("function"!=typeof r)throw new F("Graph.".concat(e,": provided updater is not a function."));return i.attributes=r(i.attributes),this.emit("edgeAttributesUpdated",{key:i.key,type:"update",attributes:i.attributes}),this}}}];var $=O,tt=R,et=function(){var t=arguments,e=null,n=-1;return new $((function(){for(var r=null;;){if(null===e){if(++n>=t.length)return{done:!0};e=tt(t[n])}if(!0!==(r=e.next()).done)break;e=null}return r}))},nt=[{name:"edges",type:"mixed"},{name:"inEdges",type:"directed",direction:"in"},{name:"outEdges",type:"directed",direction:"out"},{name:"inboundEdges",type:"mixed",direction:"in"},{name:"outboundEdges",type:"mixed",direction:"out"},{name:"directedEdges",type:"directed"},{name:"undirectedEdges",type:"undirected"}];function rt(t,e,n,r){var i=!1;for(var o in e)if(o!==r){var a=e[o];if(i=n(a.key,a.attributes,a.source.key,a.target.key,a.source.attributes,a.target.attributes,a.undirected),t&&i)return a.key}}function it(t,e,n,r){var i,o,a,u=!1;for(var c in e)if(c!==r){i=e[c];do{if(o=i.source,a=i.target,u=n(i.key,i.attributes,o.key,a.key,o.attributes,a.attributes,i.undirected),t&&u)return i.key;i=i.next}while(void 0!==i)}}function ot(t,e){var n,r=Object.keys(t),i=r.length,o=0;return new O((function(){do{if(n)n=n.next;else{if(o>=i)return{done:!0};var a=r[o++];if(a===e){n=void 0;continue}n=t[a]}}while(!n);return{done:!1,value:{edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected}}}))}function at(t,e,n,r){var i=e[n];if(i){var o=i.source,a=i.target;return r(i.key,i.attributes,o.key,a.key,o.attributes,a.attributes,i.undirected)&&t?i.key:void 0}}function ut(t,e,n,r){var i=e[n];if(i){var o=!1;do{if(o=r(i.key,i.attributes,i.source.key,i.target.key,i.source.attributes,i.target.attributes,i.undirected),t&&o)return i.key;i=i.next}while(void 0!==i)}}function ct(t,e){var n=t[e];return void 0!==n.next?new O((function(){if(!n)return{done:!0};var t={edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected};return n=n.next,{done:!1,value:t}})):O.of({edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected})}function st(t,e){if(0===t.size)return[];if("mixed"===e||e===t.type)return"function"==typeof Array.from?Array.from(t._edges.keys()):T(t._edges.keys(),t._edges.size);for(var n,r,i="undirected"===e?t.undirectedSize:t.directedSize,o=new Array(i),a="undirected"===e,u=t._edges.values(),c=0;!0!==(n=u.next()).done;)(r=n.value).undirected===a&&(o[c++]=r.key);return o}function dt(t,e,n,r){if(0!==e.size)for(var i,o,a="mixed"!==n&&n!==e.type,u="undirected"===n,c=!1,s=e._edges.values();!0!==(i=s.next()).done;)if(o=i.value,!a||o.undirected===u){var d=o,h=d.key,p=d.attributes,f=d.source,l=d.target;if(c=r(h,p,f.key,l.key,f.attributes,l.attributes,o.undirected),t&&c)return h}}function ht(t,e){if(0===t.size)return O.empty();var n="mixed"!==e&&e!==t.type,r="undirected"===e,i=t._edges.values();return new O((function(){for(var t,e;;){if((t=i.next()).done)return t;if(e=t.value,!n||e.undirected===r)break}return{value:{edge:e.key,attributes:e.attributes,source:e.source.key,target:e.target.key,sourceAttributes:e.source.attributes,targetAttributes:e.target.attributes,undirected:e.undirected},done:!1}}))}function pt(t,e,n,r,i,o){var a,u=e?it:rt;if("undirected"!==n){if("out"!==r&&(a=u(t,i.in,o),t&&a))return a;if("in"!==r&&(a=u(t,i.out,o,r?void 0:i.key),t&&a))return a}if("directed"!==n&&(a=u(t,i.undirected,o),t&&a))return a}function ft(t,e,n,r){var i=[];return pt(!1,t,e,n,r,(function(t){i.push(t)})),i}function lt(t,e,n){var r=O.empty();return"undirected"!==t&&("out"!==e&&void 0!==n.in&&(r=et(r,ot(n.in))),"in"!==e&&void 0!==n.out&&(r=et(r,ot(n.out,e?void 0:n.key)))),"directed"!==t&&void 0!==n.undirected&&(r=et(r,ot(n.undirected))),r}function gt(t,e,n,r,i,o,a){var u,c=n?ut:at;if("undirected"!==e){if(void 0!==i.in&&"out"!==r&&(u=c(t,i.in,o,a),t&&u))return u;if(void 0!==i.out&&"in"!==r&&(r||i.key!==o)&&(u=c(t,i.out,o,a),t&&u))return u}if("directed"!==e&&void 0!==i.undirected&&(u=c(t,i.undirected,o,a),t&&u))return u}function yt(t,e,n,r,i){var o=[];return gt(!1,t,e,n,r,i,(function(t){o.push(t)})),o}function wt(t,e,n,r){var i=O.empty();return"undirected"!==t&&(void 0!==n.in&&"out"!==e&&r in n.in&&(i=et(i,ct(n.in,r))),void 0!==n.out&&"in"!==e&&r in n.out&&(e||n.key!==r)&&(i=et(i,ct(n.out,r)))),"directed"!==t&&void 0!==n.undirected&&r in n.undirected&&(i=et(i,ct(n.undirected,r))),i}var vt=[{name:"neighbors",type:"mixed"},{name:"inNeighbors",type:"directed",direction:"in"},{name:"outNeighbors",type:"directed",direction:"out"},{name:"inboundNeighbors",type:"mixed",direction:"in"},{name:"outboundNeighbors",type:"mixed",direction:"out"},{name:"directedNeighbors",type:"directed"},{name:"undirectedNeighbors",type:"undirected"}];function bt(){this.A=null,this.B=null}function mt(t,e,n,r,i){for(var o in r){var a=r[o],u=a.source,c=a.target,s=u===n?c:u;if(!e||!e.has(s.key)){var d=i(s.key,s.attributes);if(t&&d)return s.key}}}function kt(t,e,n,r,i){if("mixed"!==e){if("undirected"===e)return mt(t,null,r,r.undirected,i);if("string"==typeof n)return mt(t,null,r,r[n],i)}var o,a=new bt;if("undirected"!==e){if("out"!==n){if(o=mt(t,null,r,r.in,i),t&&o)return o;a.wrap(r.in)}if("in"!==n){if(o=mt(t,a,r,r.out,i),t&&o)return o;a.wrap(r.out)}}if("directed"!==e&&(o=mt(t,a,r,r.undirected,i),t&&o))return o}function _t(t,e,n){var r=Object.keys(n),i=r.length,o=0;return new O((function(){var a=null;do{if(o>=i)return t&&t.wrap(n),{done:!0};var u=n[r[o++]],c=u.source,s=u.target;a=c===e?s:c,t&&t.has(a.key)&&(a=null)}while(null===a);return{done:!1,value:{neighbor:a.key,attributes:a.attributes}}}))}function Gt(t,e){var n=e.name,r=e.type,i=e.direction;t.prototype[n]=function(t){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return[];t=""+t;var e=this._nodes.get(t);if(void 0===e)throw new I("Graph.".concat(n,': could not find the "').concat(t,'" node in the graph.'));return function(t,e,n){if("mixed"!==t){if("undirected"===t)return Object.keys(n.undirected);if("string"==typeof e)return Object.keys(n[e])}var r=[];return kt(!1,t,e,n,(function(t){r.push(t)})),r}("mixed"===r?this.type:r,i,e)}}function xt(t,e){var n=e.name,r=e.type,i=e.direction,o=n.slice(0,-1)+"Entries";t.prototype[o]=function(t){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return O.empty();t=""+t;var e=this._nodes.get(t);if(void 0===e)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return function(t,e,n){if("mixed"!==t){if("undirected"===t)return _t(null,n,n.undirected);if("string"==typeof e)return _t(null,n,n[e])}var r=O.empty(),i=new bt;return"undirected"!==t&&("out"!==e&&(r=et(r,_t(i,n,n.in))),"in"!==e&&(r=et(r,_t(i,n,n.out)))),"directed"!==t&&(r=et(r,_t(i,n,n.undirected))),r}("mixed"===r?this.type:r,i,e)}}function Et(t,e,n,r,i){for(var o,a,u,c,s,d,h,p=r._nodes.values(),f=r.type;!0!==(o=p.next()).done;){var l=!1;if(a=o.value,"undirected"!==f)for(u in c=a.out){s=c[u];do{if(d=s.target,l=!0,h=i(a.key,d.key,a.attributes,d.attributes,s.key,s.attributes,s.undirected),t&&h)return s;s=s.next}while(s)}if("directed"!==f)for(u in c=a.undirected)if(!(e&&a.key>u)){s=c[u];do{if((d=s.target).key!==u&&(d=s.source),l=!0,h=i(a.key,d.key,a.attributes,d.attributes,s.key,s.attributes,s.undirected),t&&h)return s;s=s.next}while(s)}if(n&&!l&&(h=i(a.key,null,a.attributes,null,null,null,null),t&&h))return null}}function At(t){if(!h(t))throw new F('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');if(!("key"in t))throw new F("Graph.import: serialized node is missing its key.");if("attributes"in t&&(!h(t.attributes)||null===t.attributes))throw new F("Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.")}function St(t){if(!h(t))throw new F('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');if(!("source"in t))throw new F("Graph.import: serialized edge is missing its source.");if(!("target"in t))throw new F("Graph.import: serialized edge is missing its target.");if("attributes"in t&&(!h(t.attributes)||null===t.attributes))throw new F("Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.");if("undirected"in t&&"boolean"!=typeof t.undirected)throw new F("Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.")}bt.prototype.wrap=function(t){null===this.A?this.A=t:null===this.B&&(this.B=t)},bt.prototype.has=function(t){return null!==this.A&&t in this.A||null!==this.B&&t in this.B};var Dt,Lt=(Dt=255&Math.floor(256*Math.random()),function(){return Dt++}),Ut=new Set(["directed","undirected","mixed"]),Nt=new Set(["domain","_events","_eventsCount","_maxListeners"]),jt={allowSelfLoops:!0,multi:!1,type:"mixed"};function Ot(t,e,n){var r=new t.NodeDataClass(e,n);return t._nodes.set(e,r),t.emit("nodeAdded",{key:e,attributes:n}),r}function Ct(t,e,n,r,i,o,a,u){if(!r&&"undirected"===t.type)throw new Y("Graph.".concat(e,": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));if(r&&"directed"===t.type)throw new Y("Graph.".concat(e,": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));if(u&&!h(u))throw new F("Graph.".concat(e,': invalid attributes. Expecting an object but got "').concat(u,'"'));if(o=""+o,a=""+a,u=u||{},!t.allowSelfLoops&&o===a)throw new Y("Graph.".concat(e,': source & target are the same ("').concat(o,"\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));var c=t._nodes.get(o),s=t._nodes.get(a);if(!c)throw new I("Graph.".concat(e,': source node "').concat(o,'" not found.'));if(!s)throw new I("Graph.".concat(e,': target node "').concat(a,'" not found.'));var d={key:null,undirected:r,source:o,target:a,attributes:u};if(n)i=t._edgeKeyGenerator();else if(i=""+i,t._edges.has(i))throw new Y("Graph.".concat(e,': the "').concat(i,'" edge already exists in the graph.'));if(!t.multi&&(r?void 0!==c.undirected[a]:void 0!==c.out[a]))throw new Y("Graph.".concat(e,': an edge linking "').concat(o,'" to "').concat(a,"\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));var p=new H(r,i,c,s,u);t._edges.set(i,p);var f=o===a;return r?(c.undirectedDegree++,s.undirectedDegree++,f&&t._undirectedSelfLoopCount++):(c.outDegree++,s.inDegree++,f&&t._directedSelfLoopCount++),t.multi?p.attachMulti():p.attach(),r?t._undirectedSize++:t._directedSize++,d.key=i,t.emit("edgeAdded",d),i}function zt(t,e,n,r,i,o,a,u,s){if(!r&&"undirected"===t.type)throw new Y("Graph.".concat(e,": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));if(r&&"directed"===t.type)throw new Y("Graph.".concat(e,": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));if(u)if(s){if("function"!=typeof u)throw new F("Graph.".concat(e,': invalid updater function. Expecting a function but got "').concat(u,'"'))}else if(!h(u))throw new F("Graph.".concat(e,': invalid attributes. Expecting an object but got "').concat(u,'"'));var d;if(o=""+o,a=""+a,s&&(d=u,u=void 0),!t.allowSelfLoops&&o===a)throw new Y("Graph.".concat(e,': source & target are the same ("').concat(o,"\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));var p,f,l=t._nodes.get(o),g=t._nodes.get(a);if(!n&&(p=t._edges.get(i))){if(!(p.source.key===o&&p.target.key===a||r&&p.source.key===a&&p.target.key===o))throw new Y("Graph.".concat(e,': inconsistency detected when attempting to merge the "').concat(i,'" edge with "').concat(o,'" source & "').concat(a,'" target vs. ("').concat(p.source.key,'", "').concat(p.target.key,'").'));f=p}if(f||t.multi||!l||(f=r?l.undirected[a]:l.out[a]),f){var y=[f.key,!1,!1,!1];if(s?!d:!u)return y;if(s){var w=f.attributes;f.attributes=d(w),t.emit("edgeAttributesUpdated",{type:"replace",key:f.key,attributes:f.attributes})}else c(f.attributes,u),t.emit("edgeAttributesUpdated",{type:"merge",key:f.key,attributes:f.attributes,data:u});return y}u=u||{},s&&d&&(u=d(u));var v={key:null,undirected:r,source:o,target:a,attributes:u};if(n)i=t._edgeKeyGenerator();else if(i=""+i,t._edges.has(i))throw new Y("Graph.".concat(e,': the "').concat(i,'" edge already exists in the graph.'));var b=!1,m=!1;l||(l=Ot(t,o,{}),b=!0,o===a&&(g=l,m=!0)),g||(g=Ot(t,a,{}),m=!0),p=new H(r,i,l,g,u),t._edges.set(i,p);var k=o===a;return r?(l.undirectedDegree++,g.undirectedDegree++,k&&t._undirectedSelfLoopCount++):(l.outDegree++,g.inDegree++,k&&t._directedSelfLoopCount++),t.multi?p.attachMulti():p.attach(),r?t._undirectedSize++:t._directedSize++,v.key=i,t.emit("edgeAdded",v),[i,!0,b,m]}function Mt(t,e){t._edges.delete(e.key);var n=e.source,r=e.target,i=e.attributes,o=e.undirected,a=n===r;o?(n.undirectedDegree--,r.undirectedDegree--,a&&t._undirectedSelfLoopCount--):(n.outDegree--,r.inDegree--,a&&t._directedSelfLoopCount--),t.multi?e.detachMulti():e.detach(),o?t._undirectedSize--:t._directedSize--,t.emit("edgeDropped",{key:e.key,attributes:i,source:n.key,target:r.key,undirected:o})}var Wt=function(n){function r(t){var e;if(e=n.call(this)||this,"boolean"!=typeof(t=c({},jt,t)).multi)throw new F("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(t.multi,'".'));if(!Ut.has(t.type))throw new F('Graph.constructor: invalid \'type\' option. Should be one of "mixed", "directed" or "undirected" but got "'.concat(t.type,'".'));if("boolean"!=typeof t.allowSelfLoops)throw new F("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(t.allowSelfLoops,'".'));var r="mixed"===t.type?q:"directed"===t.type?J:V;f(u(e),"NodeDataClass",r);var i="geid_"+Lt()+"_",o=0;return f(u(e),"_attributes",{}),f(u(e),"_nodes",new Map),f(u(e),"_edges",new Map),f(u(e),"_directedSize",0),f(u(e),"_undirectedSize",0),f(u(e),"_directedSelfLoopCount",0),f(u(e),"_undirectedSelfLoopCount",0),f(u(e),"_edgeKeyGenerator",(function(){var t;do{t=i+o++}while(e._edges.has(t));return t})),f(u(e),"_options",t),Nt.forEach((function(t){return f(u(e),t,e[t])})),l(u(e),"order",(function(){return e._nodes.size})),l(u(e),"size",(function(){return e._edges.size})),l(u(e),"directedSize",(function(){return e._directedSize})),l(u(e),"undirectedSize",(function(){return e._undirectedSize})),l(u(e),"selfLoopCount",(function(){return e._directedSelfLoopCount+e._undirectedSelfLoopCount})),l(u(e),"directedSelfLoopCount",(function(){return e._directedSelfLoopCount})),l(u(e),"undirectedSelfLoopCount",(function(){return e._undirectedSelfLoopCount})),l(u(e),"multi",e._options.multi),l(u(e),"type",e._options.type),l(u(e),"allowSelfLoops",e._options.allowSelfLoops),l(u(e),"implementation",(function(){return"graphology"})),e}e(r,n);var i=r.prototype;return i._resetInstanceCounters=function(){this._directedSize=0,this._undirectedSize=0,this._directedSelfLoopCount=0,this._undirectedSelfLoopCount=0},i.hasNode=function(t){return this._nodes.has(""+t)},i.hasDirectedEdge=function(t,e){if("undirected"===this.type)return!1;if(1===arguments.length){var n=""+t,r=this._edges.get(n);return!!r&&!r.undirected}if(2===arguments.length){t=""+t,e=""+e;var i=this._nodes.get(t);if(!i)return!1;var o=i.out[e];return!!o&&(!this.multi||!!o.size)}throw new F("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.hasUndirectedEdge=function(t,e){if("directed"===this.type)return!1;if(1===arguments.length){var n=""+t,r=this._edges.get(n);return!!r&&r.undirected}if(2===arguments.length){t=""+t,e=""+e;var i=this._nodes.get(t);if(!i)return!1;var o=i.undirected[e];return!!o&&(!this.multi||!!o.size)}throw new F("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.hasEdge=function(t,e){if(1===arguments.length){var n=""+t;return this._edges.has(n)}if(2===arguments.length){t=""+t,e=""+e;var r=this._nodes.get(t);if(!r)return!1;var i=void 0!==r.out&&r.out[e];return i||(i=void 0!==r.undirected&&r.undirected[e]),!!i&&(!this.multi||!!i.size)}throw new F("Graph.hasEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.directedEdge=function(t,e){if("undirected"!==this.type){if(t=""+t,e=""+e,this.multi)throw new Y("Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.");var n=this._nodes.get(t);if(!n)throw new I('Graph.directedEdge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.directedEdge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.out&&n.out[e]||void 0;return r?r.key:void 0}},i.undirectedEdge=function(t,e){if("directed"!==this.type){if(t=""+t,e=""+e,this.multi)throw new Y("Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.");var n=this._nodes.get(t);if(!n)throw new I('Graph.undirectedEdge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.undirectedEdge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.undirected&&n.undirected[e]||void 0;return r?r.key:void 0}},i.edge=function(t,e){if(this.multi)throw new Y("Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.");t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.edge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.edge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.out&&n.out[e]||n.undirected&&n.undirected[e]||void 0;if(r)return r.key},i.areDirectedNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areDirectedNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&(e in n.in||e in n.out)},i.areOutNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areOutNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.out},i.areInNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areInNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.in},i.areUndirectedNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areUndirectedNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"directed"!==this.type&&e in n.undirected},i.areNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&(e in n.in||e in n.out)||"directed"!==this.type&&e in n.undirected},i.areInboundNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areInboundNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.in||"directed"!==this.type&&e in n.undirected},i.areOutboundNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areOutboundNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.out||"directed"!==this.type&&e in n.undirected},i.inDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.inDegree},i.outDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.outDegree},i.directedDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.directedDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.inDegree+e.outDegree},i.undirectedDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.undirectedDegree: could not find the "'.concat(t,'" node in the graph.'));return"directed"===this.type?0:e.undirectedDegree},i.inboundDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inboundDegree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.inDegree),n},i.outboundDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outboundDegree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.outDegree),n},i.degree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.degree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.inDegree+e.outDegree),n},i.inDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.in[t],r=n?this.multi?n.size:1:0;return e.inDegree-r},i.outDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.out[t],r=n?this.multi?n.size:1:0;return e.outDegree-r},i.directedDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.directedDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.out[t],r=n?this.multi?n.size:1:0;return e.inDegree+e.outDegree-2*r},i.undirectedDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.undirectedDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("directed"===this.type)return 0;var n=e.undirected[t],r=n?this.multi?n.size:1:0;return e.undirectedDegree-2*r},i.inboundDegreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.inboundDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.inDegree,i+=(e=n.out[t])?this.multi?e.size:1:0),r-i},i.outboundDegreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.outboundDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.outDegree,i+=(e=n.in[t])?this.multi?e.size:1:0),r-i},i.degreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.degreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.inDegree+n.outDegree,i+=2*((e=n.out[t])?this.multi?e.size:1:0)),r-i},i.source=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.source: could not find the "'.concat(t,'" edge in the graph.'));return e.source.key},i.target=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.target: could not find the "'.concat(t,'" edge in the graph.'));return e.target.key},i.extremities=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.extremities: could not find the "'.concat(t,'" edge in the graph.'));return[e.source.key,e.target.key]},i.opposite=function(t,e){t=""+t,e=""+e;var n=this._edges.get(e);if(!n)throw new I('Graph.opposite: could not find the "'.concat(e,'" edge in the graph.'));var r=n.source.key,i=n.target.key;if(t===r)return i;if(t===i)return r;throw new I('Graph.opposite: the "'.concat(t,'" node is not attached to the "').concat(e,'" edge (').concat(r,", ").concat(i,")."))},i.hasExtremity=function(t,e){t=""+t,e=""+e;var n=this._edges.get(t);if(!n)throw new I('Graph.hasExtremity: could not find the "'.concat(t,'" edge in the graph.'));return n.source.key===e||n.target.key===e},i.isUndirected=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isUndirected: could not find the "'.concat(t,'" edge in the graph.'));return e.undirected},i.isDirected=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isDirected: could not find the "'.concat(t,'" edge in the graph.'));return!e.undirected},i.isSelfLoop=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isSelfLoop: could not find the "'.concat(t,'" edge in the graph.'));return e.source===e.target},i.addNode=function(t,e){var n=function(t,e,n){if(n&&!h(n))throw new F('Graph.addNode: invalid attributes. Expecting an object but got "'.concat(n,'"'));if(e=""+e,n=n||{},t._nodes.has(e))throw new Y('Graph.addNode: the "'.concat(e,'" node already exist in the graph.'));var r=new t.NodeDataClass(e,n);return t._nodes.set(e,r),t.emit("nodeAdded",{key:e,attributes:n}),r}(this,t,e);return n.key},i.mergeNode=function(t,e){if(e&&!h(e))throw new F('Graph.mergeNode: invalid attributes. Expecting an object but got "'.concat(e,'"'));t=""+t,e=e||{};var n=this._nodes.get(t);return n?(e&&(c(n.attributes,e),this.emit("nodeAttributesUpdated",{type:"merge",key:t,attributes:n.attributes,data:e})),[t,!1]):(n=new this.NodeDataClass(t,e),this._nodes.set(t,n),this.emit("nodeAdded",{key:t,attributes:e}),[t,!0])},i.updateNode=function(t,e){if(e&&"function"!=typeof e)throw new F('Graph.updateNode: invalid updater function. Expecting a function but got "'.concat(e,'"'));t=""+t;var n=this._nodes.get(t);if(n){if(e){var r=n.attributes;n.attributes=e(r),this.emit("nodeAttributesUpdated",{type:"replace",key:t,attributes:n.attributes})}return[t,!1]}var i=e?e({}):{};return n=new this.NodeDataClass(t,i),this._nodes.set(t,n),this.emit("nodeAdded",{key:t,attributes:i}),[t,!0]},i.dropNode=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.dropNode: could not find the "'.concat(t,'" node in the graph.'));if("undirected"!==this.type){for(var r in n.out){e=n.out[r];do{Mt(this,e),e=e.next}while(e)}for(var i in n.in){e=n.in[i];do{Mt(this,e),e=e.next}while(e)}}if("directed"!==this.type)for(var o in n.undirected){e=n.undirected[o];do{Mt(this,e),e=e.next}while(e)}this._nodes.delete(t),this.emit("nodeDropped",{key:t,attributes:n.attributes})},i.dropEdge=function(t){var e;if(arguments.length>1){var n=""+arguments[0],r=""+arguments[1];if(!(e=s(this,n,r,this.type)))throw new I('Graph.dropEdge: could not find the "'.concat(n,'" -> "').concat(r,'" edge in the graph.'))}else if(t=""+t,!(e=this._edges.get(t)))throw new I('Graph.dropEdge: could not find the "'.concat(t,'" edge in the graph.'));return Mt(this,e),this},i.dropDirectedEdge=function(t,e){if(arguments.length<2)throw new Y("Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.");if(this.multi)throw new Y("Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.");var n=s(this,t=""+t,e=""+e,"directed");if(!n)throw new I('Graph.dropDirectedEdge: could not find a "'.concat(t,'" -> "').concat(e,'" edge in the graph.'));return Mt(this,n),this},i.dropUndirectedEdge=function(t,e){if(arguments.length<2)throw new Y("Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.");if(this.multi)throw new Y("Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.");var n=s(this,t,e,"undirected");if(!n)throw new I('Graph.dropUndirectedEdge: could not find a "'.concat(t,'" -> "').concat(e,'" edge in the graph.'));return Mt(this,n),this},i.clear=function(){this._edges.clear(),this._nodes.clear(),this._resetInstanceCounters(),this.emit("cleared")},i.clearEdges=function(){for(var t,e=this._nodes.values();!0!==(t=e.next()).done;)t.value.clear();this._edges.clear(),this._resetInstanceCounters(),this.emit("edgesCleared")},i.getAttribute=function(t){return this._attributes[t]},i.getAttributes=function(){return this._attributes},i.hasAttribute=function(t){return this._attributes.hasOwnProperty(t)},i.setAttribute=function(t,e){return this._attributes[t]=e,this.emit("attributesUpdated",{type:"set",attributes:this._attributes,name:t}),this},i.updateAttribute=function(t,e){if("function"!=typeof e)throw new F("Graph.updateAttribute: updater should be a function.");var n=this._attributes[t];return this._attributes[t]=e(n),this.emit("attributesUpdated",{type:"set",attributes:this._attributes,name:t}),this},i.removeAttribute=function(t){return delete this._attributes[t],this.emit("attributesUpdated",{type:"remove",attributes:this._attributes,name:t}),this},i.replaceAttributes=function(t){if(!h(t))throw new F("Graph.replaceAttributes: provided attributes are not a plain object.");return this._attributes=t,this.emit("attributesUpdated",{type:"replace",attributes:this._attributes}),this},i.mergeAttributes=function(t){if(!h(t))throw new F("Graph.mergeAttributes: provided attributes are not a plain object.");return c(this._attributes,t),this.emit("attributesUpdated",{type:"merge",attributes:this._attributes,data:t}),this},i.updateAttributes=function(t){if("function"!=typeof t)throw new F("Graph.updateAttributes: provided updater is not a function.");return this._attributes=t(this._attributes),this.emit("attributesUpdated",{type:"update",attributes:this._attributes}),this},i.updateEachNodeAttributes=function(t,e){if("function"!=typeof t)throw new F("Graph.updateEachNodeAttributes: expecting an updater function.");if(e&&!g(e))throw new F("Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}");for(var n,r,i=this._nodes.values();!0!==(n=i.next()).done;)(r=n.value).attributes=t(r.key,r.attributes);this.emit("eachNodeAttributesUpdated",{hints:e||null})},i.updateEachEdgeAttributes=function(t,e){if("function"!=typeof t)throw new F("Graph.updateEachEdgeAttributes: expecting an updater function.");if(e&&!g(e))throw new F("Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}");for(var n,r,i,o,a=this._edges.values();!0!==(n=a.next()).done;)i=(r=n.value).source,o=r.target,r.attributes=t(r.key,r.attributes,i.key,o.key,i.attributes,o.attributes,r.undirected);this.emit("eachEdgeAttributesUpdated",{hints:e||null})},i.forEachAdjacencyEntry=function(t){if("function"!=typeof t)throw new F("Graph.forEachAdjacencyEntry: expecting a callback.");Et(!1,!1,!1,this,t)},i.forEachAdjacencyEntryWithOrphans=function(t){if("function"!=typeof t)throw new F("Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.");Et(!1,!1,!0,this,t)},i.forEachAssymetricAdjacencyEntry=function(t){if("function"!=typeof t)throw new F("Graph.forEachAssymetricAdjacencyEntry: expecting a callback.");Et(!1,!0,!1,this,t)},i.forEachAssymetricAdjacencyEntryWithOrphans=function(t){if("function"!=typeof t)throw new F("Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.");Et(!1,!0,!0,this,t)},i.nodes=function(){return"function"==typeof Array.from?Array.from(this._nodes.keys()):T(this._nodes.keys(),this._nodes.size)},i.forEachNode=function(t){if("function"!=typeof t)throw new F("Graph.forEachNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)t((n=e.value).key,n.attributes)},i.findNode=function(t){if("function"!=typeof t)throw new F("Graph.findNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(t((n=e.value).key,n.attributes))return n.key},i.mapNodes=function(t){if("function"!=typeof t)throw new F("Graph.mapNode: expecting a callback.");for(var e,n,r=this._nodes.values(),i=new Array(this.order),o=0;!0!==(e=r.next()).done;)n=e.value,i[o++]=t(n.key,n.attributes);return i},i.someNode=function(t){if("function"!=typeof t)throw new F("Graph.someNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(t((n=e.value).key,n.attributes))return!0;return!1},i.everyNode=function(t){if("function"!=typeof t)throw new F("Graph.everyNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(!t((n=e.value).key,n.attributes))return!1;return!0},i.filterNodes=function(t){if("function"!=typeof t)throw new F("Graph.filterNodes: expecting a callback.");for(var e,n,r=this._nodes.values(),i=[];!0!==(e=r.next()).done;)t((n=e.value).key,n.attributes)&&i.push(n.key);return i},i.reduceNodes=function(t,e){if("function"!=typeof t)throw new F("Graph.reduceNodes: expecting a callback.");if(arguments.length<2)throw new F("Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.");for(var n,r,i=e,o=this._nodes.values();!0!==(n=o.next()).done;)i=t(i,(r=n.value).key,r.attributes);return i},i.nodeEntries=function(){var t=this._nodes.values();return new O((function(){var e=t.next();if(e.done)return e;var n=e.value;return{value:{node:n.key,attributes:n.attributes},done:!1}}))},i.export=function(){var t=new Array(this._nodes.size),e=0;this._nodes.forEach((function(n,r){t[e++]=function(t,e){var n={key:t};return p(e.attributes)||(n.attributes=c({},e.attributes)),n}(r,n)}));var n=new Array(this._edges.size);return e=0,this._edges.forEach((function(t,r){n[e++]=function(t,e){var n={key:t,source:e.source.key,target:e.target.key};return p(e.attributes)||(n.attributes=c({},e.attributes)),e.undirected&&(n.undirected=!0),n}(r,t)})),{options:{type:this.type,multi:this.multi,allowSelfLoops:this.allowSelfLoops},attributes:this.getAttributes(),nodes:t,edges:n}},i.import=function(t){var e,n,r,i,o,a=this,u=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(d(t))return t.forEachNode((function(t,e){u?a.mergeNode(t,e):a.addNode(t,e)})),t.forEachEdge((function(t,e,n,r,i,o,c){u?c?a.mergeUndirectedEdgeWithKey(t,n,r,e):a.mergeDirectedEdgeWithKey(t,n,r,e):c?a.addUndirectedEdgeWithKey(t,n,r,e):a.addDirectedEdgeWithKey(t,n,r,e)})),this;if(!h(t))throw new F("Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.");if(t.attributes){if(!h(t.attributes))throw new F("Graph.import: invalid attributes. Expecting a plain object.");u?this.mergeAttributes(t.attributes):this.replaceAttributes(t.attributes)}if(t.nodes){if(r=t.nodes,!Array.isArray(r))throw new F("Graph.import: invalid nodes. Expecting an array.");for(e=0,n=r.length;e<n;e++){At(i=r[e]);var c=i,s=c.key,p=c.attributes;u?this.mergeNode(s,p):this.addNode(s,p)}}if(t.edges){if(r=t.edges,!Array.isArray(r))throw new F("Graph.import: invalid edges. Expecting an array.");for(e=0,n=r.length;e<n;e++){St(o=r[e]);var f=o,l=f.source,g=f.target,y=f.attributes,w=f.undirected,v=void 0!==w&&w;"key"in o?(u?v?this.mergeUndirectedEdgeWithKey:this.mergeDirectedEdgeWithKey:v?this.addUndirectedEdgeWithKey:this.addDirectedEdgeWithKey).call(this,o.key,l,g,y):(u?v?this.mergeUndirectedEdge:this.mergeDirectedEdge:v?this.addUndirectedEdge:this.addDirectedEdge).call(this,l,g,y)}}return this},i.nullCopy=function(t){var e=new r(c({},this._options,t));return e.replaceAttributes(c({},this.getAttributes())),e},i.emptyCopy=function(t){var e=this.nullCopy(t);return this._nodes.forEach((function(t,n){var r=c({},t.attributes);t=new e.NodeDataClass(n,r),e._nodes.set(n,t)})),e},i.copy=function(t){if("string"==typeof(t=t||{}).type&&t.type!==this.type&&"mixed"!==t.type)throw new Y('Graph.copy: cannot create an incompatible copy from "'.concat(this.type,'" type to "').concat(t.type,'" because this would mean losing information about the current graph.'));if("boolean"==typeof t.multi&&t.multi!==this.multi&&!0!==t.multi)throw new Y("Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.");if("boolean"==typeof t.allowSelfLoops&&t.allowSelfLoops!==this.allowSelfLoops&&!0!==t.allowSelfLoops)throw new Y("Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.");for(var e,n,r=this.emptyCopy(t),i=this._edges.values();!0!==(e=i.next()).done;)Ct(r,"copy",!1,(n=e.value).undirected,n.key,n.source.key,n.target.key,c({},n.attributes));return r},i.toJSON=function(){return this.export()},i.toString=function(){return"[object Graph]"},i.inspect=function(){var e=this,n={};this._nodes.forEach((function(t,e){n[e]=t.attributes}));var r={},i={};this._edges.forEach((function(t,n){var o,a=t.undirected?"--":"->",u="",c=t.source.key,s=t.target.key;t.undirected&&c>s&&(o=c,c=s,s=o);var d="(".concat(c,")").concat(a,"(").concat(s,")");n.startsWith("geid_")?e.multi&&(void 0===i[d]?i[d]=0:i[d]++,u+="".concat(i[d],". ")):u+="[".concat(n,"]: "),r[u+=d]=t.attributes}));var o={};for(var a in this)this.hasOwnProperty(a)&&!Nt.has(a)&&"function"!=typeof this[a]&&"symbol"!==t(a)&&(o[a]=this[a]);return o.attributes=this._attributes,o.nodes=n,o.edges=r,f(o,"constructor",this.constructor),o},r}(w.exports.EventEmitter);"undefined"!=typeof Symbol&&(Wt.prototype[Symbol.for("nodejs.util.inspect.custom")]=Wt.prototype.inspect),[{name:function(t){return"".concat(t,"Edge")},generateKey:!0},{name:function(t){return"".concat(t,"DirectedEdge")},generateKey:!0,type:"directed"},{name:function(t){return"".concat(t,"UndirectedEdge")},generateKey:!0,type:"undirected"},{name:function(t){return"".concat(t,"EdgeWithKey")}},{name:function(t){return"".concat(t,"DirectedEdgeWithKey")},type:"directed"},{name:function(t){return"".concat(t,"UndirectedEdgeWithKey")},type:"undirected"}].forEach((function(t){["add","merge","update"].forEach((function(e){var n=t.name(e),r="add"===e?Ct:zt;t.generateKey?Wt.prototype[n]=function(i,o,a){return r(this,n,!0,"undirected"===(t.type||this.type),null,i,o,a,"update"===e)}:Wt.prototype[n]=function(i,o,a,u){return r(this,n,!1,"undirected"===(t.type||this.type),i,o,a,u,"update"===e)}}))})),function(t){X.forEach((function(e){var n=e.name,r=e.attacher;r(t,n("Node"),0),r(t,n("Source"),1),r(t,n("Target"),2),r(t,n("Opposite"),3)}))}(Wt),function(t){Z.forEach((function(e){var n=e.name,r=e.attacher;r(t,n("Edge"),"mixed"),r(t,n("DirectedEdge"),"directed"),r(t,n("UndirectedEdge"),"undirected")}))}(Wt),function(t){nt.forEach((function(e){!function(t,e){var n=e.name,r=e.type,i=e.direction;t.prototype[n]=function(t,e){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return[];if(!arguments.length)return st(this,r);if(1===arguments.length){t=""+t;var o=this._nodes.get(t);if(void 0===o)throw new I("Graph.".concat(n,': could not find the "').concat(t,'" node in the graph.'));return ft(this.multi,"mixed"===r?this.type:r,i,o)}if(2===arguments.length){t=""+t,e=""+e;var a=this._nodes.get(t);if(!a)throw new I("Graph.".concat(n,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(n,':  could not find the "').concat(e,'" target node in the graph.'));return yt(r,this.multi,i,a,e)}throw new F("Graph.".concat(n,": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length,")."))}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="forEach"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e,n){if("mixed"===r||"mixed"===this.type||r===this.type){if(1===arguments.length)return dt(!1,this,r,n=t);if(2===arguments.length){t=""+t,n=e;var a=this._nodes.get(t);if(void 0===a)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return pt(!1,this.multi,"mixed"===r?this.type:r,i,a,n)}if(3===arguments.length){t=""+t,e=""+e;var u=this._nodes.get(t);if(!u)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return gt(!1,r,this.multi,i,u,e,n)}throw new F("Graph.".concat(o,": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length,")."))}};var a="map"+n[0].toUpperCase()+n.slice(1);t.prototype[a]=function(){var t,e=Array.prototype.slice.call(arguments),n=e.pop();if(0===e.length){var i=0;"directed"!==r&&(i+=this.undirectedSize),"undirected"!==r&&(i+=this.directedSize),t=new Array(i);var a=0;e.push((function(e,r,i,o,u,c,s){t[a++]=n(e,r,i,o,u,c,s)}))}else t=[],e.push((function(e,r,i,o,a,u,c){t.push(n(e,r,i,o,a,u,c))}));return this[o].apply(this,e),t};var u="filter"+n[0].toUpperCase()+n.slice(1);t.prototype[u]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop(),n=[];return t.push((function(t,r,i,o,a,u,c){e(t,r,i,o,a,u,c)&&n.push(t)})),this[o].apply(this,t),n};var c="reduce"+n[0].toUpperCase()+n.slice(1);t.prototype[c]=function(){var t,e,n=Array.prototype.slice.call(arguments);if(n.length<2||n.length>4)throw new F("Graph.".concat(c,": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(n.length,")."));if("function"==typeof n[n.length-1]&&"function"!=typeof n[n.length-2])throw new F("Graph.".concat(c,": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));2===n.length?(t=n[0],e=n[1],n=[]):3===n.length?(t=n[1],e=n[2],n=[n[0]]):4===n.length&&(t=n[2],e=n[3],n=[n[0],n[1]]);var r=e;return n.push((function(e,n,i,o,a,u,c){r=t(r,e,n,i,o,a,u,c)})),this[o].apply(this,n),r}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="find"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e,n){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return!1;if(1===arguments.length)return dt(!0,this,r,n=t);if(2===arguments.length){t=""+t,n=e;var a=this._nodes.get(t);if(void 0===a)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return pt(!0,this.multi,"mixed"===r?this.type:r,i,a,n)}if(3===arguments.length){t=""+t,e=""+e;var u=this._nodes.get(t);if(!u)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return gt(!0,r,this.multi,i,u,e,n)}throw new F("Graph.".concat(o,": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length,")."))};var a="some"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[a]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop();return t.push((function(t,n,r,i,o,a,u){return e(t,n,r,i,o,a,u)})),!!this[o].apply(this,t)};var u="every"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[u]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop();return t.push((function(t,n,r,i,o,a,u){return!e(t,n,r,i,o,a,u)})),!this[o].apply(this,t)}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o=n.slice(0,-1)+"Entries";t.prototype[o]=function(t,e){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return O.empty();if(!arguments.length)return ht(this,r);if(1===arguments.length){t=""+t;var n=this._nodes.get(t);if(!n)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return lt(r,i,n)}if(2===arguments.length){t=""+t,e=""+e;var a=this._nodes.get(t);if(!a)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return wt(r,i,a,e)}throw new F("Graph.".concat(o,": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length,")."))}}(t,e)}))}(Wt),function(t){vt.forEach((function(e){Gt(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="forEach"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e){if("mixed"===r||"mixed"===this.type||r===this.type){t=""+t;var n=this._nodes.get(t);if(void 0===n)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));kt(!1,"mixed"===r?this.type:r,i,n,e)}};var a="map"+n[0].toUpperCase()+n.slice(1);t.prototype[a]=function(t,e){var n=[];return this[o](t,(function(t,r){n.push(e(t,r))})),n};var u="filter"+n[0].toUpperCase()+n.slice(1);t.prototype[u]=function(t,e){var n=[];return this[o](t,(function(t,r){e(t,r)&&n.push(t)})),n};var c="reduce"+n[0].toUpperCase()+n.slice(1);t.prototype[c]=function(t,e,n){if(arguments.length<3)throw new F("Graph.".concat(c,": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));var r=n;return this[o](t,(function(t,n){r=e(r,t,n)})),r}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o=n[0].toUpperCase()+n.slice(1,-1),a="find"+o;t.prototype[a]=function(t,e){if("mixed"===r||"mixed"===this.type||r===this.type){t=""+t;var n=this._nodes.get(t);if(void 0===n)throw new I("Graph.".concat(a,': could not find the "').concat(t,'" node in the graph.'));return kt(!0,"mixed"===r?this.type:r,i,n,e)}};var u="some"+o;t.prototype[u]=function(t,e){return!!this[a](t,e)};var c="every"+o;t.prototype[c]=function(t,e){return!this[a](t,(function(t,n){return!e(t,n)}))}}(t,e),xt(t,e)}))}(Wt);var Pt=function(t){function n(e){var n=c({type:"directed"},e);if("multi"in n&&!1!==n.multi)throw new F("DirectedGraph.from: inconsistent indication that the graph should be multi in given options!");if("directed"!==n.type)throw new F('DirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Rt=function(t){function n(e){var n=c({type:"undirected"},e);if("multi"in n&&!1!==n.multi)throw new F("UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!");if("undirected"!==n.type)throw new F('UndirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Kt=function(t){function n(e){var n=c({multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiGraph.from: inconsistent indication that the graph should be simple in given options!");return t.call(this,n)||this}return e(n,t),n}(Wt),Tt=function(t){function n(e){var n=c({type:"directed",multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!");if("directed"!==n.type)throw new F('MultiDirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Bt=function(t){function n(e){var n=c({type:"undirected",multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!");if("undirected"!==n.type)throw new F('MultiUndirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt);function Ft(t){t.from=function(e,n){var r=c({},e.options,n),i=new t(r);return i.import(e),i}}return Ft(Wt),Ft(Pt),Ft(Rt),Ft(Kt),Ft(Tt),Ft(Bt),Wt.Graph=Wt,Wt.DirectedGraph=Pt,Wt.UndirectedGraph=Rt,Wt.MultiGraph=Kt,Wt.MultiDirectedGraph=Tt,Wt.MultiUndirectedGraph=Bt,Wt.InvalidArgumentsGraphError=F,Wt.NotFoundGraphError=I,Wt.UsageGraphError=Y,Wt}));
+//# sourceMappingURL=graphology.umd.min.js.map
diff --git a/libs/shared/graph-layout/node_modules/graphology/package.json b/libs/shared/graph-layout/node_modules/graphology/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..192894e7d629fb3be2591579dae3dc3fb57731d3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/package.json
@@ -0,0 +1,84 @@
+{
+  "name": "graphology",
+  "version": "0.24.1",
+  "description": "A robust and multipurpose Graph object for JavaScript.",
+  "main": "dist/graphology.cjs.js",
+  "module": "dist/graphology.esm.js",
+  "browser": "dist/graphology.umd.min.js",
+  "types": "dist/graphology.d.ts",
+  "scripts": {
+    "clean": "rimraf dist specs",
+    "build": "npm run clean && rollup -c && babel tests --out-dir specs && cp src/endpoint.esm.d.ts dist/graphology.d.ts",
+    "prepare": "npm run build",
+    "prepublishOnly": "npm test && npm run test:types && npm run build",
+    "test": "mocha -u exports --require @babel/register ./test.js",
+    "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns --strictNullChecks ./test-types.ts"
+  },
+  "files": [
+    "dist/*.js",
+    "dist/*.ts",
+    "specs"
+  ],
+  "keywords": [
+    "graph",
+    "graph theory",
+    "directed",
+    "undirected",
+    "network"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "contributors": [
+    {
+      "name": "Alexis Jacomy",
+      "url": "http://github.com/jacomyal"
+    },
+    {
+      "name": "Benjamin Ooghe-Tabanou",
+      "url": "http://github.com/boogheta"
+    },
+    {
+      "name": "Guillaume Plique",
+      "url": "http://github.com/Yomguithereal"
+    }
+  ],
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "dependencies": {
+    "events": "^3.3.0",
+    "obliterator": "^2.0.2"
+  },
+  "peerDependencies": {
+    "graphology-types": ">=0.24.0"
+  },
+  "babel": {
+    "presets": [
+      "@babel/preset-env"
+    ],
+    "plugins": [
+      [
+        "@babel/transform-classes",
+        {
+          "loose": true
+        }
+      ],
+      [
+        "@babel/transform-destructuring",
+        {
+          "loose": true
+        }
+      ],
+      [
+        "@babel/transform-spread",
+        {
+          "loose": true
+        }
+      ]
+    ]
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/attributes.js b/libs/shared/graph-layout/node_modules/graphology/specs/attributes.js
new file mode 100644
index 0000000000000000000000000000000000000000..f026c38eacec861c6fea6a2837d868a8233d9ed0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/attributes.js
@@ -0,0 +1,992 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = attributes;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function attributes(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+
+  function commonTests(method) {
+    return _defineProperty({}, '#.' + method, {
+      'it should throw if the given path is not found.': function itShouldThrowIfTheGivenPathIsNotFound() {
+        if (!method.includes('Edge')) return;
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph[method]('source', 'target', 'name', 'value');
+        }, notFound());
+      },
+      'it should throw when using a path on a multi graph.': function itShouldThrowWhenUsingAPathOnAMultiGraph() {
+        if (!method.includes('Edge')) return;
+        var graph = new Graph({
+          multi: true
+        });
+
+        _assert["default"]["throws"](function () {
+          graph[method]('source', 'target', 'name', 'value');
+        }, usage());
+      },
+      'it should throw if the element is not found in the graph.': function itShouldThrowIfTheElementIsNotFoundInTheGraph() {
+        var graph = new Graph();
+
+        if (method.includes('Edge') && method.includes('Directed') || method.includes('Undirected')) {
+          _assert["default"]["throws"](function () {
+            graph[method]('Test');
+          }, usage());
+        } else {
+          _assert["default"]["throws"](function () {
+            graph[method]('Test');
+          }, notFound());
+        }
+      }
+    });
+  }
+
+  var tests = {};
+  var relevantMethods = Object.keys(Graph.prototype).filter(function (name) {
+    return (name.includes('NodeAttribute') || name.includes('EdgeAttribute') || name.includes('SourceAttribute') || name.includes('TargetAttribute') || name.includes('OppositeAttribute')) && !name.includes('Each');
+  });
+  relevantMethods.forEach(function (method) {
+    return (0, _helpers.deepMerge)(tests, commonTests(method));
+  });
+  return (0, _helpers.deepMerge)(tests, {
+    '#.getAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), undefined);
+      }
+    },
+    '#.getNodeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('Martha', 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('Martha', 'age'), undefined);
+      }
+    },
+    '#.getSourceAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        var _graph$mergeEdge = graph.mergeEdge('Martha', 'Riwan'),
+            edge = _graph$mergeEdge[0];
+
+        _assert["default"].strictEqual(graph.getSourceAttribute(edge, 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        var _graph$mergeEdge2 = graph.mergeEdge('Martha', 'Riwan'),
+            edge = _graph$mergeEdge2[0];
+
+        _assert["default"].strictEqual(graph.getSourceAttribute(edge, 'age'), undefined);
+      }
+    },
+    '#.getTargetAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        var _graph$mergeEdge3 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge3[0];
+
+        _assert["default"].strictEqual(graph.getTargetAttribute(edge, 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        var _graph$mergeEdge4 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge4[0];
+
+        _assert["default"].strictEqual(graph.getTargetAttribute(edge, 'age'), undefined);
+      }
+    },
+    '#.getOppositeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+        graph.addNode('Riwan', {
+          age: 25
+        });
+
+        var _graph$mergeEdge5 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge5[0];
+
+        _assert["default"].strictEqual(graph.getOppositeAttribute('Riwan', edge, 'age'), 34);
+
+        _assert["default"].strictEqual(graph.getOppositeAttribute('Martha', edge, 'age'), 25);
+      }
+    },
+    '#.getEdgeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute('John', 'Thomas', 'weight'), 2);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas');
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), undefined);
+      }
+    },
+    '#.getAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph'
+        });
+      },
+      'it should return an empty object if the node does not have attributes.': function itShouldReturnAnEmptyObjectIfTheNodeDoesNotHaveAttributes() {
+        var graph = new Graph();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {});
+      }
+    },
+    '#.getNodeAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {
+          age: 34
+        });
+      },
+      'it should return an empty object if the node does not have attributes.': function itShouldReturnAnEmptyObjectIfTheNodeDoesNotHaveAttributes() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {});
+      }
+    },
+    '#.getEdgeAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas', {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Thomas'), {
+          weight: 2
+        });
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas', 'weight'), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas', 'weight'), {
+          weight: 3
+        });
+      },
+      'it should return an empty object if the edge does not have attributes.': function itShouldReturnAnEmptyObjectIfTheEdgeDoesNotHaveAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas');
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {});
+      }
+    },
+    '#.hasAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.hasAttribute('name'), true);
+
+        _assert["default"].strictEqual(graph.hasAttribute('info'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.hasAttribute('toString'), false);
+      }
+    },
+    '#.hasNodeAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'age'), true);
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'eyes'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'toString'), false);
+      }
+    },
+    '#.hasEdgeAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdgeWithKey('J->M', 'John', 'Martha', {
+          weight: 10
+        });
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'weight'), true);
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'type'), false);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdgeAttribute('John', 'Thomas', 'weight'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdgeWithKey('J->M', 'John', 'Martha', {
+          weight: 10
+        });
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'toString'), false);
+      }
+    },
+    '#.setAttribute': {
+      "it should correctly set the graph's attribute.": function itShouldCorrectlySetTheGraphSAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      }
+    },
+    '#.setNodeAttribute': {
+      "it should correctly set the node's attribute.": function itShouldCorrectlySetTheNodeSAttribute() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+        graph.setNodeAttribute('John', 'age', 45);
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 45);
+      }
+    },
+    '#.setEdgeAttribute': {
+      "it should correctly set the edge's attribute.": function itShouldCorrectlySetTheEdgeSAttribute() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+        graph.setEdgeAttribute(edge, 'weight', 40);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 40);
+
+        graph.setEdgeAttribute('John', 'Martha', 'weight', 60);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 60);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.setDirectedEdgeAttribute('John', 'Thomas', 'weight', 2);
+        graph.setUndirectedEdgeAttribute('John', 'Thomas', 'weight', 3);
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      }
+    },
+    '#.updateAttribute': {
+      'it should throw if the updater is not a function.': function itShouldThrowIfTheUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        graph.setAttribute('count', 0);
+
+        _assert["default"]["throws"](function () {
+          graph.updateAttribute('count', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the graph's attribute.": function itShouldCorrectlySetTheGraphSAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.updateAttribute('name', function (name) {
+          return name + '1';
+        });
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph1');
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 'graph';
+        };
+
+        graph.updateAttribute('name', updater);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      }
+    },
+    '#.updateNodeAttribute': {
+      'it should throw if given an invalid updater.': function itShouldThrowIfGivenAnInvalidUpdater() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttribute('John', 'age', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      'it should throw if not enough arguments are provided.': function itShouldThrowIfNotEnoughArgumentsAreProvided() {
+        var graph = new Graph();
+        graph.addNode('Lucy');
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttribute('Lucy', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the node's attribute.": function itShouldCorrectlySetTheNodeSAttribute() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+        graph.updateNodeAttribute('John', 'age', function (x) {
+          return x + 1;
+        });
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 21);
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 10;
+        };
+
+        graph.updateNodeAttribute('John', 'age', updater);
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 10);
+      }
+    },
+    '#.updateEdgeAttribute': {
+      'it should throw if given an invalid updater.': function itShouldThrowIfGivenAnInvalidUpdater() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeAttribute('John', 'Martha', 'weight', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the edge's attribute.": function itShouldCorrectlySetTheEdgeSAttribute() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+        graph.updateEdgeAttribute(edge, 'weight', function (x) {
+          return x + 1;
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 4);
+
+        graph.updateEdgeAttribute('John', 'Martha', 'weight', function (x) {
+          return x + 2;
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 6);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.updateDirectedEdgeAttribute('John', 'Thomas', 'weight', function (x) {
+          return x + 2;
+        });
+        graph.updateUndirectedEdgeAttribute('John', 'Thomas', 'weight', function (x) {
+          return x + 3;
+        });
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 10;
+        };
+
+        graph.updateEdgeAttribute(edge, 'weight', updater);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 10);
+      }
+    },
+    '#.removeAttribute': {
+      'it should correctly remove the attribute.': function itShouldCorrectlyRemoveTheAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.removeAttribute('name');
+
+        _assert["default"].strictEqual(graph.hasAttribute('name'), false);
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {});
+      }
+    },
+    '#.removeNodeAttribute': {
+      'it should correctly remove the attribute.': function itShouldCorrectlyRemoveTheAttribute() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+        graph.removeNodeAttribute('Martha', 'age');
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('Martha', 'age'), false);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {});
+      }
+    },
+    '#.removeEdgeAttribute': {
+      'it should correclty remove the attribute.': function itShouldCorrecltyRemoveTheAttribute() {
+        var graph = new Graph();
+
+        var _graph$mergeEdge6 = graph.mergeEdge('John', 'Martha', {
+          weight: 1,
+          size: 3
+        }),
+            edge = _graph$mergeEdge6[0];
+
+        graph.removeEdgeAttribute('John', 'Martha', 'weight');
+        graph.removeEdgeAttribute(edge, 'size');
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute(edge, 'weight'), false);
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute(edge, 'size'), false);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {});
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+        graph.removeDirectedEdgeAttribute('John', 'Thomas', 'weight');
+        graph.removeUndirectedEdgeAttribute('John', 'Thomas', 'weight');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+      }
+    },
+    '#.replaceAttribute': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.replaceAttributes(true);
+        }, invalid());
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.replaceAttributes({
+          name: 'other graph'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'other graph'
+        });
+      }
+    },
+    '#.replaceNodeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.replaceNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.replaceNodeAttributes('John', {
+          age: 23,
+          eyes: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 23,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.replaceEdgeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.replaceEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.replaceDirectedEdgeAttributes('John', 'Thomas', {
+          weight: 2
+        });
+        graph.replaceUndirectedEdgeAttributes('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3
+        });
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.replaceEdgeAttributes(edge, {
+          weight: 4,
+          type: 'KNOWS'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 4,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.mergeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.mergeAttributes(true);
+        }, invalid());
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.mergeAttributes({
+          color: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph',
+          color: 'blue'
+        });
+      }
+    },
+    '#.mergeNodeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.mergeNodeAttributes('John', {
+          eyes: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 45,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.mergeEdgeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.mergeDirectedEdgeAttributes('John', 'Thomas', {
+          weight: 2
+        });
+        graph.mergeUndirectedEdgeAttributes('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2,
+          test: 0
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3,
+          test: 0
+        });
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.mergeEdgeAttributes(edge, {
+          type: 'KNOWS'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 1,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.updateAttributes': {
+      'it should throw if given updater is not a function.': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateAttribute(true);
+        }, invalid());
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.updateAttributes(function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            color: 'blue'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph',
+          color: 'blue'
+        });
+      }
+    },
+    '#.updateNodeAttributes': {
+      'it should throw if given updater is not a function': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.updateNodeAttributes('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            eyes: 'blue'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 45,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.updateEdgeAttributes': {
+      'it should throw if given updater is not a function.': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.updateDirectedEdgeAttributes('John', 'Thomas', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 2
+          });
+        });
+        graph.updateUndirectedEdgeAttributes('John', 'Thomas', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 3
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2,
+          test: 0
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3,
+          test: 0
+        });
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.updateEdgeAttributes(edge, function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            type: 'KNOWS'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 1,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.updateEachNodeAttributes': {
+      'it should throw when given invalid arguments.': function itShouldThrowWhenGivenInvalidArguments() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(null);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(Function.prototype, 'test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(Function.prototype, {
+            attributes: 'yes'
+          });
+        }, invalid());
+      },
+      "it should update each node's attributes.": function itShouldUpdateEachNodeSAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes().map(function (n) {
+          return graph.getNodeAttributes(n);
+        }), [{
+          age: 35
+        }, {
+          age: 57
+        }, {
+          age: 14
+        }]);
+      }
+    },
+    '#.updateEachEdgeAttributes': {
+      'it should throw when given invalid arguments.': function itShouldThrowWhenGivenInvalidArguments() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(null);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(Function.prototype, 'test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(Function.prototype, {
+            attributes: 'yes'
+          });
+        }, invalid());
+      },
+      "it should update each node's attributes.": function itShouldUpdateEachNodeSAttributes() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        graph.updateEachEdgeAttributes(function (edge, attr, source, _t, _sa, _ta, undirected) {
+          _assert["default"].strictEqual(source, 'John');
+
+          _assert["default"].strictEqual(undirected, false);
+
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.mapEdges(function (_, attr) {
+          return attr;
+        }), [{
+          weight: 2
+        }, {
+          weight: 11
+        }]);
+      }
+    }
+  });
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/events.js b/libs/shared/graph-layout/node_modules/graphology/specs/events.js
new file mode 100644
index 0000000000000000000000000000000000000000..df9c4c44fca1e880288ccaedf0bd5494d0599a17
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/events.js
@@ -0,0 +1,426 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = events;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var VALID_TYPES = new Set(['set', 'merge', 'replace', 'remove']);
+
+function events(Graph) {
+  return {
+    nodeAdded: {
+      'it should fire when a node is added.': function itShouldFireWhenANodeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'John');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            age: 34
+          });
+        });
+        graph.on('nodeAdded', handler);
+        graph.addNode('John', {
+          age: 34
+        });
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    edgeAdded: {
+      'it should fire when an edge is added.': function itShouldFireWhenAnEdgeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'J->T');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            weight: 1
+          });
+
+          _assert["default"].strictEqual(data.source, 'John');
+
+          _assert["default"].strictEqual(data.target, 'Thomas');
+
+          _assert["default"].strictEqual(data.undirected, false);
+        });
+        graph.on('edgeAdded', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas', {
+          weight: 1
+        });
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    nodeDropped: {
+      'it should fire when a node is dropped.': function itShouldFireWhenANodeIsDropped() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'John');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            age: 34
+          });
+        });
+        graph.on('nodeDropped', handler);
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.dropNode('John');
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    edgeDropped: {
+      'it should fire when an edge is added.': function itShouldFireWhenAnEdgeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'J->T');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            weight: 1
+          });
+
+          _assert["default"].strictEqual(data.source, 'John');
+
+          _assert["default"].strictEqual(data.target, 'Thomas');
+
+          _assert["default"].strictEqual(data.undirected, false);
+        });
+        graph.on('edgeDropped', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas', {
+          weight: 1
+        });
+        graph.dropEdge('J->T');
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    cleared: {
+      'it should fire when the graph is cleared.': function itShouldFireWhenTheGraphIsCleared() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)();
+        graph.on('cleared', handler);
+        graph.clear();
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    attributesUpdated: {
+      'it should fire when a graph attribute is updated.': function itShouldFireWhenAGraphAttributeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'name');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'name');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              author: 'John'
+            });
+          }
+
+          _assert["default"].deepStrictEqual(payload.attributes, graph.getAttributes());
+        });
+        graph.on('attributesUpdated', handler);
+        graph.setAttribute('name', 'Awesome graph');
+        graph.replaceAttributes({
+          name: 'Shitty graph'
+        });
+        graph.mergeAttributes({
+          author: 'John'
+        });
+        graph.removeAttribute('name');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      }
+    },
+    nodeAttributesUpdated: {
+      "it should fire when a node's attributes are updated.": function itShouldFireWhenANodeSAttributesAreUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.key, 'John');
+
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'age');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'eyes');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              eyes: 'blue'
+            });
+          }
+
+          _assert["default"].strictEqual(payload.attributes, graph.getNodeAttributes(payload.key));
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.addNode('John');
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.replaceNodeAttributes('John', {
+          age: 56
+        });
+        graph.mergeNodeAttributes('John', {
+          eyes: 'blue'
+        });
+        graph.removeNodeAttribute('John', 'eyes');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      },
+      'it should fire when a node is merged.': function itShouldFireWhenANodeIsMerged() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'merge',
+            key: 'John',
+            attributes: {
+              count: 2
+            },
+            data: {
+              count: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(payload.key), {
+            count: 2
+          });
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.mergeNode('John', {
+          count: 1
+        });
+        graph.mergeNode('John', {
+          count: 2
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should fire when a node is updated.': function itShouldFireWhenANodeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'replace',
+            key: 'John',
+            attributes: {
+              count: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(payload.key), {
+            count: 2
+          });
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.mergeNode('John', {
+          count: 1
+        });
+        graph.updateNode('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            count: attr.count + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    edgeAttributesUpdated: {
+      "it should fire when an edge's attributes are updated.": function itShouldFireWhenAnEdgeSAttributesAreUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.key, 'J->T');
+
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'weight');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'type');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              type: 'KNOWS'
+            });
+          }
+
+          _assert["default"].strictEqual(payload.attributes, graph.getEdgeAttributes(payload.key));
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas');
+        graph.setEdgeAttribute('J->T', 'weight', 34);
+        graph.replaceEdgeAttributes('J->T', {
+          weight: 56
+        });
+        graph.mergeEdgeAttributes('J->T', {
+          type: 'KNOWS'
+        });
+        graph.removeEdgeAttribute('J->T', 'type');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      },
+      'it should fire when an edge is merged.': function itShouldFireWhenAnEdgeIsMerged() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'merge',
+            key: graph.edge('John', 'Mary'),
+            attributes: {
+              weight: 2
+            },
+            data: {
+              weight: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getEdgeAttributes(payload.key), {
+            weight: 2
+          });
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        graph.mergeEdge('John', 'Mary', {
+          weight: 1
+        });
+        graph.mergeEdge('John', 'Mary', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should fire when an edge is updated.': function itShouldFireWhenAnEdgeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'replace',
+            key: 'j->m',
+            attributes: {
+              weight: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getEdgeAttributes(payload.key), {
+            weight: 2
+          });
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        graph.mergeEdgeWithKey('j->m', 'John', 'Mary', {
+          weight: 1
+        });
+        graph.updateEdgeWithKey('j->m', 'John', 'Mary', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    eachNodeAttributesUpdated: {
+      'it should fire when using #.updateEachNodeAttributes.': function itShouldFireWhenUsingUpdateEachNodeAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.hints, null);
+        });
+        graph.on('eachNodeAttributesUpdated', handler);
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should provide hints when user gave them.': function itShouldProvideHintsWhenUserGaveThem() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload.hints, {
+            attributes: ['age']
+          });
+        });
+        graph.on('eachNodeAttributesUpdated', handler);
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        }, {
+          attributes: ['age']
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    eachEdgeAttributesUpdated: {
+      'it should fire when using #.updateEachEdgeAttributes.': function itShouldFireWhenUsingUpdateEachEdgeAttributes() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.hints, null);
+        });
+        graph.on('eachEdgeAttributesUpdated', handler);
+        graph.updateEachEdgeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.weight + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should provide hints when user gave them.': function itShouldProvideHintsWhenUserGaveThem() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload.hints, {
+            attributes: ['weight']
+          });
+        });
+        graph.on('eachEdgeAttributesUpdated', handler);
+        graph.updateEachEdgeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        }, {
+          attributes: ['weight']
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/helpers.js b/libs/shared/graph-layout/node_modules/graphology/specs/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..5198d01d9110c19b4fa898f1ba162c77030e26e6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/helpers.js
@@ -0,0 +1,101 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.addNodesFrom = addNodesFrom;
+exports.capitalize = capitalize;
+exports.deepMerge = deepMerge;
+exports.sameMembers = sameMembers;
+exports.spy = spy;
+
+function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
+
+/**
+ * Graphology Specs Helpers
+ * =========================
+ *
+ * Miscellaneous helpers to test more easily.
+ */
+
+/**
+ * Capitalize function.
+ */
+function capitalize(string) {
+  return string[0].toUpperCase() + string.slice(1);
+}
+/**
+ * Simplistic deep merger function.
+ */
+
+
+function deepMerge() {
+  for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) {
+    objects[_key] = arguments[_key];
+  }
+
+  var o = objects[0];
+  var t, i, l, k;
+
+  for (i = 1, l = objects.length; i < l; i++) {
+    t = objects[i];
+
+    for (k in t) {
+      if (_typeof(t[k]) === 'object') {
+        o[k] = deepMerge(o[k] || {}, t[k]);
+      } else {
+        o[k] = t[k];
+      }
+    }
+  }
+
+  return o;
+}
+/**
+ * Checking that two arrays have the same members.
+ */
+
+
+function sameMembers(a1, a2) {
+  if (a1.length !== a2.length) return false;
+  var set = new Set(a1);
+
+  for (var i = 0, l = a2.length; i < l; i++) {
+    if (!set.has(a2[i])) return false;
+  }
+
+  return true;
+}
+/**
+ * Function spying on the execution of the provided function to ease some
+ * tests, notably related to event handling.
+ *
+ * @param {function} target - Target function.
+ * @param {function}        - The spy.
+ */
+
+
+function spy(target) {
+  var fn = function fn() {
+    fn.called = true;
+    fn.times++;
+    if (typeof target === 'function') return target.apply(null, arguments);
+  };
+
+  fn.called = false;
+  fn.times = 0;
+  return fn;
+}
+/**
+ * Function adding multiple nodes from an array to the given graph.
+ *
+ * @param {Graph} graph - Target graph.
+ * @param {array} nodes - Node array.
+ */
+
+
+function addNodesFrom(graph, nodes) {
+  nodes.forEach(function (node) {
+    graph.addNode(node);
+  });
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/index.js b/libs/shared/graph-layout/node_modules/graphology/specs/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..18b6595fc9c64727a989d1b977807f7f2d3fc845
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/index.js
@@ -0,0 +1,78 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = specs;
+
+var _instantiation = _interopRequireDefault(require("./instantiation"));
+
+var _properties = _interopRequireDefault(require("./properties"));
+
+var _read = _interopRequireDefault(require("./read"));
+
+var _mutation = _interopRequireDefault(require("./mutation"));
+
+var _attributes = _interopRequireDefault(require("./attributes"));
+
+var _iteration = _interopRequireDefault(require("./iteration"));
+
+var _serialization = _interopRequireDefault(require("./serialization"));
+
+var _events = _interopRequireDefault(require("./events"));
+
+var _utils = _interopRequireDefault(require("./utils"));
+
+var _known = _interopRequireDefault(require("./known"));
+
+var _misc = _interopRequireDefault(require("./misc"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Specs
+ * =================
+ *
+ * Unit tests factory taking the Graph object implementation.
+ */
+var createErrorChecker = function createErrorChecker(name) {
+  return function () {
+    return function (error) {
+      return error && error.name === name;
+    };
+  };
+};
+/**
+ * Returning the unit tests to run.
+ *
+ * @param  {string} path - Path to the implementation (should be absolute).
+ * @return {object}      - The tests to run with Mocha.
+ */
+
+
+function specs(Graph, implementation) {
+  var errors = [['invalid', 'InvalidArgumentsGraphError'], ['notFound', 'NotFoundGraphError'], ['usage', 'UsageGraphError']]; // Building error checkers
+
+  var errorCheckers = {};
+  errors.forEach(function (_ref) {
+    var fn = _ref[0],
+        name = _ref[1];
+    return errorCheckers[fn] = createErrorChecker(name);
+  });
+  var tests = {
+    Basic: {
+      Instantiation: (0, _instantiation["default"])(Graph, implementation, errorCheckers),
+      Properties: (0, _properties["default"])(Graph, errorCheckers),
+      Mutation: (0, _mutation["default"])(Graph, errorCheckers),
+      Read: (0, _read["default"])(Graph, errorCheckers),
+      Attributes: (0, _attributes["default"])(Graph, errorCheckers),
+      Iteration: (0, _iteration["default"])(Graph, errorCheckers),
+      Serialization: (0, _serialization["default"])(Graph, errorCheckers),
+      Events: (0, _events["default"])(Graph),
+      Utils: (0, _utils["default"])(Graph, errorCheckers),
+      'Known Methods': (0, _known["default"])(Graph, errorCheckers),
+      Miscellaneous: (0, _misc["default"])(Graph)
+    }
+  };
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/instantiation.js b/libs/shared/graph-layout/node_modules/graphology/specs/instantiation.js
new file mode 100644
index 0000000000000000000000000000000000000000..45945a419fc28d227c34ec7e3c04a144811f13f0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/instantiation.js
@@ -0,0 +1,211 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = instantiation;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/* eslint no-unused-vars: 0 */
+
+/**
+ * Graphology Instantiation Specs
+ * ===============================
+ *
+ * Testing the instantiation of the graph.
+ */
+var CONSTRUCTORS = ['DirectedGraph', 'UndirectedGraph', 'MultiGraph', 'MultiDirectedGraph', 'MultiUndirectedGraph'];
+var OPTIONS = [{
+  multi: false,
+  type: 'directed'
+}, {
+  multi: false,
+  type: 'undirected'
+}, {
+  multi: true,
+  type: 'mixed'
+}, {
+  multi: true,
+  type: 'directed'
+}, {
+  multi: true,
+  type: 'undirected'
+}];
+
+function instantiation(Graph, implementation, checkers) {
+  var invalid = checkers.invalid;
+  return {
+    'Static #.from method': {
+      'it should be possible to create a Graph from a Graph instance.': function itShouldBePossibleToCreateAGraphFromAGraphInstance() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var other = Graph.from(graph);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), other.nodes());
+
+        _assert["default"].deepStrictEqual(graph.edges(), other.edges());
+      },
+      'it should be possible to create a Graph from a serialized graph': function itShouldBePossibleToCreateAGraphFromASerializedGraph() {
+        var graph = Graph.from({
+          nodes: [{
+            key: 'John'
+          }, {
+            key: 'Thomas'
+          }],
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to provide options.': function itShouldBePossibleToProvideOptions() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          }
+        }, {
+          type: 'directed'
+        });
+
+        _assert["default"].strictEqual(graph.type, 'directed');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      },
+      'it should be possible to take options from the serialized format.': function itShouldBePossibleToTakeOptionsFromTheSerializedFormat() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          },
+          options: {
+            type: 'directed',
+            multi: true
+          }
+        });
+
+        _assert["default"].strictEqual(graph.type, 'directed');
+
+        _assert["default"].strictEqual(graph.multi, true);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      },
+      'given options should take precedence over the serialization ones.': function givenOptionsShouldTakePrecedenceOverTheSerializationOnes() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          },
+          options: {
+            type: 'directed',
+            multi: true
+          }
+        }, {
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(graph.type, 'undirected');
+
+        _assert["default"].strictEqual(graph.multi, true);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      }
+    },
+    Options: {
+      /**
+       * allowSelfLoops
+       */
+      allowSelfLoops: {
+        'providing a non-boolean value should throw.': function providingANonBooleanValueShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              allowSelfLoops: 'test'
+            });
+          }, invalid());
+        }
+      },
+
+      /**
+       * multi
+       */
+      multi: {
+        'providing a non-boolean value should throw.': function providingANonBooleanValueShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              multi: 'test'
+            });
+          }, invalid());
+        }
+      },
+
+      /**
+       * type
+       */
+      type: {
+        'providing an invalid type should throw.': function providingAnInvalidTypeShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              type: 'test'
+            });
+          }, invalid());
+        }
+      }
+    },
+    Constructors: {
+      'all alternative constructors should be available.': function allAlternativeConstructorsShouldBeAvailable() {
+        CONSTRUCTORS.forEach(function (name) {
+          return (0, _assert["default"])(name in implementation);
+        });
+      },
+      'alternative constructors should have the correct options.': function alternativeConstructorsShouldHaveTheCorrectOptions() {
+        CONSTRUCTORS.forEach(function (name, index) {
+          var graph = new implementation[name]();
+          var _OPTIONS$index = OPTIONS[index],
+              multi = _OPTIONS$index.multi,
+              type = _OPTIONS$index.type;
+
+          _assert["default"].strictEqual(graph.multi, multi);
+
+          _assert["default"].strictEqual(graph.type, type);
+        });
+      },
+      'alternative constructors should throw if given inconsistent options.': function alternativeConstructorsShouldThrowIfGivenInconsistentOptions() {
+        CONSTRUCTORS.forEach(function (name, index) {
+          var _OPTIONS$index2 = OPTIONS[index],
+              multi = _OPTIONS$index2.multi,
+              type = _OPTIONS$index2.type;
+
+          _assert["default"]["throws"](function () {
+            var graph = new implementation[name]({
+              multi: !multi
+            });
+          }, invalid());
+
+          if (type === 'mixed') return;
+
+          _assert["default"]["throws"](function () {
+            var graph = new implementation[name]({
+              type: type === 'directed' ? 'undirected' : 'directed'
+            });
+          }, invalid());
+        });
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/iteration/edges.js b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/edges.js
new file mode 100644
index 0000000000000000000000000000000000000000..8210ea7f2bbc2de7231af1f727c18e9ce3ad6623
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/edges.js
@@ -0,0 +1,894 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = edgesIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _take = _interopRequireDefault(require("obliterator/take"));
+
+var _map = _interopRequireDefault(require("obliterator/map"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var METHODS = ['edges', 'inEdges', 'outEdges', 'inboundEdges', 'outboundEdges', 'directedEdges', 'undirectedEdges'];
+
+function edgesIteration(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound;
+  var graph = new Graph({
+    multi: true
+  });
+  (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas', 'Martha', 'Roger', 'Catherine', 'Alone', 'Forever']);
+  graph.replaceNodeAttributes('John', {
+    age: 13
+  });
+  graph.replaceNodeAttributes('Martha', {
+    age: 15
+  });
+  graph.addDirectedEdgeWithKey('J->T', 'John', 'Thomas', {
+    weight: 14
+  });
+  graph.addDirectedEdgeWithKey('J->M', 'John', 'Martha');
+  graph.addDirectedEdgeWithKey('C->J', 'Catherine', 'John');
+  graph.addUndirectedEdgeWithKey('M<->R', 'Martha', 'Roger');
+  graph.addUndirectedEdgeWithKey('M<->J', 'Martha', 'John');
+  graph.addUndirectedEdgeWithKey('J<->R', 'John', 'Roger');
+  graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+  var ALL_EDGES = ['J->T', 'J->M', 'C->J', 'M<->R', 'M<->J', 'J<->R', 'T<->M'];
+  var ALL_DIRECTED_EDGES = ['J->T', 'J->M', 'C->J'];
+  var ALL_UNDIRECTED_EDGES = ['M<->R', 'M<->J', 'J<->R', 'T<->M'];
+  var TEST_DATA = {
+    edges: {
+      all: ALL_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J', 'J->T', 'J->M', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M', 'M<->J']
+      }
+    },
+    inEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: []
+      }
+    },
+    outEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['J->T', 'J->M']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M']
+      }
+    },
+    inboundEdges: {
+      all: ALL_DIRECTED_EDGES.concat(ALL_UNDIRECTED_EDGES),
+      node: {
+        key: 'John',
+        edges: ['C->J', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['M<->J']
+      }
+    },
+    outboundEdges: {
+      all: ALL_DIRECTED_EDGES.concat(ALL_UNDIRECTED_EDGES),
+      node: {
+        key: 'John',
+        edges: ['J->T', 'J->M', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M', 'M<->J']
+      }
+    },
+    directedEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J', 'J->T', 'J->M']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M']
+      }
+    },
+    undirectedEdges: {
+      all: ALL_UNDIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['M<->J']
+      }
+    }
+  };
+
+  function commonTests(name) {
+    return _defineProperty({}, '#.' + name, {
+      'it should throw if too many arguments are provided.': function itShouldThrowIfTooManyArgumentsAreProvided() {
+        _assert["default"]["throws"](function () {
+          graph[name](1, 2, 3);
+        }, invalid());
+      },
+      'it should throw when the node is not found.': function itShouldThrowWhenTheNodeIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test');
+        }, notFound());
+      },
+      'it should throw if either source or target is not found.': function itShouldThrowIfEitherSourceOrTargetIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test', 'Alone');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph[name]('Alone', 'Test');
+        }, notFound());
+      }
+    });
+  }
+
+  function specificTests(name, data) {
+    var _ref2;
+
+    var capitalized = name[0].toUpperCase() + name.slice(1, -1);
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    var forEachName = 'forEach' + capitalized;
+    var findName = 'find' + capitalized;
+    var mapName = 'map' + capitalized + 's';
+    var filterName = 'filter' + capitalized + 's';
+    var reduceName = 'reduce' + capitalized + 's';
+    var someName = 'some' + capitalized;
+    var everyName = 'every' + capitalized;
+    return _ref2 = {}, _defineProperty(_ref2, '#.' + name, {
+      'it should return all the relevant edges.': function itShouldReturnAllTheRelevantEdges() {
+        var edges = graph[name]().sort();
+
+        _assert["default"].deepStrictEqual(edges, data.all.slice().sort());
+      },
+      "it should return a node's relevant edges.": function itShouldReturnANodeSRelevantEdges() {
+        var edges = graph[name](data.node.key);
+
+        _assert["default"].deepStrictEqual(edges, data.node.edges);
+
+        _assert["default"].deepStrictEqual(graph[name]('Alone'), []);
+      },
+      'it should return all the relevant edges between source & target.': function itShouldReturnAllTheRelevantEdgesBetweenSourceTarget() {
+        var edges = graph[name](data.path.source, data.path.target);
+        (0, _assert["default"])((0, _helpers.sameMembers)(edges, data.path.edges));
+
+        _assert["default"].deepStrictEqual(graph[name]('Forever', 'Alone'), []);
+      }
+    }), _defineProperty(_ref2, '#.' + forEachName, {
+      'it should possible to use callback iterators.': function itShouldPossibleToUseCallbackIterators() {
+        var edges = [];
+        graph[forEachName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        edges.sort();
+
+        _assert["default"].deepStrictEqual(edges, data.all.slice().sort());
+      },
+      "it should be possible to use callback iterators over a node's relevant edges.": function itShouldBePossibleToUseCallbackIteratorsOverANodeSRelevantEdges() {
+        var edges = [];
+        graph[forEachName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        edges.sort();
+
+        _assert["default"].deepStrictEqual(edges, data.node.edges.slice().sort());
+      },
+      'it should be possible to use callback iterators over all the relevant edges between source & target.': function itShouldBePossibleToUseCallbackIteratorsOverAllTheRelevantEdgesBetweenSourceTarget() {
+        var edges = [];
+        graph[forEachName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        (0, _assert["default"])((0, _helpers.sameMembers)(edges, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + mapName, {
+      'it should possible to map edges.': function itShouldPossibleToMapEdges() {
+        var result = graph[mapName](function (key) {
+          return key;
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.all.slice().sort());
+      },
+      "it should be possible to map a node's relevant edges.": function itShouldBePossibleToMapANodeSRelevantEdges() {
+        var result = graph[mapName](data.node.key, function (key) {
+          return key;
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.node.edges.slice().sort());
+      },
+      'it should be possible to map the relevant edges between source & target.': function itShouldBePossibleToMapTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[mapName](data.path.source, data.path.target, function (key) {
+          return key;
+        });
+        result.sort();
+        (0, _assert["default"])((0, _helpers.sameMembers)(result, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + filterName, {
+      'it should possible to filter edges.': function itShouldPossibleToFilterEdges() {
+        var result = graph[filterName](function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.all.slice().sort());
+      },
+      "it should be possible to filter a node's relevant edges.": function itShouldBePossibleToFilterANodeSRelevantEdges() {
+        var result = graph[filterName](data.node.key, function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.node.edges.slice().sort());
+      },
+      'it should be possible to filter the relevant edges between source & target.': function itShouldBePossibleToFilterTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[filterName](data.path.source, data.path.target, function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+        (0, _assert["default"])((0, _helpers.sameMembers)(result, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + reduceName, {
+      'it should throw when given bad arguments.': function itShouldThrowWhenGivenBadArguments() {
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName](1, 2, 3, 4, 5);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('notafunction', 0);
+        }, TypeError);
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('test', function () {
+            return true;
+          });
+        }, invalid());
+      },
+      'it should possible to reduce edges.': function itShouldPossibleToReduceEdges() {
+        var result = graph[reduceName](function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.all.length);
+      },
+      "it should be possible to reduce a node's relevant edges.": function itShouldBePossibleToReduceANodeSRelevantEdges() {
+        var result = graph[reduceName](data.node.key, function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.node.edges.length);
+      },
+      'it should be possible to reduce the relevant edges between source & target.': function itShouldBePossibleToReduceTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[reduceName](data.path.source, data.path.target, function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.path.edges.length);
+      }
+    }), _defineProperty(_ref2, '#.' + findName, {
+      'it should possible to find an edge.': function itShouldPossibleToFindAnEdge() {
+        var edges = [];
+        var found = graph[findName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[findName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      },
+      "it should be possible to find a node's edge.": function itShouldBePossibleToFindANodeSEdge() {
+        var edges = [];
+        var found = graph[findName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[findName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      },
+      'it should be possible to find an edge between source & target.': function itShouldBePossibleToFindAnEdgeBetweenSourceTarget() {
+        var edges = [];
+        var found = graph[findName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, graph[name](data.path.source, data.path.target).length ? 1 : 0);
+
+        found = graph[findName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    }), _defineProperty(_ref2, '#.' + someName, {
+      'it should possible to assert whether any edge matches a predicate.': function itShouldPossibleToAssertWhetherAnyEdgeMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[someName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      "it should possible to assert whether any node's edge matches a predicate.": function itShouldPossibleToAssertWhetherAnyNodeSEdgeMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[someName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should possible to assert whether any edge between source & target matches a predicate.': function itShouldPossibleToAssertWhetherAnyEdgeBetweenSourceTargetMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, graph[name](data.path.source, data.path.target).length !== 0);
+
+        _assert["default"].strictEqual(edges.length, graph[name](data.path.source, data.path.target).length ? 1 : 0);
+
+        found = graph[someName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should always return false on empty sets.': function itShouldAlwaysReturnFalseOnEmptySets() {
+        var empty = new Graph();
+
+        _assert["default"].strictEqual(empty[someName](function () {
+          return true;
+        }), false);
+      }
+    }), _defineProperty(_ref2, '#.' + everyName, {
+      'it should possible to assert whether all edges matches a predicate.': function itShouldPossibleToAssertWhetherAllEdgesMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      "it should possible to assert whether all of a node's edges matches a predicate.": function itShouldPossibleToAssertWhetherAllOfANodeSEdgesMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should possible to assert whether all edges between source & target matches a predicate.': function itShouldPossibleToAssertWhetherAllEdgesBetweenSourceTargetMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+        var isEmpty = graph[name](data.path.source, data.path.target).length === 0;
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, isEmpty ? true : false);
+      },
+      'it should always return true on empty sets.': function itShouldAlwaysReturnTrueOnEmptySets() {
+        var empty = new Graph();
+
+        _assert["default"].strictEqual(empty[everyName](function () {
+          return true;
+        }), true);
+      }
+    }), _defineProperty(_ref2, '#.' + iteratorName, {
+      'it should be possible to return an iterator over the relevant edges.': function itShouldBePossibleToReturnAnIteratorOverTheRelevantEdges() {
+        var iterator = graph[iteratorName]();
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.all.map(function (edge) {
+          var _graph$extremities = graph.extremities(edge),
+              source = _graph$extremities[0],
+              target = _graph$extremities[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      },
+      "it should be possible to return an iterator over a node's relevant edges.": function itShouldBePossibleToReturnAnIteratorOverANodeSRelevantEdges() {
+        var iterator = graph[iteratorName](data.node.key);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.node.edges.map(function (edge) {
+          var _graph$extremities2 = graph.extremities(edge),
+              source = _graph$extremities2[0],
+              target = _graph$extremities2[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      },
+      'it should be possible to return an iterator over relevant edges between source & target.': function itShouldBePossibleToReturnAnIteratorOverRelevantEdgesBetweenSourceTarget() {
+        var iterator = graph[iteratorName](data.path.source, data.path.target);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.path.edges.map(function (edge) {
+          var _graph$extremities3 = graph.extremities(edge),
+              source = _graph$extremities3[0],
+              target = _graph$extremities3[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      }
+    }), _ref2;
+  }
+
+  var tests = {
+    Miscellaneous: {
+      'simple graph indices should work.': function simpleGraphIndicesShouldWork() {
+        var simpleGraph = new Graph();
+        (0, _helpers.addNodesFrom)(simpleGraph, [1, 2, 3, 4]);
+        simpleGraph.addEdgeWithKey('1->2', 1, 2);
+        simpleGraph.addEdgeWithKey('1->3', 1, 3);
+        simpleGraph.addEdgeWithKey('1->4', 1, 4);
+
+        _assert["default"].deepStrictEqual(simpleGraph.edges(1), ['1->2', '1->3', '1->4']);
+      },
+      'it should also work with typed graphs.': function itShouldAlsoWorkWithTypedGraphs() {
+        var undirected = new Graph({
+          type: 'undirected'
+        }),
+            directed = new Graph({
+          type: 'directed'
+        });
+        undirected.mergeEdgeWithKey('1--2', 1, 2);
+        directed.mergeEdgeWithKey('1->2', 1, 2);
+
+        _assert["default"].deepStrictEqual(undirected.edges(1, 2), ['1--2']);
+
+        _assert["default"].deepStrictEqual(directed.edges(1, 2), ['1->2']);
+      },
+      'self loops should appear when using #.inEdges and should appear only once with #.edges.': function selfLoopsShouldAppearWhenUsingInEdgesAndShouldAppearOnlyOnceWithEdges() {
+        var directed = new Graph({
+          type: 'directed'
+        });
+        directed.addNode('Lucy');
+        directed.addEdgeWithKey('Lucy', 'Lucy', 'Lucy');
+
+        _assert["default"].deepStrictEqual(directed.inEdges('Lucy'), ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.inEdgeEntries('Lucy')).map(function (x) {
+          return x.edge;
+        }), ['Lucy']);
+
+        var edges = [];
+        directed.forEachInEdge('Lucy', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(directed.edges('Lucy'), ['Lucy']);
+
+        edges = [];
+        directed.forEachEdge('Lucy', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.edgeEntries('Lucy')).map(function (x) {
+          return x.edge;
+        }), ['Lucy']);
+      },
+      'it should be possible to retrieve self loops.': function itShouldBePossibleToRetrieveSelfLoops() {
+        var loopy = new Graph();
+        loopy.addNode('John');
+        loopy.addEdgeWithKey('d', 'John', 'John');
+        loopy.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].deepStrictEqual(new Set(loopy.edges('John', 'John')), new Set(['d', 'u']));
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('John', 'John'), ['d']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('John', 'John'), ['u']);
+
+        var edges = [];
+        loopy.forEachDirectedEdge('John', 'John', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['d']);
+
+        edges = [];
+        loopy.forEachUndirectedEdge('John', 'John', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['u']);
+      },
+      'self loops in multi graphs should work properly (#352).': function selfLoopsInMultiGraphsShouldWorkProperly352() {
+        var loopy = new Graph({
+          multi: true
+        });
+        loopy.addNode('n');
+        loopy.addEdgeWithKey('e1', 'n', 'n');
+        loopy.addEdgeWithKey('e2', 'n', 'n');
+        loopy.addUndirectedEdgeWithKey('e3', 'n', 'n'); // Arrays
+
+        _assert["default"].deepStrictEqual(loopy.edges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outboundEdges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.inboundEdges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.inEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('n'), ['e3']);
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.edges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outboundEdges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.inboundEdges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outEdges('n', 'n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.inEdges('n', 'n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('n', 'n'), ['e3']);
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('n', 'n'), ['e2', 'e1']); // Iterators
+
+
+        var mapKeys = function mapKeys(it) {
+          return (0, _take["default"])((0, _map["default"])(it, function (e) {
+            return e.edge;
+          }));
+        };
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.edgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outboundEdgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inboundEdgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.undirectedEdgeEntries('n')), ['e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.directedEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.edgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outboundEdgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inboundEdgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outEdgeEntries('n', 'n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inEdgeEntries('n', 'n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.undirectedEdgeEntries('n', 'n')), ['e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.directedEdgeEntries('n', 'n')), ['e2', 'e1']);
+      },
+      'findOutboundEdge should work on multigraphs (#319).': function findOutboundEdgeShouldWorkOnMultigraphs319() {
+        var loopy = new Graph({
+          multi: true
+        });
+        loopy.mergeEdgeWithKey('e1', 'n', 'm');
+        loopy.mergeEdgeWithKey('e2', 'n', 'n');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge(function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge('n', function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge('n', 'n', function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+      }
+    }
+  }; // Common tests
+
+  METHODS.forEach(function (name) {
+    return (0, _helpers.deepMerge)(tests, commonTests(name));
+  }); // Specific tests
+
+  for (var name in TEST_DATA) {
+    (0, _helpers.deepMerge)(tests, specificTests(name, TEST_DATA[name]));
+  }
+
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/iteration/index.js b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf2e892c5affbf78127c05c101c06732b023b6c0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/index.js
@@ -0,0 +1,161 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = iteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _nodes = _interopRequireDefault(require("./nodes"));
+
+var _edges = _interopRequireDefault(require("./edges"));
+
+var _neighbors = _interopRequireDefault(require("./neighbors"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Iteration Specs
+ * ===========================
+ *
+ * Testing the iteration-related methods of the graph.
+ */
+function iteration(Graph, checkers) {
+  return {
+    Adjacency: {
+      '#.forEachAdjacencyEntry': {
+        'it should iterate over the relevant elements.': function itShouldIterateOverTheRelevantElements() {
+          function test(multi) {
+            var graph = new Graph({
+              multi: multi
+            });
+            graph.addNode('John', {
+              hello: 'world'
+            });
+
+            var _graph$mergeUndirecte = graph.mergeUndirectedEdge('John', 'Mary', {
+              weight: 3
+            }),
+                e1 = _graph$mergeUndirecte[0];
+
+            graph.mergeUndirectedEdge('Thomas', 'John');
+            graph.mergeDirectedEdge('John', 'Thomas');
+            var count = 0;
+            graph.forEachAdjacencyEntry(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+
+              if (node === 'John') {
+                _assert["default"].deepStrictEqual(attr, {
+                  hello: 'world'
+                });
+              } else {
+                _assert["default"].deepStrictEqual(attr, {});
+              }
+
+              if (neighbor === 'John') {
+                _assert["default"].deepStrictEqual(neighborAttr, {
+                  hello: 'world'
+                });
+              } else {
+                _assert["default"].deepStrictEqual(neighborAttr, {});
+              }
+
+              if (edge === e1) {
+                _assert["default"].deepStrictEqual(edgeAttr, {
+                  weight: 3
+                });
+              } else {
+                _assert["default"].deepStrictEqual(edgeAttr, {});
+              }
+
+              _assert["default"].strictEqual(graph.isUndirected(edge), undirected);
+            });
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize * 2);
+
+            graph.addNode('Disconnected');
+            count = 0;
+            graph.forEachAdjacencyEntryWithOrphans(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+              if (node !== 'Disconnected') return;
+
+              _assert["default"].strictEqual(neighbor, null);
+
+              _assert["default"].strictEqual(neighborAttr, null);
+
+              _assert["default"].strictEqual(edge, null);
+
+              _assert["default"].strictEqual(edgeAttr, null);
+
+              _assert["default"].strictEqual(undirected, null);
+            }, true);
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize * 2 + 1);
+          }
+
+          test(false);
+          test(true);
+        }
+      },
+      '#.forEachAssymetricAdjacencyEntry': {
+        'it should iterate over the relevant elements.': function itShouldIterateOverTheRelevantElements() {
+          function test(multi) {
+            var graph = new Graph({
+              multi: multi
+            });
+            graph.addNode('John', {
+              hello: 'world'
+            });
+            graph.mergeUndirectedEdge('John', 'Mary', {
+              weight: 3
+            });
+            graph.mergeUndirectedEdge('Thomas', 'John');
+            graph.mergeDirectedEdge('John', 'Thomas');
+            var edges = [];
+            graph.forEachAssymetricAdjacencyEntry(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              if (undirected) {
+                _assert["default"].strictEqual(node < neighbor, true);
+              }
+
+              edges.push(edge);
+            });
+
+            _assert["default"].strictEqual(edges.length, graph.directedSize + graph.undirectedSize);
+
+            _assert["default"].deepStrictEqual(new Set(edges).size, edges.length);
+
+            graph.addNode('Disconnected');
+            var count = 0;
+            var nulls = 0;
+            graph.forEachAssymetricAdjacencyEntryWithOrphans(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+              if (neighbor) return;
+              nulls++;
+
+              _assert["default"].strictEqual(neighbor, null);
+
+              _assert["default"].strictEqual(neighborAttr, null);
+
+              _assert["default"].strictEqual(edge, null);
+
+              _assert["default"].strictEqual(edgeAttr, null);
+
+              _assert["default"].strictEqual(undirected, null);
+            }, true);
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize + 3);
+
+            _assert["default"].strictEqual(nulls, 3);
+          }
+
+          test(false);
+          test(true);
+        }
+      }
+    },
+    Nodes: (0, _nodes["default"])(Graph, checkers),
+    Edges: (0, _edges["default"])(Graph, checkers),
+    Neighbors: (0, _neighbors["default"])(Graph, checkers)
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/iteration/neighbors.js b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/neighbors.js
new file mode 100644
index 0000000000000000000000000000000000000000..7948bd77c1e018dc351142e1f8dfc014d04bbdc6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/neighbors.js
@@ -0,0 +1,284 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = neighborsIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _take = _interopRequireDefault(require("obliterator/take"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var METHODS = ['neighbors', 'inNeighbors', 'outNeighbors', 'inboundNeighbors', 'outboundNeighbors', 'directedNeighbors', 'undirectedNeighbors'];
+
+function neighborsIteration(Graph, checkers) {
+  var notFound = checkers.notFound,
+      invalid = checkers.invalid;
+  var graph = new Graph({
+    multi: true
+  });
+  (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas', 'Martha', 'Roger', 'Catherine', 'Alone', 'Forever']);
+  graph.replaceNodeAttributes('John', {
+    age: 34
+  });
+  graph.replaceNodeAttributes('Martha', {
+    age: 35
+  });
+  graph.addDirectedEdgeWithKey('J->T', 'John', 'Thomas');
+  graph.addDirectedEdgeWithKey('J->M', 'John', 'Martha');
+  graph.addDirectedEdgeWithKey('C->J', 'Catherine', 'John');
+  graph.addUndirectedEdgeWithKey('M<->R', 'Martha', 'Roger');
+  graph.addUndirectedEdgeWithKey('M<->J', 'Martha', 'John');
+  graph.addUndirectedEdgeWithKey('J<->R', 'John', 'Roger');
+  graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+  var TEST_DATA = {
+    neighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Thomas', 'Martha', 'Roger']
+      }
+    },
+    inNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine']
+      }
+    },
+    outNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Thomas', 'Martha']
+      }
+    },
+    inboundNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Martha', 'Roger']
+      }
+    },
+    outboundNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Thomas', 'Martha', 'Roger']
+      }
+    },
+    directedNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Thomas', 'Martha']
+      }
+    },
+    undirectedNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Martha', 'Roger']
+      }
+    }
+  };
+
+  function commonTests(name) {
+    return _defineProperty({}, '#.' + name, {
+      'it should throw when the node is not found.': function itShouldThrowWhenTheNodeIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test');
+        }, notFound());
+
+        if (~name.indexOf('count')) return;
+
+        _assert["default"]["throws"](function () {
+          graph[name]('Test', 'SecondTest');
+        }, notFound());
+      }
+    });
+  }
+
+  function specificTests(name, data) {
+    var _ref2;
+
+    var capitalized = name[0].toUpperCase() + name.slice(1, -1);
+    var forEachName = 'forEach' + capitalized;
+    var findName = 'find' + capitalized;
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    var areName = 'are' + capitalized + 's';
+    var mapName = 'map' + capitalized + 's';
+    var filterName = 'filter' + capitalized + 's';
+    var reduceName = 'reduce' + capitalized + 's';
+    var someName = 'some' + capitalized;
+    var everyName = 'every' + capitalized;
+    return _ref2 = {}, _defineProperty(_ref2, '#.' + name, {
+      'it should return the correct neighbors array.': function itShouldReturnTheCorrectNeighborsArray() {
+        var neighbors = graph[name](data.node.key);
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors);
+
+        _assert["default"].deepStrictEqual(graph[name]('Alone'), []);
+      }
+    }), _defineProperty(_ref2, '#.' + forEachName, {
+      'it should be possible to iterate over neighbors using a callback.': function itShouldBePossibleToIterateOverNeighborsUsingACallback() {
+        var neighbors = [];
+        graph[forEachName](data.node.key, function (target, attrs) {
+          neighbors.push(target);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), attrs);
+
+          _assert["default"].strictEqual(graph[areName](data.node.key, target), true);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + mapName, {
+      'it should be possible to map neighbors using a callback.': function itShouldBePossibleToMapNeighborsUsingACallback() {
+        var result = graph[mapName](data.node.key, function (target) {
+          return target;
+        });
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + filterName, {
+      'it should be possible to filter neighbors using a callback.': function itShouldBePossibleToFilterNeighborsUsingACallback() {
+        var result = graph[filterName](data.node.key, function () {
+          return true;
+        });
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+
+        result = graph[filterName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].deepStrictEqual(result, []);
+      }
+    }), _defineProperty(_ref2, '#.' + reduceName, {
+      'it sould throw if not given an initial value.': function itSouldThrowIfNotGivenAnInitialValue() {
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('node', function () {
+            return true;
+          });
+        }, invalid());
+      },
+      'it should be possible to reduce neighbors using a callback.': function itShouldBePossibleToReduceNeighborsUsingACallback() {
+        var result = graph[reduceName](data.node.key, function (acc, key) {
+          return acc.concat(key);
+        }, []);
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + findName, {
+      'it should be possible to find neighbors.': function itShouldBePossibleToFindNeighbors() {
+        var neighbors = [];
+        var found = graph[findName](data.node.key, function (target, attrs) {
+          neighbors.push(target);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), attrs);
+
+          _assert["default"].strictEqual(graph[areName](data.node.key, target), true);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, neighbors[0]);
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors.slice(0, 1));
+
+        found = graph[findName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    }), _defineProperty(_ref2, '#.' + someName, {
+      'it should always return false on empty set.': function itShouldAlwaysReturnFalseOnEmptySet() {
+        var loneGraph = new Graph();
+        loneGraph.addNode('alone');
+
+        _assert["default"].strictEqual(loneGraph[someName]('alone', function () {
+          return true;
+        }), false);
+      },
+      'it should be possible to assert whether any neighbor matches a predicate.': function itShouldBePossibleToAssertWhetherAnyNeighborMatchesAPredicate() {
+        _assert["default"].strictEqual(graph[someName](data.node.key, function () {
+          return true;
+        }), data.node.neighbors.length > 0);
+      }
+    }), _defineProperty(_ref2, '#.' + everyName, {
+      'it should always return true on empty set.': function itShouldAlwaysReturnTrueOnEmptySet() {
+        var loneGraph = new Graph();
+        loneGraph.addNode('alone');
+
+        _assert["default"].strictEqual(loneGraph[everyName]('alone', function () {
+          return true;
+        }), true);
+      },
+      'it should be possible to assert whether any neighbor matches a predicate.': function itShouldBePossibleToAssertWhetherAnyNeighborMatchesAPredicate() {
+        _assert["default"].strictEqual(graph[everyName](data.node.key, function () {
+          return true;
+        }), data.node.neighbors.length > 0);
+      }
+    }), _defineProperty(_ref2, '#.' + iteratorName, {
+      'it should be possible to create an iterator over neighbors.': function itShouldBePossibleToCreateAnIteratorOverNeighbors() {
+        var iterator = graph[iteratorName](data.node.key);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.node.neighbors.map(function (neighbor) {
+          return {
+            neighbor: neighbor,
+            attributes: graph.getNodeAttributes(neighbor)
+          };
+        }));
+      }
+    }), _ref2;
+  }
+
+  var tests = {
+    Miscellaneous: {
+      'self loops should appear when using #.inNeighbors and should appear only once with #.neighbors.': function selfLoopsShouldAppearWhenUsingInNeighborsAndShouldAppearOnlyOnceWithNeighbors() {
+        var directed = new Graph({
+          type: 'directed'
+        });
+        directed.addNode('Lucy');
+        directed.addEdgeWithKey('test', 'Lucy', 'Lucy');
+
+        _assert["default"].deepStrictEqual(directed.inNeighbors('Lucy'), ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.inNeighborEntries('Lucy')).map(function (x) {
+          return x.neighbor;
+        }), ['Lucy']);
+
+        var neighbors = [];
+        directed.forEachInNeighbor('Lucy', function (neighbor) {
+          neighbors.push(neighbor);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(directed.neighbors('Lucy'), ['Lucy']);
+
+        neighbors = [];
+        directed.forEachNeighbor('Lucy', function (neighbor) {
+          neighbors.push(neighbor);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.neighborEntries('Lucy')).map(function (x) {
+          return x.neighbor;
+        }), ['Lucy']);
+      }
+    }
+  }; // Common tests
+
+  METHODS.forEach(function (name) {
+    return (0, _helpers.deepMerge)(tests, commonTests(name));
+  }); // Specific tests
+
+  for (var name in TEST_DATA) {
+    (0, _helpers.deepMerge)(tests, specificTests(name, TEST_DATA[name]));
+  }
+
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/iteration/nodes.js b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/nodes.js
new file mode 100644
index 0000000000000000000000000000000000000000..33298820d5db1d8504c4be075824ae5b51644fef
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/iteration/nodes.js
@@ -0,0 +1,248 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = nodesIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Nodes Iteration Specs
+ * =================================
+ *
+ * Testing the nodes iteration-related methods of the graph.
+ */
+function nodesIteration(Graph, checkers) {
+  var invalid = checkers.invalid;
+  return {
+    '#.nodes': {
+      'it should return the list of nodes of the graph.': function itShouldReturnTheListOfNodesOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['one', 'two', 'three']);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['one', 'two', 'three']);
+      }
+    },
+    '#.forEachNode': {
+      'it should throw if given callback is not a function.': function itShouldThrowIfGivenCallbackIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.forEachNode(null);
+        }, invalid());
+      },
+      'it should be possible to iterate over nodes and their attributes.': function itShouldBePossibleToIterateOverNodesAndTheirAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Martha', {
+          age: 33
+        });
+        var count = 0;
+        graph.forEachNode(function (key, attributes) {
+          _assert["default"].strictEqual(key, count ? 'Martha' : 'John');
+
+          _assert["default"].deepStrictEqual(attributes, count ? {
+            age: 33
+          } : {
+            age: 34
+          });
+
+          count++;
+        });
+
+        _assert["default"].strictEqual(count, 2);
+      }
+    },
+    '#.findNode': {
+      'it should throw if given callback is not a function.': function itShouldThrowIfGivenCallbackIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.findNode(null);
+        }, invalid());
+      },
+      'it should be possible to find a node in the graph.': function itShouldBePossibleToFindANodeInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Martha', {
+          age: 33
+        });
+        var count = 0;
+        var found = graph.findNode(function (key, attributes) {
+          _assert["default"].strictEqual(key, 'John');
+
+          _assert["default"].deepStrictEqual(attributes, {
+            age: 34
+          });
+
+          count++;
+          if (key === 'John') return true;
+        });
+
+        _assert["default"].strictEqual(found, 'John');
+
+        _assert["default"].strictEqual(count, 1);
+
+        found = graph.findNode(function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    },
+    '#.mapNodes': {
+      'it should be possible to map nodes.': function itShouldBePossibleToMapNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        var result = graph.mapNodes(function (node, attr) {
+          return attr.weight * 2;
+        });
+
+        _assert["default"].deepStrictEqual(result, [4, 6]);
+      }
+    },
+    '#.someNode': {
+      'it should always return false on empty sets.': function itShouldAlwaysReturnFalseOnEmptySets() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.someNode(function () {
+          return true;
+        }), false);
+      },
+      'it should be possible to find if some node matches a predicate.': function itShouldBePossibleToFindIfSomeNodeMatchesAPredicate() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.someNode(function (node, attr) {
+          return attr.weight > 6;
+        }), false);
+
+        _assert["default"].strictEqual(graph.someNode(function (node, attr) {
+          return attr.weight > 2;
+        }), true);
+      }
+    },
+    '#.everyNode': {
+      'it should always return true on empty sets.': function itShouldAlwaysReturnTrueOnEmptySets() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.everyNode(function () {
+          return true;
+        }), true);
+      },
+      'it should be possible to find if all node matches a predicate.': function itShouldBePossibleToFindIfAllNodeMatchesAPredicate() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.everyNode(function (node, attr) {
+          return attr.weight > 2;
+        }), false);
+
+        _assert["default"].strictEqual(graph.everyNode(function (node, attr) {
+          return attr.weight > 1;
+        }), true);
+      }
+    },
+    '#.filterNodes': {
+      'it should be possible to filter nodes.': function itShouldBePossibleToFilterNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        graph.addNode('three', {
+          weight: 4
+        });
+        var result = graph.filterNodes(function (node, _ref) {
+          var weight = _ref.weight;
+          return weight >= 3;
+        });
+
+        _assert["default"].deepStrictEqual(result, ['two', 'three']);
+      }
+    },
+    '#.reduceNodes': {
+      'it should throw if initial value is not given.': function itShouldThrowIfInitialValueIsNotGiven() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.reduceNodes(function (x, _, attr) {
+            return x + attr.weight;
+          });
+        }, invalid());
+      },
+      'it should be possible to reduce nodes.': function itShouldBePossibleToReduceNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        graph.addNode('three', {
+          weight: 4
+        });
+        var result = graph.reduceNodes(function (x, _, attr) {
+          return x + attr.weight;
+        }, 0);
+
+        _assert["default"].strictEqual(result, 9);
+      }
+    },
+    '#.nodeEntries': {
+      'it should be possible to create a nodes iterator.': function itShouldBePossibleToCreateANodesIterator() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['one', 'two', 'three']);
+        graph.replaceNodeAttributes('two', {
+          hello: 'world'
+        });
+        var iterator = graph.nodeEntries();
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'one',
+          attributes: {}
+        });
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'two',
+          attributes: {
+            hello: 'world'
+          }
+        });
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'three',
+          attributes: {}
+        });
+
+        _assert["default"].strictEqual(iterator.next().done, true);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/known.js b/libs/shared/graph-layout/node_modules/graphology/specs/known.js
new file mode 100644
index 0000000000000000000000000000000000000000..040ffb036d23726a0a45e93be2bada79bd7d2e28
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/known.js
@@ -0,0 +1,50 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = knownMethods;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Known Methods Specs
+ * ===============================
+ *
+ * Testing the known methods of the graph.
+ */
+function knownMethods(Graph) {
+  return {
+    '#.toJSON': {
+      'it should return the serialized graph.': function itShouldReturnTheSerializedGraph() {
+        var graph = new Graph({
+          multi: true
+        });
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Jack', 'Martha']);
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.addEdgeWithKey('J->J•1', 'John', 'Jack');
+        graph.addEdgeWithKey('J->J•2', 'John', 'Jack', {
+          weight: 2
+        });
+        graph.addEdgeWithKey('J->J•3', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•1', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•2', 'John', 'Jack', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.toJSON(), graph["export"]());
+      }
+    },
+    '#.toString': {
+      'it should return "[object Graph]".': function itShouldReturnObjectGraph() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.toString(), '[object Graph]');
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/misc.js b/libs/shared/graph-layout/node_modules/graphology/specs/misc.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0044b1a2f68ce6013b81d0aa74ee36af5e36b21
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/misc.js
@@ -0,0 +1,112 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = misc;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Misc Specs
+ * ======================
+ *
+ * Testing the miscellaneous things about the graph.
+ */
+function misc(Graph) {
+  return {
+    Structure: {
+      'a simple mixed graph can have A->B, B->A & A<->B': function aSimpleMixedGraphCanHaveABBAAB() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Audrey', 'Benjamin']);
+
+        _assert["default"].doesNotThrow(function () {
+          graph.addEdge('Audrey', 'Benjamin');
+          graph.addEdge('Benjamin', 'Audrey');
+          graph.addUndirectedEdge('Benjamin', 'Audrey');
+        });
+      },
+      'deleting the last edge between A & B should correctly clear neighbor index.': function deletingTheLastEdgeBetweenABShouldCorrectlyClearNeighborIndex() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.addNode('A');
+        graph.addNode('B');
+        graph.addEdge('A', 'B');
+        graph.addEdge('A', 'B');
+        graph.forEachEdge('A', function (edge) {
+          return graph.dropEdge(edge);
+        });
+
+        _assert["default"].deepStrictEqual(graph.neighbors('A'), []);
+
+        _assert["default"].deepStrictEqual(graph.neighbors('B'), []);
+      },
+      'exhaustive deletion use-cases should not break doubly-linked lists implementation of multigraph edge storage.': function exhaustiveDeletionUseCasesShouldNotBreakDoublyLinkedListsImplementationOfMultigraphEdgeStorage() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.mergeEdgeWithKey('1', 'A', 'B');
+        graph.mergeEdgeWithKey('2', 'A', 'B');
+        graph.mergeEdgeWithKey('3', 'A', 'B');
+        graph.mergeEdgeWithKey('4', 'A', 'B');
+        graph.dropEdge('1');
+        graph.dropEdge('2');
+        graph.dropEdge('3');
+        graph.dropEdge('4');
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), false);
+
+        graph.mergeEdgeWithKey('1', 'A', 'B');
+        graph.mergeEdgeWithKey('2', 'A', 'B');
+        graph.mergeEdgeWithKey('3', 'A', 'B');
+        graph.mergeEdgeWithKey('4', 'A', 'B');
+
+        _assert["default"].strictEqual(graph.size, 4);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), true);
+
+        graph.dropEdge('2');
+        graph.dropEdge('3');
+
+        _assert["default"].strictEqual(graph.size, 2);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), true);
+
+        graph.dropEdge('4');
+        graph.dropEdge('1');
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), false);
+      }
+    },
+    'Key coercion': {
+      'keys should be correctly coerced to strings.': function keysShouldBeCorrectlyCoercedToStrings() {
+        var graph = new Graph();
+        graph.addNode(1);
+        graph.addNode('2');
+
+        _assert["default"].strictEqual(graph.hasNode(1), true);
+
+        _assert["default"].strictEqual(graph.hasNode('1'), true);
+
+        _assert["default"].strictEqual(graph.hasNode(2), true);
+
+        _assert["default"].strictEqual(graph.hasNode('2'), true);
+
+        graph.addEdgeWithKey(3, 1, 2);
+
+        _assert["default"].strictEqual(graph.hasEdge(3), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('3'), true);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/mutation.js b/libs/shared/graph-layout/node_modules/graphology/specs/mutation.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6a114c1b30abe04ba95646608ce0a06a5ee42a7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/mutation.js
@@ -0,0 +1,883 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = mutation;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function mutation(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+  return {
+    '#.addNode': {
+      'it should throw if given attributes is not an object.': function itShouldThrowIfGivenAttributesIsNotAnObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.addNode('test', true);
+        }, invalid());
+      },
+      'it should throw if the given node already exist.': function itShouldThrowIfTheGivenNodeAlreadyExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addNode('Martha');
+        }, usage());
+      },
+      'it should return the added node.': function itShouldReturnTheAddedNode() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.addNode('John'), 'John');
+      }
+    },
+    '#.mergeNode': {
+      'it should add the node if it does not exist yet.': function itShouldAddTheNodeIfItDoesNotExistYet() {
+        var graph = new Graph();
+        graph.mergeNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should do nothing if the node already exists.': function itShouldDoNothingIfTheNodeAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.mergeNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should merge the attributes.': function itShouldMergeTheAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          eyes: 'blue'
+        });
+        graph.mergeNode('John', {
+          age: 15
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          eyes: 'blue',
+          age: 15
+        });
+      },
+      'it should coerce keys to string.': function itShouldCoerceKeysToString() {
+        var graph = new Graph();
+        graph.addNode(4);
+
+        _assert["default"].doesNotThrow(function () {
+          return graph.mergeNode(4);
+        });
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+
+        var _graph$mergeNode = graph.mergeNode('Jack'),
+            key = _graph$mergeNode[0],
+            wasAdded = _graph$mergeNode[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, true);
+
+        var _graph$mergeNode2 = graph.mergeNode('Jack');
+
+        key = _graph$mergeNode2[0];
+        wasAdded = _graph$mergeNode2[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, false);
+      }
+    },
+    '#.updateNode': {
+      'it should add the node if it does not exist yet.': function itShouldAddTheNodeIfItDoesNotExistYet() {
+        var graph = new Graph();
+        graph.updateNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should do nothing if the node already exists.': function itShouldDoNothingIfTheNodeAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.updateNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should update the attributes.': function itShouldUpdateTheAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          eyes: 'blue',
+          count: 1
+        });
+        graph.updateNode('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            count: attr.count + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          eyes: 'blue',
+          count: 2
+        });
+      },
+      'it should be possible to start from blank attributes.': function itShouldBePossibleToStartFromBlankAttributes() {
+        var graph = new Graph();
+        graph.updateNode('John', function () {
+          return {
+            count: 2
+          };
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          count: 2
+        });
+      },
+      'it should coerce keys to string.': function itShouldCoerceKeysToString() {
+        var graph = new Graph();
+        graph.addNode(4);
+
+        _assert["default"].doesNotThrow(function () {
+          return graph.updateNode(4);
+        });
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+
+        var _graph$updateNode = graph.updateNode('Jack'),
+            key = _graph$updateNode[0],
+            wasAdded = _graph$updateNode[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, true);
+
+        var _graph$updateNode2 = graph.updateNode('Jack');
+
+        key = _graph$updateNode2[0];
+        wasAdded = _graph$updateNode2[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, false);
+      }
+    },
+    '#.addDirectedEdge': {
+      'it should throw if given attributes is not an object.': function itShouldThrowIfGivenAttributesIsNotAnObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('source', 'target', true);
+        }, invalid());
+      },
+      'it should throw if the graph is undirected.': function itShouldThrowIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('source', 'target');
+        }, usage());
+      },
+      'it should throw if either the source or the target does not exist.': function itShouldThrowIfEitherTheSourceOrTheTargetDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Eric');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Martha', 'Eric');
+        }, notFound());
+      },
+      'it should throw if the edge is a loop and the graph does not allow it.': function itShouldThrowIfTheEdgeIsALoopAndTheGraphDoesNotAllowIt() {
+        var graph = new Graph({
+          allowSelfLoops: false
+        });
+        graph.addNode('Thomas');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Thomas');
+        }, usage());
+      },
+      'it should be possible to add self loops.': function itShouldBePossibleToAddSelfLoops() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        var loop = graph.addDirectedEdge('Thomas', 'Thomas');
+
+        _assert["default"].deepStrictEqual(graph.extremities(loop), ['Thomas', 'Thomas']);
+      },
+      'it should throw if the graph is not multi & we try to add twice the same edge.': function itShouldThrowIfTheGraphIsNotMultiWeTryToAddTwiceTheSameEdge() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addDirectedEdge('Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+      },
+      "it should return the generated edge's key.": function itShouldReturnTheGeneratedEdgeSKey() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('Thomas', 'Martha');
+        (0, _assert["default"])(typeof edge === 'string' || typeof edge === 'number');
+        (0, _assert["default"])(!(edge instanceof Graph));
+      }
+    },
+    '#.addEdge': {
+      'it should add a directed edge if the graph is directed or mixed.': function itShouldAddADirectedEdgeIfTheGraphIsDirectedOrMixed() {
+        var graph = new Graph(),
+            directedGraph = new Graph({
+          type: 'directed'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var mixedEdge = graph.addEdge('John', 'Martha');
+        directedGraph.addNode('John');
+        directedGraph.addNode('Martha');
+        var directedEdge = directedGraph.addEdge('John', 'Martha');
+        (0, _assert["default"])(graph.isDirected(mixedEdge));
+        (0, _assert["default"])(directedGraph.isDirected(directedEdge));
+      },
+      'it should add an undirected edge if the graph is undirected.': function itShouldAddAnUndirectedEdgeIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addEdge('John', 'Martha');
+        (0, _assert["default"])(graph.isUndirected(edge));
+      }
+    },
+    '#.addDirectedEdgeWithKey': {
+      'it should throw if an edge with the same key already exists.': function itShouldThrowIfAnEdgeWithTheSameKeyAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addUndirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+      }
+    },
+    '#.addUndirectedEdgeWithKey': {
+      'it should throw if an edge with the same key already exists.': function itShouldThrowIfAnEdgeWithTheSameKeyAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+        }, usage());
+      }
+    },
+    '#.addEdgeWithKey': {
+      'it should add a directed edge if the graph is directed or mixed.': function itShouldAddADirectedEdgeIfTheGraphIsDirectedOrMixed() {
+        var graph = new Graph(),
+            directedGraph = new Graph({
+          type: 'directed'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var mixedEdge = graph.addEdgeWithKey('J->M', 'John', 'Martha');
+        directedGraph.addNode('John');
+        directedGraph.addNode('Martha');
+        var directedEdge = directedGraph.addEdgeWithKey('J->M', 'John', 'Martha');
+        (0, _assert["default"])(graph.isDirected(mixedEdge));
+        (0, _assert["default"])(directedGraph.isDirected(directedEdge));
+      },
+      'it should add an undirected edge if the graph is undirected.': function itShouldAddAnUndirectedEdgeIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addEdgeWithKey('J<->M', 'John', 'Martha');
+        (0, _assert["default"])(graph.isUndirected(edge));
+      }
+    },
+    '#.mergeEdge': {
+      'it should add the edge if it does not yet exist.': function itShouldAddTheEdgeIfItDoesNotYetExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should do nothing if the edge already exists.': function itShouldDoNothingIfTheEdgeAlreadyExists() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha');
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should merge existing attributes if any.': function itShouldMergeExistingAttributesIfAny() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          type: 'KNOWS'
+        });
+        graph.mergeEdge('John', 'Martha', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          type: 'KNOWS',
+          weight: 2
+        });
+      },
+      'it should add missing nodes in the path.': function itShouldAddMissingNodesInThePath() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Martha']);
+      },
+      'it should throw in case of inconsistencies.': function itShouldThrowInCaseOfInconsistencies() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey('J->M', 'John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeEdgeWithKey('J->M', 'John', 'Thomas');
+        }, usage());
+      },
+      'it should be able to merge undirected edges in both directions.': function itShouldBeAbleToMergeUndirectedEdgesInBothDirections() {
+        _assert["default"].doesNotThrow(function () {
+          var graph = new Graph();
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'John', 'Martha');
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'John', 'Martha');
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'Martha', 'John');
+        }, usage());
+      },
+      'it should distinguish between typed edges.': function itShouldDistinguishBetweenTypedEdges() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'Martha', {
+          type: 'LIKES'
+        });
+        graph.mergeUndirectedEdge('John', 'Martha', {
+          weight: 34
+        });
+
+        _assert["default"].strictEqual(graph.size, 2);
+      },
+      'it should be possible to merge a self loop.': function itShouldBePossibleToMergeASelfLoop() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'John', {
+          type: 'IS'
+        });
+
+        _assert["default"].strictEqual(graph.order, 1);
+
+        _assert["default"].strictEqual(graph.size, 1);
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+        var info = graph.mergeEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), true, true, true]);
+
+        info = graph.mergeEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), false, false, false]);
+
+        graph.addNode('Mary');
+        info = graph.mergeEdge('Mary', 'Sue');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Mary', 'Sue'), true, false, true]);
+
+        info = graph.mergeEdge('Gwladys', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Gwladys', 'Mary'), true, true, false]);
+
+        graph.addNode('Quintin');
+        info = graph.mergeEdge('Quintin', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Quintin', 'Mary'), true, false, false]);
+      }
+    },
+    '#.updateEdge': {
+      'it should add the edge if it does not yet exist.': function itShouldAddTheEdgeIfItDoesNotYetExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should do nothing if the edge already exists.': function itShouldDoNothingIfTheEdgeAlreadyExists() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha');
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should be possible to start from blank attributes.': function itShouldBePossibleToStartFromBlankAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.updateEdge('John', 'Martha', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 3
+          });
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          weight: 3
+        });
+      },
+      'it should update existing attributes if any.': function itShouldUpdateExistingAttributesIfAny() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          type: 'KNOWS'
+        });
+        graph.updateEdge('John', 'Martha', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 2
+          });
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          type: 'KNOWS',
+          weight: 2
+        });
+      },
+      'it should add missing nodes in the path.': function itShouldAddMissingNodesInThePath() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Martha']);
+      },
+      'it should throw in case of inconsistencies.': function itShouldThrowInCaseOfInconsistencies() {
+        var graph = new Graph();
+        graph.updateEdgeWithKey('J->M', 'John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeWithKey('J->M', 'John', 'Thomas');
+        }, usage());
+      },
+      'it should distinguish between typed edges.': function itShouldDistinguishBetweenTypedEdges() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'Martha', function () {
+          return {
+            type: 'LIKES'
+          };
+        });
+        graph.updateUndirectedEdge('John', 'Martha', function () {
+          return {
+            weight: 34
+          };
+        });
+
+        _assert["default"].strictEqual(graph.size, 2);
+      },
+      'it should be possible to merge a self loop.': function itShouldBePossibleToMergeASelfLoop() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'John', function () {
+          return {
+            type: 'IS'
+          };
+        });
+
+        _assert["default"].strictEqual(graph.order, 1);
+
+        _assert["default"].strictEqual(graph.size, 1);
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+        var info = graph.updateEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), true, true, true]);
+
+        info = graph.updateEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), false, false, false]);
+
+        graph.addNode('Mary');
+        info = graph.updateEdge('Mary', 'Sue');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Mary', 'Sue'), true, false, true]);
+
+        info = graph.updateEdge('Gwladys', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Gwladys', 'Mary'), true, true, false]);
+
+        graph.addNode('Quintin');
+        info = graph.updateEdge('Quintin', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Quintin', 'Mary'), true, false, false]);
+      }
+    },
+    '#.dropEdge': {
+      'it should throw if the edge or nodes in the path are not found in the graph.': function itShouldThrowIfTheEdgeOrNodesInThePathAreNotFoundInTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('Test');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('Forever', 'Alone');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('John', 'Test');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('John', 'Martha');
+        }, notFound());
+      },
+      'it should correctly remove the given edge from the graph.': function itShouldCorrectlyRemoveTheGivenEdgeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        var edge = graph.addEdge('John', 'Margaret');
+        graph.dropEdge(edge);
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.degree('Margaret'), 0);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should be possible to remove an edge using source & target.': function itShouldBePossibleToRemoveAnEdgeUsingSourceTarget() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        graph.addEdge('John', 'Margaret');
+        graph.dropEdge('John', 'Margaret');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.degree('Margaret'), 0);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Margaret'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'John');
+        graph.dropEdge('John', 'John');
+
+        _assert["default"].deepStrictEqual(graph.edges(), []);
+
+        _assert["default"].deepStrictEqual(graph.edges('John'), []);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        var multiGraph = new Graph({
+          multi: true
+        });
+        multiGraph.mergeEdgeWithKey('j', 'John', 'John');
+        multiGraph.mergeEdgeWithKey('k', 'John', 'John');
+        multiGraph.dropEdge('j');
+
+        _assert["default"].deepStrictEqual(multiGraph.edges(), ['k']);
+
+        _assert["default"].deepStrictEqual(multiGraph.edges('John'), ['k']);
+
+        _assert["default"].strictEqual(multiGraph.size, 1);
+      }
+    },
+    '#.dropNode': {
+      'it should throw if the edge is not found in the graph.': function itShouldThrowIfTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.dropNode('Test');
+        }, notFound());
+      },
+      'it should correctly remove the given node from the graph.': function itShouldCorrectlyRemoveTheGivenNodeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        var edge = graph.addEdge('John', 'Margaret');
+        graph.mergeEdge('Jack', 'Trudy');
+        graph.dropNode('Margaret');
+
+        _assert["default"].strictEqual(graph.order, 3);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasNode('Margaret'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should also work with mixed, multi graphs and self loops.': function itShouldAlsoWorkWithMixedMultiGraphsAndSelfLoops() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('B', 'A');
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('A', 'A');
+        graph.mergeUndirectedEdge('A', 'B');
+        graph.mergeUndirectedEdge('A', 'B');
+        graph.mergeUndirectedEdge('A', 'A');
+        var copy = graph.copy();
+        graph.dropNode('B');
+
+        _assert["default"].strictEqual(graph.size, 2);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 1);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 1);
+
+        copy.dropNode('A');
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        _assert["default"].strictEqual(copy.directedSelfLoopCount, 0);
+
+        _assert["default"].strictEqual(copy.undirectedSelfLoopCount, 0);
+      },
+      'it should also coerce keys as strings.': function itShouldAlsoCoerceKeysAsStrings() {
+        function Key(name) {
+          this.name = name;
+        }
+
+        Key.prototype.toString = function () {
+          return this.name;
+        };
+
+        var graph = new Graph();
+        var key = new Key('test');
+        graph.addNode(key);
+        graph.dropNode(key);
+
+        _assert["default"].strictEqual(graph.order, 0);
+
+        _assert["default"].strictEqual(graph.hasNode(key), false);
+      }
+    },
+    '#.dropDirectedEdge': {
+      'it should throw if given incorrect arguments.': function itShouldThrowIfGivenIncorrectArguments() {
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true
+          });
+          graph.mergeEdge('a', 'b');
+          graph.dropDirectedEdge('a', 'b');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true
+          });
+          graph.mergeEdgeWithKey('1', 'a', 'b');
+          graph.dropDirectedEdge('1');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph();
+          graph.dropDirectedEdge('a', 'b');
+        }, notFound());
+      },
+      'it should correctly drop the relevant edge.': function itShouldCorrectlyDropTheRelevantEdge() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('a', 'b');
+        graph.mergeDirectedEdge('a', 'b');
+        graph.dropDirectedEdge('a', 'b');
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('a', 'b'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('a', 'b'), true);
+      }
+    },
+    '#.dropUndirectedEdge': {
+      'it should throw if given incorrect arguments.': function itShouldThrowIfGivenIncorrectArguments() {
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true,
+            type: 'undirected'
+          });
+          graph.mergeEdge('a', 'b');
+          graph.dropUndirectedEdge('a', 'b');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true,
+            type: 'undirected'
+          });
+          graph.mergeEdgeWithKey('1', 'a', 'b');
+          graph.dropUndirectedEdge('1');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          graph.dropUndirectedEdge('a', 'b');
+        }, notFound());
+      },
+      'it should correctly drop the relevant edge.': function itShouldCorrectlyDropTheRelevantEdge() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('a', 'b');
+        graph.mergeDirectedEdge('a', 'b');
+        graph.dropUndirectedEdge('a', 'b');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('a', 'b'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('a', 'b'), true);
+      }
+    },
+    '#.clear': {
+      'it should empty the graph.': function itShouldEmptyTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+        graph.clear();
+
+        _assert["default"].strictEqual(graph.order, 0);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), false);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+      },
+      'it should be possible to use the graph normally afterwards.': function itShouldBePossibleToUseTheGraphNormallyAfterwards() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        graph.addEdge('Lindsay', 'Martha');
+        graph.clear();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), true);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), true);
+      }
+    },
+    '#.clearEdges': {
+      'it should drop every edge from the graph.': function itShouldDropEveryEdgeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), true);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+      },
+      'it should properly reset instance counters.': function itShouldProperlyResetInstanceCounters() {
+        var graph = new Graph();
+        graph.mergeEdge(0, 1);
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        graph.mergeEdge(0, 1);
+        graph.clear();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      },
+      'it should properly clear node indices, regarding self loops notably.': function itShouldProperlyClearNodeIndicesRegardingSelfLoopsNotably() {
+        var graph = new Graph();
+        graph.mergeEdge(1, 1);
+
+        _assert["default"].strictEqual(graph.degree(1), 2);
+
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.degree(1), 0);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/properties.js b/libs/shared/graph-layout/node_modules/graphology/specs/properties.js
new file mode 100644
index 0000000000000000000000000000000000000000..4babe3dc5dc23e69132ea1a0429124d920ccafff
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/properties.js
@@ -0,0 +1,214 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = properties;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
+
+var PROPERTIES = ['order', 'size', 'directedSize', 'undirectedSize', 'type', 'multi', 'allowSelfLoops', 'implementation', 'selfLoopCount', 'directedSelfLoopCount', 'undirectedSelfLoopCount'];
+
+function properties(Graph) {
+  return {
+    /**
+     * Regarding all properties.
+     */
+    misc: {
+      'all expected properties should be set.': function allExpectedPropertiesShouldBeSet() {
+        var graph = new Graph();
+        PROPERTIES.forEach(function (property) {
+          (0, _assert["default"])(property in graph, property);
+        });
+      },
+      'properties should be read-only.': function propertiesShouldBeReadOnly() {
+        var graph = new Graph(); // Attempting to mutate the properties
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"]["throws"](function () {
+            graph[property] = 'test';
+          }, TypeError);
+        });
+      }
+    },
+
+    /**
+     * Order.
+     */
+    '#.order': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.order, 0);
+      },
+      'adding nodes should increase order.': function addingNodesShouldIncreaseOrder() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+
+        _assert["default"].strictEqual(graph.order, 2);
+      }
+    },
+
+    /**
+     * Size.
+     */
+    '#.size': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.size, 0);
+      },
+      'adding & dropping edges should affect size.': function addingDroppingEdgesShouldAffectSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        graph.dropEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.size, 0);
+      }
+    },
+
+    /**
+     * Directed Size.
+     */
+    '#.directedSize': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      },
+      'adding & dropping edges should affect directed size.': function addingDroppingEdgesShouldAffectDirectedSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        var directedEdge = graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        var undirectedEdge = graph.addUndirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        graph.dropEdge(directedEdge);
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        graph.dropEdge(undirectedEdge);
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      }
+    },
+
+    /**
+     * Undirected Size.
+     */
+    '#.undirectedSize': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+      },
+      'adding & dropping edges should affect undirected size.': function addingDroppingEdgesShouldAffectUndirectedSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        var directedEdge = graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+
+        var undirectedEdge = graph.addUndirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 1);
+
+        graph.dropEdge(directedEdge);
+
+        _assert["default"].strictEqual(graph.undirectedSize, 1);
+
+        graph.dropEdge(undirectedEdge);
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+      }
+    },
+
+    /**
+     * Multi.
+     */
+    '#.multi': {
+      'it should be false by default.': function itShouldBeFalseByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.multi, false);
+      }
+    },
+
+    /**
+     * Type.
+     */
+    '#.type': {
+      'it should be "mixed" by default.': function itShouldBeMixedByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.type, 'mixed');
+      }
+    },
+
+    /**
+     * Self loops.
+     */
+    '#.allowSelfLoops': {
+      'it should be true by default.': function itShouldBeTrueByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.allowSelfLoops, true);
+      }
+    },
+
+    /**
+     * Implementation.
+     */
+    '#.implementation': {
+      'it should exist and be a string.': function itShouldExistAndBeAString() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(_typeof(graph.implementation), 'string');
+      }
+    },
+
+    /**
+     * Self Loop Count.
+     */
+    '#.selfLoopCount': {
+      'it should exist and be correct.': function itShouldExistAndBeCorrect() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('John', 'John');
+        graph.mergeDirectedEdge('Lucy', 'Lucy');
+        graph.mergeUndirectedEdge('Joana', 'Joana');
+
+        _assert["default"].strictEqual(graph.selfLoopCount, 3);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 2);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 1);
+
+        graph.forEachEdge(function (edge) {
+          return graph.dropEdge(edge);
+        });
+
+        _assert["default"].strictEqual(graph.selfLoopCount, 0);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 0);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 0);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/read.js b/libs/shared/graph-layout/node_modules/graphology/specs/read.js
new file mode 100644
index 0000000000000000000000000000000000000000..afe9f1c66fcdc4b23b4e813f1d80362be6125569
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/read.js
@@ -0,0 +1,909 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = read;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Read Specs
+ * ======================
+ *
+ * Testing the read methods of the graph.
+ */
+function read(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+  return {
+    '#.hasNode': {
+      'it should correctly return whether the given node is found in the graph.': function itShouldCorrectlyReturnWhetherTheGivenNodeIsFoundInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.hasNode('John'), false);
+
+        graph.addNode('John');
+
+        _assert["default"].strictEqual(graph.hasNode('John'), true);
+      }
+    },
+    '#.hasDirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasDirectedEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('M->C'), true);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('C<->J'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Martha', 'Catherine'), true);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Catherine', 'John'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Catherine'), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), false);
+      }
+    },
+    '#.hasUndirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasUndirectedEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('M->C'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('C<->J'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Martha', 'Catherine'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Catherine', 'John'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('John', 'Catherine'), true);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), true);
+      }
+    },
+    '#.hasEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasEdge('M->C'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('C<->J'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge('Martha', 'Catherine'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge('Catherine', 'John'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Catherine'), true);
+      },
+      'it should work properly with typed graphs.': function itShouldWorkProperlyWithTypedGraphs() {
+        var directedGraph = new Graph({
+          type: 'directed'
+        }),
+            undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        (0, _helpers.addNodesFrom)(directedGraph, [1, 2]);
+        (0, _helpers.addNodesFrom)(undirectedGraph, [1, 2]);
+
+        _assert["default"].strictEqual(directedGraph.hasEdge(1, 2), false);
+
+        _assert["default"].strictEqual(undirectedGraph.hasEdge(1, 2), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('Lucy', 'Lucy'), true);
+      }
+    },
+    '#.directedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.directedEdge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.directedEdge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.directedEdge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.directedEdge('Lucy', 'Jack'), undefined);
+
+        _assert["default"].strictEqual(graph.directedEdge('Jack', 'Lucy'), 'J->L');
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Jack', 'Lucy');
+
+        _assert["default"].strictEqual(undirectedGraph.directedEdge('Jack', 'Lucy'), undefined);
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.directedEdge('John', 'John'), 'd');
+      }
+    },
+    '#.undirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.undirectedEdge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.undirectedEdge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.undirectedEdge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('Lucy', 'Jack'), 'J<->L');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('Jack', 'Lucy'), 'J<->L');
+
+        var directedGraph = new Graph({
+          type: 'directed'
+        });
+        directedGraph.mergeEdge('Jack', 'Lucy');
+
+        _assert["default"].strictEqual(directedGraph.undirectedEdge('Jack', 'Lucy'), undefined);
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('John', 'John'), 'u');
+      }
+    },
+    '#.edge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.edge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.edge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.edge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.edge('Lucy', 'Jack'), 'J<->L');
+
+        _assert["default"].strictEqual(graph.edge('Jack', 'Lucy'), 'J->L');
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.edge('John', 'John'), 'd');
+      }
+    },
+    '#.areDirectedNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areDirectedNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areDirectedNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areInNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areInNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areInNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areOutNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areOutNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areOutNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areOutboundNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areOutboundNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Martha', 'Mary'), true);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areOutboundNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.areInboundNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areInboundNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Martha', 'Mary'), true);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areInboundNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.areUndirectedNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areUndirectedNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Martha', 'Mary'), true);
+
+        var directedGraph = new Graph({
+          type: 'directed'
+        });
+        directedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(directedGraph.areUndirectedNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Martha', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Joseph', 'Martha'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.source': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.source('test');
+        }, notFound());
+      },
+      'it should return the correct source.': function itShouldReturnTheCorrectSource() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.source(edge), 'John');
+      }
+    },
+    '#.target': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.target('test');
+        }, notFound());
+      },
+      'it should return the correct target.': function itShouldReturnTheCorrectTarget() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.target(edge), 'Martha');
+      }
+    },
+    '#.extremities': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.extremities('test');
+        }, notFound());
+      },
+      'it should return the correct extremities.': function itShouldReturnTheCorrectExtremities() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].deepStrictEqual(graph.extremities(edge), ['John', 'Martha']);
+      }
+    },
+    '#.opposite': {
+      'it should throw if either the node or the edge is not found in the graph.': function itShouldThrowIfEitherTheNodeOrTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Jeremy', 'T->J');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Thomas', 'T->J');
+        }, notFound());
+      },
+      'it should throw if the node & the edge are not related.': function itShouldThrowIfTheNodeTheEdgeAreNotRelated() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Thomas', 'Isabella', 'Estelle']);
+        graph.addEdgeWithKey('I->E', 'Isabella', 'Estelle');
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Thomas', 'I->E');
+        }, notFound());
+      },
+      'it should return the correct node.': function itShouldReturnTheCorrectNode() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Thomas', 'Estelle']);
+        var edge = graph.addEdge('Thomas', 'Estelle');
+
+        _assert["default"].strictEqual(graph.opposite('Thomas', edge), 'Estelle');
+      }
+    },
+    '#.hasExtremity': {
+      'it should throw if either the edge is not found in the graph.': function itShouldThrowIfEitherTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+        graph.mergeEdge('Thomas', 'Laura');
+
+        _assert["default"]["throws"](function () {
+          graph.hasExtremity('inexisting-edge', 'Thomas');
+        }, notFound());
+      },
+      'it should return the correct answer.': function itShouldReturnTheCorrectAnswer() {
+        var graph = new Graph();
+        graph.addNode('Jack');
+
+        var _graph$mergeEdge = graph.mergeEdge('Thomas', 'Estelle'),
+            edge = _graph$mergeEdge[0];
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Thomas'), true);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Estelle'), true);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Jack'), false);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Who?'), false);
+      }
+    },
+    '#.isDirected': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isDirected('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is directed or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsDirectedOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        graph.addNode('Suzan');
+        var directedEdge = graph.addDirectedEdge('John', 'Rachel'),
+            undirectedEdge = graph.addUndirectedEdge('Rachel', 'Suzan');
+
+        _assert["default"].strictEqual(graph.isDirected(directedEdge), true);
+
+        _assert["default"].strictEqual(graph.isDirected(undirectedEdge), false);
+      }
+    },
+    '#.isUndirected': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isUndirected('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is undirected or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsUndirectedOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        graph.addNode('Suzan');
+        var directedEdge = graph.addDirectedEdge('John', 'Rachel'),
+            undirectedEdge = graph.addUndirectedEdge('Rachel', 'Suzan');
+
+        _assert["default"].strictEqual(graph.isUndirected(directedEdge), false);
+
+        _assert["default"].strictEqual(graph.isUndirected(undirectedEdge), true);
+      }
+    },
+    '#.isSelfLoop': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isSelfLoop('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is a self-loop or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsASelfLoopOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        var selfLoop = graph.addDirectedEdge('John', 'John'),
+            edge = graph.addUndirectedEdge('John', 'Rachel');
+
+        _assert["default"].strictEqual(graph.isSelfLoop(selfLoop), true);
+
+        _assert["default"].strictEqual(graph.isSelfLoop(edge), false);
+      }
+    },
+    Degree: {
+      '#.inDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.inDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct in degree.': function itShouldReturnTheCorrectInDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('William', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Sue'), 2);
+
+          graph.addDirectedEdge('Sue', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Sue'), 3);
+
+          _assert["default"].strictEqual(graph.inDegreeWithoutSelfLoops('Sue'), 2);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Helen'), 0);
+        }
+      },
+      '#.inboundDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.inboundDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct in degree.': function itShouldReturnTheCorrectInDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('William', 'Sue');
+          graph.addUndirectedEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Sue'), 3);
+
+          graph.addDirectedEdge('Sue', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Sue'), 4);
+
+          _assert["default"].strictEqual(graph.inboundDegreeWithoutSelfLoops('Sue'), 3);
+        },
+        'it should always the undirected degree in an undirected graph.': function itShouldAlwaysTheUndirectedDegreeInAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Helen'), 1);
+        }
+      },
+      '#.outDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.outDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct out degree.': function itShouldReturnTheCorrectOutDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+
+          _assert["default"].strictEqual(graph.outDegree('Helen'), 2);
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.outDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.outDegreeWithoutSelfLoops('Helen'), 2);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outDegree('Sue'), 0);
+        }
+      },
+      '#.outboundDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.outboundDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct out degree.': function itShouldReturnTheCorrectOutDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addUndirectedEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Helen'), 3);
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Helen'), 4);
+
+          _assert["default"].strictEqual(graph.outboundDegreeWithoutSelfLoops('Helen'), 3);
+        },
+        'it should always the undirected degree in an undirected graph.': function itShouldAlwaysTheUndirectedDegreeInAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Sue'), 1);
+        }
+      },
+      '#.directedDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.directedDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct directed degree.': function itShouldReturnTheCorrectDirectedDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John', 'Martha']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addDirectedEdge('Martha', 'Helen');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), graph.inDegree('Helen') + graph.outDegree('Helen'));
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), 5);
+
+          _assert["default"].strictEqual(graph.directedDegreeWithoutSelfLoops('Helen'), 3);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Helen'), 0);
+        }
+      },
+      '#.undirectedDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.undirectedDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct undirected degree.': function itShouldReturnTheCorrectUndirectedDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 1);
+
+          graph.addUndirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.undirectedDegreeWithoutSelfLoops('Helen'), 1);
+        },
+        'it should always return 0 in a directed graph.': function itShouldAlwaysReturn0InADirectedGraph() {
+          var graph = new Graph({
+            type: 'directed'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 0);
+        }
+      },
+      '#.degree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.degree('Test');
+          }, notFound());
+        },
+        'it should return the correct degree.': function itShouldReturnTheCorrectDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John', 'Martha']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addDirectedEdge('Martha', 'Helen');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.degree('Helen'), 4);
+
+          _assert["default"].strictEqual(graph.degree('Helen'), graph.directedDegree('Helen') + graph.undirectedDegree('Helen'));
+
+          graph.addUndirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.degree('Helen'), 6);
+
+          _assert["default"].strictEqual(graph.degreeWithoutSelfLoops('Helen'), 4);
+        }
+      },
+      'it should also work with typed graphs.': function itShouldAlsoWorkWithTypedGraphs() {
+        var directedGraph = new Graph({
+          type: 'directed'
+        }),
+            undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        (0, _helpers.addNodesFrom)(directedGraph, [1, 2]);
+        (0, _helpers.addNodesFrom)(undirectedGraph, [1, 2]);
+
+        _assert["default"].strictEqual(directedGraph.degree(1), 0);
+
+        _assert["default"].strictEqual(undirectedGraph.degree(1), 0);
+
+        directedGraph.addDirectedEdge(1, 2);
+        undirectedGraph.addUndirectedEdge(1, 2);
+
+        _assert["default"].strictEqual(directedGraph.degree(1), 1);
+
+        _assert["default"].strictEqual(undirectedGraph.degree(1), 1);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/serialization.js b/libs/shared/graph-layout/node_modules/graphology/specs/serialization.js
new file mode 100644
index 0000000000000000000000000000000000000000..fc9318ae907039544a0aa110933e4c56ad84e946
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/serialization.js
@@ -0,0 +1,171 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = serialization;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Serializaton Specs
+ * ==============================
+ *
+ * Testing the serialization methods of the graph.
+ */
+function serialization(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound;
+  return {
+    '#.export': {
+      'it should correctly return the serialized graph.': function itShouldCorrectlyReturnTheSerializedGraph() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.setAttribute('name', 'graph');
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Jack', 'Martha']);
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.addEdgeWithKey('J->J•1', 'John', 'Jack');
+        graph.addEdgeWithKey('J->J•2', 'John', 'Jack', {
+          weight: 2
+        });
+        graph.addEdgeWithKey('J->J•3', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•1', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•2', 'John', 'Jack', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph["export"](), {
+          attributes: {
+            name: 'graph'
+          },
+          nodes: [{
+            key: 'John',
+            attributes: {
+              age: 34
+            }
+          }, {
+            key: 'Jack'
+          }, {
+            key: 'Martha'
+          }],
+          edges: [{
+            key: 'J->J•1',
+            source: 'John',
+            target: 'Jack'
+          }, {
+            key: 'J->J•2',
+            source: 'John',
+            target: 'Jack',
+            attributes: {
+              weight: 2
+            }
+          }, {
+            key: 'J->J•3',
+            source: 'John',
+            target: 'Jack'
+          }, {
+            key: 'J<->J•1',
+            source: 'John',
+            target: 'Jack',
+            undirected: true
+          }, {
+            key: 'J<->J•2',
+            source: 'John',
+            target: 'Jack',
+            attributes: {
+              weight: 3
+            },
+            undirected: true
+          }],
+          options: {
+            allowSelfLoops: true,
+            multi: true,
+            type: 'mixed'
+          }
+        });
+      }
+    },
+    '#.import': {
+      'it should throw if the given data is invalid.': function itShouldThrowIfTheGivenDataIsInvalid() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph["import"](true);
+        }, invalid());
+      },
+      'it should be possible to import a graph instance.': function itShouldBePossibleToImportAGraphInstance() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var other = new Graph();
+        other["import"](graph);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), other.nodes());
+
+        _assert["default"].deepStrictEqual(graph.edges(), other.edges());
+      },
+      'it should be possible to import a serialized graph.': function itShouldBePossibleToImportASerializedGraph() {
+        var graph = new Graph();
+        graph["import"]({
+          nodes: [{
+            key: 'John'
+          }, {
+            key: 'Thomas'
+          }],
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to import only edges when merging.': function itShouldBePossibleToImportOnlyEdgesWhenMerging() {
+        var graph = new Graph();
+        graph["import"]({
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        }, true);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to import attributes.': function itShouldBePossibleToImportAttributes() {
+        var graph = new Graph();
+        graph["import"]({
+          attributes: {
+            name: 'graph'
+          }
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph'
+        });
+      },
+      'it should throw if nodes are absent, edges are present and we merge.': function itShouldThrowIfNodesAreAbsentEdgesArePresentAndWeMerge() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph["import"]({
+            edges: [{
+              source: '1',
+              target: '2'
+            }]
+          });
+        }, notFound());
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/graphology/specs/utils.js b/libs/shared/graph-layout/node_modules/graphology/specs/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b7158a8a9d0b1236b83e7dff5a749cdb1cf299a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/graphology/specs/utils.js
@@ -0,0 +1,249 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = utils;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Utils Specs
+ * =======================
+ *
+ * Testing the utils methods.
+ */
+var PROPERTIES = ['type', 'multi', 'map', 'selfLoops'];
+
+function utils(Graph, checkers) {
+  var usage = checkers.usage;
+  return {
+    '#.nullCopy': {
+      'it should create an null copy of the graph.': function itShouldCreateAnNullCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.nullCopy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), []);
+
+        _assert["default"].strictEqual(copy.order, 0);
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], copy[property]);
+        });
+      },
+      'it should be possible to pass options to merge.': function itShouldBePossibleToPassOptionsToMerge() {
+        var graph = new Graph({
+          type: 'directed'
+        });
+        var copy = graph.nullCopy({
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(copy.type, 'undirected');
+
+        _assert["default"]["throws"](function () {
+          copy.addDirectedEdge('one', 'two');
+        }, /addDirectedEdge/);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.nullCopy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      }
+    },
+    '#.emptyCopy': {
+      'it should create an empty copy of the graph.': function itShouldCreateAnEmptyCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.emptyCopy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(copy.order, 2);
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], copy[property]);
+        });
+        copy.mergeEdge('Mary', 'Thomas');
+        copy.setNodeAttribute('John', 'age', 32);
+
+        _assert["default"].strictEqual(copy.order, 3);
+
+        _assert["default"].strictEqual(copy.size, 1);
+
+        _assert["default"].deepStrictEqual(copy.getNodeAttributes('John'), {
+          age: 32
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {});
+      },
+      'it should be possible to pass options to merge.': function itShouldBePossibleToPassOptionsToMerge() {
+        var graph = new Graph({
+          type: 'directed'
+        });
+        var copy = graph.emptyCopy({
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(copy.type, 'undirected');
+
+        _assert["default"]["throws"](function () {
+          copy.addDirectedEdge('one', 'two');
+        }, /addDirectedEdge/);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.emptyCopy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      }
+    },
+    '#.copy': {
+      'it should create a full copy of the graph.': function itShouldCreateAFullCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), graph.nodes());
+
+        _assert["default"].deepStrictEqual(copy.edges(), graph.edges());
+
+        _assert["default"].strictEqual(copy.order, 2);
+
+        _assert["default"].strictEqual(copy.size, 1);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], graph[property]);
+        });
+      },
+      'it should not break when copying a graph with wrangled edge ids (issue #213).': function itShouldNotBreakWhenCopyingAGraphWithWrangledEdgeIdsIssue213() {
+        var graph = new Graph();
+        graph.addNode('n0');
+        graph.addNode('n1');
+        graph.addNode('n2');
+        graph.addNode('n3');
+        graph.addEdge('n0', 'n1');
+        graph.addEdge('n1', 'n2');
+        graph.addEdge('n2', 'n3');
+        graph.addEdge('n3', 'n0');
+
+        _assert["default"].doesNotThrow(function () {
+          graph.copy();
+        }); // Do surgery on second edge
+
+
+        var edgeToSplit = graph.edge('n1', 'n2');
+        var newNode = 'n12';
+        graph.addNode(newNode);
+        graph.dropEdge('n1', 'n2');
+        graph.addEdge('n1', newNode);
+        graph.addEdgeWithKey(edgeToSplit, newNode, 'n2');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(new Set(graph.nodes()), new Set(copy.nodes()));
+
+        _assert["default"].deepStrictEqual(new Set(graph.edges()), new Set(copy.edges()));
+
+        _assert["default"].notStrictEqual(graph.getNodeAttributes('n1'), copy.getNodeAttributes('n1'));
+
+        _assert["default"].doesNotThrow(function () {
+          copy.addEdge('n0', 'n12');
+        });
+      },
+      'it should not break on adversarial inputs.': function itShouldNotBreakOnAdversarialInputs() {
+        var graph = new Graph();
+        graph.mergeEdge(0, 1);
+        graph.mergeEdge(1, 2);
+        graph.mergeEdge(2, 0);
+        var copy = graph.copy();
+        copy.mergeEdge(3, 4);
+        var serializedCopy = Graph.from(graph["export"]());
+
+        _assert["default"].doesNotThrow(function () {
+          serializedCopy.mergeEdge(3, 4);
+        });
+
+        _assert["default"].strictEqual(serializedCopy.size, 4);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      },
+      'it should be possible to upgrade a graph.': function itShouldBePossibleToUpgradeAGraph() {
+        var strict = new Graph({
+          type: 'directed',
+          allowSelfLoops: false
+        });
+        var loose = new Graph({
+          multi: true
+        });
+
+        _assert["default"].strictEqual(strict.copy({
+          type: 'directed'
+        }).type, 'directed');
+
+        _assert["default"].strictEqual(strict.copy({
+          multi: false
+        }).multi, false);
+
+        _assert["default"].strictEqual(strict.copy({
+          allowSelfLoops: false
+        }).allowSelfLoops, false);
+
+        _assert["default"].strictEqual(strict.copy({
+          type: 'mixed'
+        }).type, 'mixed');
+
+        _assert["default"].strictEqual(strict.copy({
+          multi: true
+        }).multi, true);
+
+        _assert["default"].strictEqual(strict.copy({
+          allowSelfLoops: true
+        }).allowSelfLoops, true);
+
+        _assert["default"]["throws"](function () {
+          strict.copy({
+            type: 'undirected'
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            type: 'undirected'
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            multi: false
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            allowSelfLoops: false
+          });
+        }, usage());
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/LICENSE.txt b/libs/shared/graph-layout/node_modules/mnemonist/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2d8d205db7c719dba8f670ebc4550ba390b477b3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/README.md b/libs/shared/graph-layout/node_modules/mnemonist/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2d8f7f302d153c4eb1020b475ab27a4fd7a9c432
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/README.md
@@ -0,0 +1,112 @@
+[![Build Status](https://github.com/Yomguithereal/mnemonist/workflows/Tests/badge.svg)](https://github.com/Yomguithereal/mnemonist/actions)
+
+# Mnemonist
+
+Mnemonist is a curated collection of data structures for the JavaScript language.
+
+It gathers classic data structures (think heap, trie etc.) as well as more exotic ones such as Buckhard-Keller trees etc.
+
+It strives at being:
+
+* As performant as possible for a high-level language.
+* Completely modular (don't need to import the whole library just to use a simple heap).
+* Simple & straightforward to use and consistent with JavaScript standard objects' API.
+* Completely typed and comfortably usable with Typescript.
+
+## Installation
+
+```
+npm install --save mnemonist
+```
+
+## Documentation
+
+Full documentation for the library can be found [here](https://yomguithereal.github.io/mnemonist).
+
+**Classics**
+
+* [Heap](https://yomguithereal.github.io/mnemonist/heap)
+* [Linked List](https://yomguithereal.github.io/mnemonist/linked-list)
+* [LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache), [LRUMap](https://yomguithereal.github.io/mnemonist/lru-map)
+* [MultiMap](https://yomguithereal.github.io/mnemonist/multi-map)
+* [MultiSet](https://yomguithereal.github.io/mnemonist/multi-set)
+* [Queue](https://yomguithereal.github.io/mnemonist/queue)
+* [Set (helpers)](https://yomguithereal.github.io/mnemonist/set)
+* [Stack](https://yomguithereal.github.io/mnemonist/stack)
+* [Trie](https://yomguithereal.github.io/mnemonist/trie)
+* [TrieMap](https://yomguithereal.github.io/mnemonist/trie-map)
+
+**Low-level & structures for very specific use cases**
+
+* [Circular Buffer](https://yomguithereal.github.io/mnemonist/circular-buffer)
+* [Fixed Deque](https://yomguithereal.github.io/mnemonist/fixed-deque)
+* [Fibonacci Heap](https://yomguithereal.github.io/mnemonist/fibonacci-heap)
+* [Fixed Reverse Heap](https://yomguithereal.github.io/mnemonist/fixed-reverse-heap)
+* [Fixed Stack](https://yomguithereal.github.io/mnemonist/fixed-stack)
+* [Hashed Array Tree](https://yomguithereal.github.io/mnemonist/hashed-array-tree)
+* [Static DisjointSet](https://yomguithereal.github.io/mnemonist/static-disjoint-set)
+* [SparseQueueSet](https://yomguithereal.github.io/mnemonist/sparse-queue-set)
+* [SparseMap](https://yomguithereal.github.io/mnemonist/sparse-map)
+* [SparseSet](https://yomguithereal.github.io/mnemonist/sparse-set)
+* [Suffix Array](https://yomguithereal.github.io/mnemonist/suffix-array)
+* [Generalized Suffix Array](https://yomguithereal.github.io/mnemonist/generalized-suffix-array)
+* [Vector](https://yomguithereal.github.io/mnemonist/vector)
+
+**Information retrieval & Natural language processing**
+
+* [Fuzzy Map](https://yomguithereal.github.io/mnemonist/fuzzy-map)
+* [Fuzzy MultiMap](https://yomguithereal.github.io/mnemonist/fuzzy-multi-map)
+* [Inverted Index](https://yomguithereal.github.io/mnemonist/inverted-index)
+* [Passjoin Index](https://yomguithereal.github.io/mnemonist/passjoin-index)
+* [SymSpell](https://yomguithereal.github.io/mnemonist/symspell)
+
+**Space & time indexation**
+
+* [Static IntervalTree](https://yomguithereal.github.io/mnemonist/static-interval-tree)
+* [KD-Tree](https://yomguithereal.github.io/mnemonist/kd-tree)
+
+**Metric space indexation**
+
+* [Burkhard-Keller Tree](https://yomguithereal.github.io/mnemonist/bk-tree)
+* [Vantage Point Tree](https://yomguithereal.github.io/mnemonist/vp-tree)
+
+**Probabilistic & succinct data structures**
+
+* [BitSet](https://yomguithereal.github.io/mnemonist/bit-set)
+* [BitVector](https://yomguithereal.github.io/mnemonist/bit-vector)
+* [Bloom Filter](https://yomguithereal.github.io/mnemonist/bloom-filter)
+
+**Utility classes**
+
+* [BiMap](https://yomguithereal.github.io/mnemonist/bi-map)
+* [DefaultMap](https://yomguithereal.github.io/mnemonist/default-map)
+* [DefaultWeakMap](https://yomguithereal.github.io/mnemonist/default-weak-map)
+
+---
+
+Note that this list does not include a `Graph` data structure, whose implementation is usually far too complex for the scope of this library.
+
+However, we advise the reader to take a look at the [`graphology`](https://graphology.github.io/) library instead.
+
+Don't find the data structure you need? Maybe we can work it out [together](https://github.com/Yomguithereal/mnemonist/issues).
+
+## Contribution
+
+Contributions are obviously welcome. Be sure to lint the code & add relevant unit tests.
+
+```
+# Installing
+git clone git@github.com:Yomguithereal/mnemonist.git
+cd mnemonist
+npm install
+
+# Linting
+npm run lint
+
+# Running the unit tests
+npm test
+```
+
+## License
+
+[MIT](LICENSE.txt)
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bi-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/bi-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0c2f766bcda43f6c5112e5a1de4a60e84b13e7f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bi-map.d.ts
@@ -0,0 +1,46 @@
+/**
+ * Mnemonist BiMap Typings
+ * ========================
+ */
+export class InverseMap<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  size: number;
+  inverse: BiMap<V, K>;
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  has(key: K): boolean;
+  get(key: K): V;
+  forEach(callback: (value: V, key: K, map: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+}
+
+export default class BiMap<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  size: number;
+  inverse: InverseMap<V, K>;
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  has(key: K): boolean;
+  get(key: K): V;
+  forEach(callback: (value: V, key: K, map: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(iterable: Iterable<[I, J]> | {[key: string]: J}): BiMap<I, J>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bi-map.js b/libs/shared/graph-layout/node_modules/mnemonist/bi-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..3d5d03f2a668ef47c8999a3dd54c00ddf0b0119e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bi-map.js
@@ -0,0 +1,195 @@
+/**
+ * Mnemonist BiMap
+ * ================
+ *
+ * JavaScript implementation of a BiMap.
+ */
+var forEach = require('obliterator/foreach');
+
+/**
+ * Inverse Map.
+ *
+ * @constructor
+ */
+function InverseMap(original) {
+
+  this.size = 0;
+  this.items = new Map();
+  this.inverse = original;
+}
+
+/**
+ * BiMap.
+ *
+ * @constructor
+ */
+function BiMap() {
+
+  this.size = 0;
+  this.items = new Map();
+  this.inverse = new InverseMap(this);
+}
+
+/**
+ * Method used to clear the map.
+ *
+ * @return {undefined}
+ */
+function clear() {
+  this.size = 0;
+  this.items.clear();
+  this.inverse.items.clear();
+}
+
+BiMap.prototype.clear = clear;
+InverseMap.prototype.clear = clear;
+
+/**
+ * Method used to set a relation.
+ *
+ * @param  {any} key - Key.
+ * @param  {any} value - Value.
+ * @return {BiMap|InverseMap}
+ */
+function set(key, value) {
+
+  // First we need to attempt to see if the relation is not flawed
+  if (this.items.has(key)) {
+    var currentValue = this.items.get(key);
+
+    // The relation already exists, we do nothing
+    if (currentValue === value)
+      return this;
+    else
+      this.inverse.items.delete(currentValue);
+  }
+
+  if (this.inverse.items.has(value)) {
+    var currentKey = this.inverse.items.get(value);
+
+    if (currentKey === key)
+      return this;
+    else
+      this.items.delete(currentKey);
+  }
+
+  // Here we actually add the relation
+  this.items.set(key, value);
+  this.inverse.items.set(value, key);
+
+  // Size
+  this.size = this.items.size;
+  this.inverse.size = this.inverse.items.size;
+
+  return this;
+}
+
+BiMap.prototype.set = set;
+InverseMap.prototype.set = set;
+
+/**
+ * Method used to delete a relation.
+ *
+ * @param  {any} key - Key.
+ * @return {boolean}
+ */
+function del(key) {
+  if (this.items.has(key)) {
+    var currentValue = this.items.get(key);
+
+    this.items.delete(key);
+    this.inverse.items.delete(currentValue);
+
+    // Size
+    this.size = this.items.size;
+    this.inverse.size = this.inverse.items.size;
+
+    return true;
+  }
+
+  return false;
+}
+
+BiMap.prototype.delete = del;
+InverseMap.prototype.delete = del;
+
+/**
+ * Mapping some Map prototype function unto our two classes.
+ */
+var METHODS = ['has', 'get', 'forEach', 'keys', 'values', 'entries'];
+
+METHODS.forEach(function(name) {
+  BiMap.prototype[name] = InverseMap.prototype[name] = function() {
+    return Map.prototype[name].apply(this.items, arguments);
+  };
+});
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined') {
+  BiMap.prototype[Symbol.iterator] = BiMap.prototype.entries;
+  InverseMap.prototype[Symbol.iterator] = InverseMap.prototype.entries;
+}
+
+/**
+ * Convenience known methods.
+ */
+BiMap.prototype.inspect = function() {
+  var dummy = {
+    left: this.items,
+    right: this.inverse.items
+  };
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(dummy, 'constructor', {
+    value: BiMap,
+    enumerable: false
+  });
+
+  return dummy;
+};
+
+if (typeof Symbol !== 'undefined')
+  BiMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = BiMap.prototype.inspect;
+
+InverseMap.prototype.inspect = function() {
+  var dummy = {
+    left: this.inverse.items,
+    right: this.items
+  };
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(dummy, 'constructor', {
+    value: InverseMap,
+    enumerable: false
+  });
+
+  return dummy;
+};
+
+if (typeof Symbol !== 'undefined')
+  InverseMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = InverseMap.prototype.inspect;
+
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a bimap.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {BiMap}
+ */
+BiMap.from = function(iterable) {
+  var bimap = new BiMap();
+
+  forEach(iterable, function(value, key) {
+    bimap.set(key, value);
+  });
+
+  return bimap;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = BiMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bit-set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/bit-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cfeb0d166e331d8218151f5a69315a9e741e525f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bit-set.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Mnemonist BitSet Typings
+ * =========================
+ */
+export default class BitSet implements Iterable<number> {
+
+  // Members
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number);
+
+  // Methods
+  clear(): void;
+  set(index: number, value?: boolean | number): void;
+  reset(index: number, value: boolean | number): void;
+  flip(index: number, value: boolean | number): void;
+  get(index: number): number;
+  test(index: number): boolean;
+  rank(r: number): number;
+  select(r: number): number;
+  forEach(callback: (index: number, value: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  entries(): IterableIterator<[number, number]>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+  toJSON(): Array<number>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bit-set.js b/libs/shared/graph-layout/node_modules/mnemonist/bit-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..f2445a01217b478c6171d811c422c783b243ed56
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bit-set.js
@@ -0,0 +1,379 @@
+/**
+ * Mnemonist BitSet
+ * =================
+ *
+ * JavaScript implementation of a fixed-size BitSet based upon a Uint32Array.
+ *
+ * Notes:
+ *   - (i >> 5) is the same as ((i / 32) | 0)
+ *   - (i & 0x0000001f) is the same as (i % 32)
+ *   - I could use a Float64Array to store more in less blocks but I would lose
+ *     the benefits of byte comparison to keep track of size without popcounts.
+ */
+var Iterator = require('obliterator/iterator'),
+    bitwise = require('./utils/bitwise.js');
+
+/**
+ * BitSet.
+ *
+ * @constructor
+ */
+function BitSet(length) {
+
+  // Properties
+  this.length = length;
+  this.clear();
+
+  // Methods
+
+  // Statics
+}
+
+/**
+ * Method used to clear the bit set.
+ *
+ * @return {undefined}
+ */
+BitSet.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.array = new Uint32Array(Math.ceil(this.length / 32));
+};
+
+/**
+ * Method used to set the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @param  {number} value - Value to set.
+ * @return {BitSet}
+ */
+BitSet.prototype.set = function(index, value) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex],
+      newBytes;
+
+  if (value === 0 || value === false)
+    newBytes = this.array[byteIndex] &= ~(1 << pos);
+  else
+    newBytes = this.array[byteIndex] |= (1 << pos);
+
+  // The operands of all bitwise operators are converted to *signed* 32-bit integers.
+  // Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Signed_32-bit_integers
+  // Shifting by 31 changes the sign (i.e. 1 << 31 = -2147483648).
+  // Therefore, get unsigned representation by applying '>>> 0'.
+  newBytes = newBytes >>> 0;
+
+  // Updating size
+  if (newBytes > oldBytes)
+    this.size++;
+  else if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+* Method used to reset the given bit's value.
+*
+* @param  {number} index - Target bit index.
+* @return {BitSet}
+*/
+BitSet.prototype.reset = function(index) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex],
+      newBytes;
+
+  newBytes = this.array[byteIndex] &= ~(1 << pos);
+
+  // Updating size
+  if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+ * Method used to flip the value of the given bit.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {BitSet}
+ */
+BitSet.prototype.flip = function(index) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex];
+
+  var newBytes = this.array[byteIndex] ^= (1 << pos);
+
+  // Get unsigned representation.
+  newBytes = newBytes >>> 0;
+
+  // Updating size
+  if (newBytes > oldBytes)
+    this.size++;
+  else if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+ * Method used to get the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {number}
+ */
+BitSet.prototype.get = function(index) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f;
+
+  return (this.array[byteIndex] >> pos) & 1;
+};
+
+/**
+ * Method used to test the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {BitSet}
+ */
+BitSet.prototype.test = function(index) {
+  return Boolean(this.get(index));
+};
+
+/**
+ * Method used to return the number of 1 from the beginning of the set up to
+ * the ith index.
+ *
+ * @param  {number} i - Ith index (cannot be > length).
+ * @return {number}
+ */
+BitSet.prototype.rank = function(i) {
+  if (this.size === 0)
+    return 0;
+
+  var byteIndex = i >> 5,
+      pos = i & 0x0000001f,
+      r = 0;
+
+  // Accessing the bytes before the last one
+  for (var j = 0; j < byteIndex; j++)
+    r += bitwise.table8Popcount(this.array[j]);
+
+  // Handling masked last byte
+  var maskedByte = this.array[byteIndex] & ((1 << pos) - 1);
+
+  r += bitwise.table8Popcount(maskedByte);
+
+  return r;
+};
+
+/**
+ * Method used to return the position of the rth 1 in the set or -1 if the
+ * set is empty.
+ *
+ * Note: usually select is implemented using binary search over rank but I
+ * tend to think the following linear implementation is faster since here
+ * rank is O(n) anyway.
+ *
+ * @param  {number} r - Rth 1 to select (should be < length).
+ * @return {number}
+ */
+BitSet.prototype.select = function(r) {
+  if (this.size === 0)
+    return -1;
+
+  // TODO: throw?
+  if (r >= this.length)
+    return -1;
+
+  var byte,
+      b = 32,
+      p = 0,
+      c = 0;
+
+  for (var i = 0, l = this.array.length; i < l; i++) {
+    byte = this.array[i];
+
+    // The byte is empty, let's continue
+    if (byte === 0)
+      continue;
+
+    // TODO: This branching might not be useful here
+    if (i === l - 1)
+      b = this.length % 32 || 32;
+
+    // TODO: popcount should speed things up here
+
+    for (var j = 0; j < b; j++, p++) {
+      c += (byte >> j) & 1;
+
+      if (c === r)
+        return p;
+    }
+  }
+};
+
+/**
+ * Method used to iterate over the bit set's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+BitSet.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var length = this.length,
+      byte,
+      bit,
+      b = 32;
+
+  for (var i = 0, l = this.array.length; i < l; i++) {
+    byte = this.array[i];
+
+    if (i === l - 1)
+      b = length % 32 || 32;
+
+    for (var j = 0; j < b; j++) {
+      bit = (byte >> j) & 1;
+
+      callback.call(scope, bit, i * 32 + j);
+    }
+  }
+};
+
+/**
+ * Method used to create an iterator over a set's values.
+ *
+ * @return {Iterator}
+ */
+BitSet.prototype.values = function() {
+  var length = this.length,
+      inner = false,
+      byte,
+      bit,
+      array = this.array,
+      l = array.length,
+      i = 0,
+      j = -1,
+      b = 32;
+
+  return new Iterator(function next() {
+    if (!inner) {
+
+      if (i >= l)
+        return {
+          done: true
+        };
+
+      if (i === l - 1)
+        b = length % 32 || 32;
+
+      byte = array[i++];
+      inner = true;
+      j = -1;
+    }
+
+    j++;
+
+    if (j >= b) {
+      inner = false;
+      return next();
+    }
+
+    bit = (byte >> j) & 1;
+
+    return {
+      value: bit
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a set's entries.
+ *
+ * @return {Iterator}
+ */
+BitSet.prototype.entries = function() {
+  var length = this.length,
+      inner = false,
+      byte,
+      bit,
+      array = this.array,
+      index,
+      l = array.length,
+      i = 0,
+      j = -1,
+      b = 32;
+
+  return new Iterator(function next() {
+    if (!inner) {
+
+      if (i >= l)
+        return {
+          done: true
+        };
+
+      if (i === l - 1)
+        b = length % 32 || 32;
+
+      byte = array[i++];
+      inner = true;
+      j = -1;
+    }
+
+    j++;
+    index = (~-i) * 32 + j;
+
+    if (j >= b) {
+      inner = false;
+      return next();
+    }
+
+    bit = (byte >> j) & 1;
+
+    return {
+      value: [index, bit]
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  BitSet.prototype[Symbol.iterator] = BitSet.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+BitSet.prototype.inspect = function() {
+  var proxy = new Uint8Array(this.length);
+
+  this.forEach(function(bit, i) {
+    proxy[i] = bit;
+  });
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: BitSet,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  BitSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = BitSet.prototype.inspect;
+
+BitSet.prototype.toJSON = function() {
+  return Array.from(this.array);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = BitSet;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4005d3c1c749d3b4b9efa76685bd39fafa746e8e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.d.ts
@@ -0,0 +1,42 @@
+/**
+ * Mnemonist BitVector Typings
+ * ============================
+ */
+type BitVectorOptions = {
+  initialLength?: number;
+  initialCapacity?: number;
+  policy?: (capacity: number) => number;
+}
+
+export default class BitVector implements Iterable<number> {
+
+  // Members
+  capacity: number;
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number);
+  constructor(options: BitVectorOptions);
+
+  // Methods
+  clear(): void;
+  set(index: number, value?: boolean | number): this;
+  reset(index: number, value: boolean | number): void;
+  flip(index: number, value: boolean | number): void;
+  reallocate(capacity: number): this;
+  grow(capacity?: number): this;
+  resize(length: number): this;
+  push(value: boolean | number): number;
+  pop(): number | undefined;
+  get(index: number): number;
+  test(index: number): boolean;
+  rank(r: number): number;
+  select(r: number): number;
+  forEach(callback: (index: number, value: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  entries(): IterableIterator<[number, number]>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+  toJSON(): Array<number>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.js b/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.js
new file mode 100644
index 0000000000000000000000000000000000000000..5ee01e66ad3c4fc4a0c6a7c9c8cbf7a048a3ae1d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bit-vector.js
@@ -0,0 +1,550 @@
+/**
+ * Mnemonist BitVector
+ * ====================
+ *
+ * JavaScript implementation of a dynamic BitSet based upon a Uint32Array.
+ *
+ * Notes:
+ *   - (i >> 5) is the same as ((i / 32) | 0)
+ *   - (i & 0x0000001f) is the same as (i % 32)
+ *   - I could use a Float64Array to store more in less blocks but I would lose
+ *     the benefits of byte comparison to keep track of size without popcounts.
+ */
+var Iterator = require('obliterator/iterator'),
+    bitwise = require('./utils/bitwise.js');
+
+/**
+ * Constants.
+ */
+var DEFAULT_GROWING_POLICY = function(capacity) {
+  return Math.max(1, Math.ceil(capacity * 1.5));
+};
+
+/**
+ * Helpers.
+ */
+function createByteArray(capacity) {
+  return new Uint32Array(Math.ceil(capacity / 32));
+}
+
+/**
+ * BitVector.
+ *
+ * @constructor
+ */
+function BitVector(initialLengthOrOptions) {
+  var initialLength = initialLengthOrOptions || 0,
+      policy = DEFAULT_GROWING_POLICY;
+
+  if (typeof initialLengthOrOptions === 'object') {
+    initialLength = (
+      initialLengthOrOptions.initialLength ||
+      initialLengthOrOptions.initialCapacity ||
+      0
+    );
+    policy = initialLengthOrOptions.policy || policy;
+  }
+
+  this.size = 0;
+  this.length = initialLength;
+  this.capacity = Math.ceil(this.length / 32) * 32;
+  this.policy = policy;
+  this.array = createByteArray(this.capacity);
+}
+
+/**
+ * Method used to set the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @param  {number|boolean} value - Value to set.
+ * @return {BitVector}
+ */
+BitVector.prototype.set = function(index, value) {
+
+  // Out of bounds?
+  if (this.length < index)
+    throw new Error('BitVector.set: index out of bounds.');
+
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex],
+      newBytes;
+
+  if (value === 0 || value === false)
+    newBytes = this.array[byteIndex] &= ~(1 << pos);
+  else
+    newBytes = this.array[byteIndex] |= (1 << pos);
+
+  // Get unsigned representation.
+  newBytes = newBytes >>> 0;
+
+  // Updating size
+  if (newBytes > oldBytes)
+    this.size++;
+  else if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+* Method used to reset the given bit's value.
+*
+* @param  {number} index - Target bit index.
+* @return {BitVector}
+*/
+BitVector.prototype.reset = function(index) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex],
+      newBytes;
+
+  newBytes = this.array[byteIndex] &= ~(1 << pos);
+
+  // Updating size
+  if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+ * Method used to flip the value of the given bit.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {BitVector}
+ */
+BitVector.prototype.flip = function(index) {
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f,
+      oldBytes = this.array[byteIndex];
+
+  var newBytes = this.array[byteIndex] ^= (1 << pos);
+
+  // Get unsigned representation.
+  newBytes = newBytes >>> 0;
+
+  // Updating size
+  if (newBytes > oldBytes)
+    this.size++;
+  else if (newBytes < oldBytes)
+    this.size--;
+
+  return this;
+};
+
+/**
+ * Method used to apply the growing policy.
+ *
+ * @param  {number} [override] - Override capacity.
+ * @return {number}
+ */
+BitVector.prototype.applyPolicy = function(override) {
+  var newCapacity = this.policy(override || this.capacity);
+
+  if (typeof newCapacity !== 'number' || newCapacity < 0)
+    throw new Error('mnemonist/bit-vector.applyPolicy: policy returned an invalid value (expecting a positive integer).');
+
+  if (newCapacity <= this.capacity)
+    throw new Error('mnemonist/bit-vector.applyPolicy: policy returned a less or equal capacity to allocate.');
+
+  // TODO: we should probably check that the returned number is an integer
+
+  // Ceil to nearest 32
+  return Math.ceil(newCapacity / 32) * 32;
+};
+
+/**
+ * Method used to reallocate the underlying array.
+ *
+ * @param  {number}       capacity - Target capacity.
+ * @return {BitVector}
+ */
+BitVector.prototype.reallocate = function(capacity) {
+  var virtualCapacity = capacity;
+
+  capacity = Math.ceil(capacity / 32) * 32;
+
+  if (virtualCapacity < this.length)
+    this.length = virtualCapacity;
+
+  if (capacity === this.capacity)
+    return this;
+
+  var oldArray = this.array;
+
+  var storageLength = capacity / 32;
+
+  if (storageLength === this.array.length)
+    return this;
+
+  if (storageLength > this.array.length) {
+    this.array = new Uint32Array(storageLength);
+    this.array.set(oldArray, 0);
+  }
+  else {
+    this.array = oldArray.slice(0, storageLength);
+  }
+
+  this.capacity = capacity;
+
+  return this;
+};
+
+/**
+ * Method used to grow the array.
+ *
+ * @param  {number}       [capacity] - Optional capacity to match.
+ * @return {BitVector}
+ */
+BitVector.prototype.grow = function(capacity) {
+  var newCapacity;
+
+  if (typeof capacity === 'number') {
+
+    if (this.capacity >= capacity)
+      return this;
+
+    // We need to match the given capacity
+    newCapacity = this.capacity;
+
+    while (newCapacity < capacity)
+      newCapacity = this.applyPolicy(newCapacity);
+
+    this.reallocate(newCapacity);
+
+    return this;
+  }
+
+  // We need to run the policy once
+  newCapacity = this.applyPolicy();
+  this.reallocate(newCapacity);
+
+  return this;
+};
+
+/**
+ * Method used to resize the array. Won't deallocate.
+ *
+ * @param  {number}       length - Target length.
+ * @return {BitVector}
+ */
+BitVector.prototype.resize = function(length) {
+  if (length === this.length)
+    return this;
+
+  if (length < this.length) {
+    this.length = length;
+    return this;
+  }
+
+  this.length = length;
+  this.reallocate(length);
+
+  return this;
+};
+
+/**
+ * Method used to push a value in the set.
+ *
+ * @param  {number|boolean} value
+ * @return {BitVector}
+ */
+BitVector.prototype.push = function(value) {
+  if (this.capacity === this.length)
+    this.grow();
+
+  if (value === 0 || value === false)
+    return ++this.length;
+
+  this.size++;
+
+  var index = this.length++,
+      byteIndex = index >> 5,
+      pos = index & 0x0000001f;
+
+  this.array[byteIndex] |= (1 << pos);
+
+  return this.length;
+};
+
+/**
+ * Method used to pop the last value of the set.
+ *
+ * @return {number} - The popped value.
+ */
+BitVector.prototype.pop = function() {
+  if (this.length === 0)
+    return;
+
+  var index = --this.length;
+
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f;
+
+  return (this.array[byteIndex] >> pos) & 1;
+};
+
+/**
+ * Method used to get the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {number}
+ */
+BitVector.prototype.get = function(index) {
+  if (this.length < index)
+    return undefined;
+
+  var byteIndex = index >> 5,
+      pos = index & 0x0000001f;
+
+  return (this.array[byteIndex] >> pos) & 1;
+};
+
+/**
+ * Method used to test the given bit's value.
+ *
+ * @param  {number} index - Target bit index.
+ * @return {BitVector}
+ */
+BitVector.prototype.test = function(index) {
+  if (this.length < index)
+    return false;
+
+  return Boolean(this.get(index));
+};
+
+/**
+ * Method used to return the number of 1 from the beginning of the set up to
+ * the ith index.
+ *
+ * @param  {number} i - Ith index (cannot be > length).
+ * @return {number}
+ */
+BitVector.prototype.rank = function(i) {
+  if (this.size === 0)
+    return 0;
+
+  var byteIndex = i >> 5,
+      pos = i & 0x0000001f,
+      r = 0;
+
+  // Accessing the bytes before the last one
+  for (var j = 0; j < byteIndex; j++)
+    r += bitwise.table8Popcount(this.array[j]);
+
+  // Handling masked last byte
+  var maskedByte = this.array[byteIndex] & ((1 << pos) - 1);
+
+  r += bitwise.table8Popcount(maskedByte);
+
+  return r;
+};
+
+/**
+ * Method used to return the position of the rth 1 in the set or -1 if the
+ * set is empty.
+ *
+ * Note: usually select is implemented using binary search over rank but I
+ * tend to think the following linear implementation is faster since here
+ * rank is O(n) anyway.
+ *
+ * @param  {number} r - Rth 1 to select (should be < length).
+ * @return {number}
+ */
+BitVector.prototype.select = function(r) {
+  if (this.size === 0)
+    return -1;
+
+  // TODO: throw?
+  if (r >= this.length)
+    return -1;
+
+  var byte,
+      b = 32,
+      p = 0,
+      c = 0;
+
+  for (var i = 0, l = this.array.length; i < l; i++) {
+    byte = this.array[i];
+
+    // The byte is empty, let's continue
+    if (byte === 0)
+      continue;
+
+    // TODO: This branching might not be useful here
+    if (i === l - 1)
+      b = this.length % 32 || 32;
+
+    // TODO: popcount should speed things up here
+
+    for (var j = 0; j < b; j++, p++) {
+      c += (byte >> j) & 1;
+
+      if (c === r)
+        return p;
+    }
+  }
+};
+
+/**
+ * Method used to iterate over the bit set's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+BitVector.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var length = this.length,
+      byte,
+      bit,
+      b = 32;
+
+  for (var i = 0, l = this.array.length; i < l; i++) {
+    byte = this.array[i];
+
+    if (i === l - 1)
+      b = length % 32 || 32;
+
+    for (var j = 0; j < b; j++) {
+      bit = (byte >> j) & 1;
+
+      callback.call(scope, bit, i * 32 + j);
+    }
+  }
+};
+
+/**
+ * Method used to create an iterator over a set's values.
+ *
+ * @return {Iterator}
+ */
+BitVector.prototype.values = function() {
+  var length = this.length,
+      inner = false,
+      byte,
+      bit,
+      array = this.array,
+      l = array.length,
+      i = 0,
+      j = -1,
+      b = 32;
+
+  return new Iterator(function next() {
+    if (!inner) {
+
+      if (i >= l)
+        return {
+          done: true
+        };
+
+      if (i === l - 1)
+        b = length % 32 || 32;
+
+      byte = array[i++];
+      inner = true;
+      j = -1;
+    }
+
+    j++;
+
+    if (j >= b) {
+      inner = false;
+      return next();
+    }
+
+    bit = (byte >> j) & 1;
+
+    return {
+      value: bit
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a set's entries.
+ *
+ * @return {Iterator}
+ */
+BitVector.prototype.entries = function() {
+  var length = this.length,
+      inner = false,
+      byte,
+      bit,
+      array = this.array,
+      index,
+      l = array.length,
+      i = 0,
+      j = -1,
+      b = 32;
+
+  return new Iterator(function next() {
+    if (!inner) {
+
+      if (i >= l)
+        return {
+          done: true
+        };
+
+      if (i === l - 1)
+        b = length % 32 || 32;
+
+      byte = array[i++];
+      inner = true;
+      j = -1;
+    }
+
+    j++;
+    index = (~-i) * 32 + j;
+
+    if (j >= b) {
+      inner = false;
+      return next();
+    }
+
+    bit = (byte >> j) & 1;
+
+    return {
+      value: [index, bit]
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  BitVector.prototype[Symbol.iterator] = BitVector.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+BitVector.prototype.inspect = function() {
+  var proxy = new Uint8Array(this.length);
+
+  this.forEach(function(bit, i) {
+    proxy[i] = bit;
+  });
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: BitVector,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  BitVector.prototype[Symbol.for('nodejs.util.inspect.custom')] = BitVector.prototype.inspect;
+
+BitVector.prototype.toJSON = function() {
+  return Array.from(this.array.slice(0, (this.length >> 5) + 1));
+};
+
+/**
+ * Exporting.
+ */
+module.exports = BitVector;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f158dfd3c8ca7426d21244643695406892f3de79
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.d.ts
@@ -0,0 +1,24 @@
+/**
+ * Mnemonist BKTree Typings
+ * =========================
+ */
+type DistanceFunction<T> = (a: T, b: T) => number;
+
+export default class BKTree<T> {
+  
+  // Members
+  distance: DistanceFunction<T>;
+  size: number;
+
+  // Constructor
+  constructor(distance: DistanceFunction<T>);
+
+  // Methods
+  add(item: T): this;
+  search(n: number, query: T): Array<{item: T, distance: number}>;
+  toJSON(): object;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}, distance: DistanceFunction<I>): BKTree<I>;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.js b/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c9792d55f1de240c173d898bb203e0e268ddf99
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bk-tree.js
@@ -0,0 +1,180 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist BK Tree
+ * ==================
+ *
+ * Implementation of a Burkhard-Keller tree, allowing fast lookups of words
+ * that lie within a specified distance of the query word.
+ *
+ * [Reference]:
+ * https://en.wikipedia.org/wiki/BK-tree
+ *
+ * [Article]:
+ * W. Burkhard and R. Keller. Some approaches to best-match file searching,
+ * CACM, 1973
+ */
+var forEach = require('obliterator/foreach');
+
+/**
+ * BK Tree.
+ *
+ * @constructor
+ * @param {function} distance - Distance function to use.
+ */
+function BKTree(distance) {
+
+  if (typeof distance !== 'function')
+    throw new Error('mnemonist/BKTree.constructor: given `distance` should be a function.');
+
+  this.distance = distance;
+  this.clear();
+}
+
+/**
+ * Method used to add an item to the tree.
+ *
+ * @param  {any} item - Item to add.
+ * @return {BKTree}
+ */
+BKTree.prototype.add = function(item) {
+
+  // Initializing the tree with the first given word
+  if (!this.root) {
+    this.root = {
+      item: item,
+      children: {}
+    };
+
+    this.size++;
+    return this;
+  }
+
+  var node = this.root,
+      d;
+
+  while (true) {
+    d = this.distance(item, node.item);
+
+    if (!node.children[d])
+      break;
+
+    node = node.children[d];
+  }
+
+  node.children[d] = {
+    item: item,
+    children: {}
+  };
+
+  this.size++;
+  return this;
+};
+
+/**
+ * Method used to query the tree.
+ *
+ * @param  {number} n     - Maximum distance between query & item.
+ * @param  {any}    query - Query
+ * @return {BKTree}
+ */
+BKTree.prototype.search = function(n, query) {
+  if (!this.root)
+    return [];
+
+  var found = [],
+      stack = [this.root],
+      node,
+      child,
+      d,
+      i,
+      l;
+
+  while (stack.length) {
+    node = stack.pop();
+    d = this.distance(query, node.item);
+
+    if (d <= n)
+      found.push({item: node.item, distance: d});
+
+    for (i = d - n, l = d + n + 1; i < l; i++) {
+      child = node.children[i];
+
+      if (child)
+        stack.push(child);
+    }
+  }
+
+  return found;
+};
+
+/**
+ * Method used to clear the tree.
+ *
+ * @return {undefined}
+ */
+BKTree.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.root = null;
+};
+
+/**
+ * Convenience known methods.
+ */
+BKTree.prototype.toJSON = function() {
+  return this.root;
+};
+
+BKTree.prototype.inspect = function() {
+  var array = [],
+      stack = [this.root],
+      node,
+      d;
+
+  while (stack.length) {
+    node = stack.pop();
+
+    if (!node)
+      continue;
+
+    array.push(node.item);
+
+    for (d in node.children)
+      stack.push(node.children[d]);
+  }
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: BKTree,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  BKTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = BKTree.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a tree.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} distance - Distance function.
+ * @return {Heap}
+ */
+BKTree.from = function(iterable, distance) {
+  var tree = new BKTree(distance);
+
+  forEach(iterable, function(value) {
+    tree.add(value);
+  });
+
+  return tree;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = BKTree;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dc9b2fa2b0d04119f263510e8ffa811b0b551dc2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Mnemonist BloomFilter Typings
+ * ==============================
+ */
+type BloomFilterOptions = {
+  capacity: number;
+  errorRate?: number;
+}
+
+export default class BloomFilter {
+
+  // Members
+  capacity: number;
+  errorRate: number;
+  hashFunctions: number;
+
+  // Constructor
+  constructor(capacity: number);
+  constructor(options: BloomFilterOptions);
+
+  // Methods
+  clear(): void;
+  add(string: string): this;
+  test(string: string): boolean;
+  toJSON(): Uint8Array;
+
+  // Statics
+  from(iterable: Iterable<string>, options?: number | BloomFilterOptions): BloomFilter;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.js b/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba3ee7669f6751da82ddece06485aa1408b1d23f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/bloom-filter.js
@@ -0,0 +1,186 @@
+/**
+ * Mnemonist Bloom Filter
+ * =======================
+ *
+ * Bloom Filter implementation relying on MurmurHash3.
+ */
+var murmurhash3 = require('./utils/murmurhash3.js'),
+    forEach = require('obliterator/foreach');
+
+/**
+ * Constants.
+ */
+var LN2_SQUARED = Math.LN2 * Math.LN2;
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  errorRate: 0.005
+};
+
+/**
+ * Function used to convert a string into a Uint16 byte array.
+ *
+ * @param  {string}      string - Target string.
+ * @return {Uint16Array}
+ */
+function stringToByteArray(string) {
+  var array = new Uint16Array(string.length),
+      i,
+      l;
+
+  for (i = 0, l = string.length; i < l; i++)
+    array[i] = string.charCodeAt(i);
+
+  return array;
+}
+
+/**
+ * Function used to hash the given byte array.
+ *
+ * @param  {number}      length - Length of the filter's byte array.
+ * @param  {number}      seed   - Seed to use for the hash function.
+ * @param  {Uint16Array}        - Byte array representing the string.
+ * @return {number}             - The hash.
+ *
+ * @note length * 8 should probably already be computed as well as seeds.
+ */
+function hashArray(length, seed, array) {
+  var hash = murmurhash3((seed * 0xFBA4C795) & 0xFFFFFFFF, array);
+
+  return hash % (length * 8);
+}
+
+/**
+ * Bloom Filter.
+ *
+ * @constructor
+ * @param {number|object} capacityOrOptions - Capacity or options.
+ */
+function BloomFilter(capacityOrOptions) {
+  var options = {};
+
+  if (!capacityOrOptions)
+    throw new Error('mnemonist/BloomFilter.constructor: a BloomFilter must be created with a capacity.');
+
+  if (typeof capacityOrOptions === 'object')
+    options = capacityOrOptions;
+  else
+    options.capacity = capacityOrOptions;
+
+  // Handling capacity
+  if (typeof options.capacity !== 'number' || options.capacity <= 0)
+    throw new Error('mnemonist/BloomFilter.constructor: `capacity` option should be a positive integer.');
+
+  this.capacity = options.capacity;
+
+  // Handling error rate
+  this.errorRate = options.errorRate || DEFAULTS.errorRate;
+
+  if (typeof this.errorRate !== 'number' || options.errorRate <= 0)
+    throw new Error('mnemonist/BloomFilter.constructor: `errorRate` option should be a positive float.');
+
+  this.clear();
+}
+
+/**
+ * Method used to clear the filter.
+ *
+ * @return {undefined}
+ */
+BloomFilter.prototype.clear = function() {
+
+  // Optimizing number of bits & number of hash functions
+  var bits = -1 / LN2_SQUARED * this.capacity * Math.log(this.errorRate),
+      length = (bits / 8) | 0;
+
+  this.hashFunctions = (length * 8 / this.capacity * Math.LN2) | 0;
+
+  // Creating the data array
+  this.data = new Uint8Array(length);
+
+  return;
+};
+
+/**
+ * Method used to add an string to the filter.
+ *
+ * @param  {string} string - Item to add.
+ * @return {BloomFilter}
+ *
+ * @note Should probably create a hash function working directly on a string.
+ */
+BloomFilter.prototype.add = function(string) {
+
+  // Converting the string to a byte array
+  var array = stringToByteArray(string);
+
+  // Applying the n hash functions
+  for (var i = 0, l = this.hashFunctions; i < l; i++) {
+    var index = hashArray(this.data.length, i, array),
+        position = (1 << (7 & index));
+
+    this.data[index >> 3] |= position;
+  }
+
+  return this;
+};
+
+/**
+ * Method used to test the given string.
+ *
+ * @param  {string} string - Item to test.
+ * @return {boolean}
+ */
+BloomFilter.prototype.test = function(string) {
+
+  // Converting the string to a byte array
+  var array = stringToByteArray(string);
+
+  // Applying the n hash functions
+  for (var i = 0, l = this.hashFunctions; i < l; i++) {
+    var index = hashArray(this.data.length, i, array);
+
+    if (!(this.data[index >> 3] & (1 << (7 & index))))
+      return false;
+  }
+
+  return true;
+};
+
+/**
+ * Convenience known methods.
+ */
+BloomFilter.prototype.toJSON = function() {
+  return this.data;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a filter.
+ *
+ * @param  {Iterable}    iterable - Target iterable.
+ * @return {BloomFilter}
+ */
+BloomFilter.from = function(iterable, options) {
+  if (!options) {
+    options = iterable.length || iterable.size;
+
+    if (typeof options !== 'number')
+      throw new Error('BloomFilter.from: could not infer the filter\'s capacity. Try passing it as second argument.');
+  }
+
+  var filter = new BloomFilter(options);
+
+  forEach(iterable, function(value) {
+    filter.add(value);
+  });
+
+  return filter;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = BloomFilter;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ec1fa4cba6286275bb425162747fb68775202820
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.d.ts
@@ -0,0 +1,34 @@
+/**
+ * Mnemonist CircularBuffer Typings
+ * =================================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+export default class CircularBuffer<T> implements Iterable<T> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  unshift(item: T): number;
+  pop(): T | undefined;
+  shift(): T | undefined;
+  peekFirst(): T | undefined;
+  peekLast(): T | undefined;
+  get(index: number): T | undefined;
+  forEach(callback: (item: T, index: number, buffer: this) => void, scope?: any): void;
+  toArray(): Iterable<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}, ArrayClass: IArrayLikeConstructor, capacity?: number): CircularBuffer<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.js b/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3ef95029ee6a58eb515799980dc176e268279bf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/circular-buffer.js
@@ -0,0 +1,131 @@
+/**
+ * Mnemonist CircularBuffer
+ * =========================
+ *
+ * Circular buffer implementation fit to use as a finite deque.
+ */
+var iterables = require('./utils/iterables.js'),
+    FixedDeque = require('./fixed-deque');
+
+/**
+ * CircularBuffer.
+ *
+ * @constructor
+ */
+function CircularBuffer(ArrayClass, capacity) {
+
+  if (arguments.length < 2)
+    throw new Error('mnemonist/circular-buffer: expecting an Array class and a capacity.');
+
+  if (typeof capacity !== 'number' || capacity <= 0)
+    throw new Error('mnemonist/circular-buffer: `capacity` should be a positive number.');
+
+  this.ArrayClass = ArrayClass;
+  this.capacity = capacity;
+  this.items = new ArrayClass(this.capacity);
+  this.clear();
+}
+
+/**
+ * Pasting most of the prototype from FixedDeque.
+ */
+function paste(name) {
+  CircularBuffer.prototype[name] = FixedDeque.prototype[name];
+}
+
+Object.keys(FixedDeque.prototype).forEach(paste);
+
+if (typeof Symbol !== 'undefined')
+  Object.getOwnPropertySymbols(FixedDeque.prototype).forEach(paste);
+
+/**
+ * Method used to append a value to the buffer.
+ *
+ * @param  {any}    item - Item to append.
+ * @return {number}      - Returns the new size of the buffer.
+ */
+CircularBuffer.prototype.push = function(item) {
+  var index = (this.start + this.size) % this.capacity;
+
+  this.items[index] = item;
+
+  // Overwriting?
+  if (this.size === this.capacity) {
+
+    // If start is at the end, we wrap around the buffer
+    this.start = (index + 1) % this.capacity;
+
+    return this.size;
+  }
+
+  return ++this.size;
+};
+
+/**
+ * Method used to prepend a value to the buffer.
+ *
+ * @param  {any}    item - Item to prepend.
+ * @return {number}      - Returns the new size of the buffer.
+ */
+CircularBuffer.prototype.unshift = function(item) {
+  var index = this.start - 1;
+
+  if (this.start === 0)
+    index = this.capacity - 1;
+
+  this.items[index] = item;
+
+  // Overwriting
+  if (this.size === this.capacity) {
+
+    this.start = index;
+
+    return this.size;
+  }
+
+  this.start = index;
+
+  return ++this.size;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a circular buffer.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {number}   capacity   - Desired capacity.
+ * @return {FiniteStack}
+ */
+CircularBuffer.from = function(iterable, ArrayClass, capacity) {
+  if (arguments.length < 3) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/circular-buffer.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+
+  var buffer = new CircularBuffer(ArrayClass, capacity);
+
+  if (iterables.isArrayLike(iterable)) {
+    var i, l;
+
+    for (i = 0, l = iterable.length; i < l; i++)
+      buffer.items[i] = iterable[i];
+
+    buffer.size = l;
+
+    return buffer;
+  }
+
+  iterables.forEach(iterable, function(value) {
+    buffer.push(value);
+  });
+
+  return buffer;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = CircularBuffer;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/critbit-tree-map.js b/libs/shared/graph-layout/node_modules/mnemonist/critbit-tree-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c41a9a5410b838ffc28249135e240ee0fe4e04f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/critbit-tree-map.js
@@ -0,0 +1,515 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist CritBitTreeMap
+ * =========================
+ *
+ * JavaScript implementation of a crit-bit tree, also called PATRICIA tree.
+ * This tree is a basically a bitwise radix tree and is supposedly much more
+ * efficient than a standard Trie.
+ *
+ * [References]:
+ * https://cr.yp.to/critbit.html
+ * https://www.imperialviolet.org/binary/critbit.pdf
+ */
+var bitwise = require('./utils/bitwise.js');
+
+/**
+ * Helpers.
+ */
+
+/**
+ * Helper returning the direction we need to take given a key and an
+ * encoded critbit.
+ *
+ * @param  {string} key     - Target key.
+ * @param  {number} critbit - Packed address of byte + mask.
+ * @return {number}         - 0, left or 1, right.
+ */
+function getDirection(key, critbit) {
+  var byteIndex = critbit >> 8;
+
+  if (byteIndex > key.length - 1)
+    return 0;
+
+  var byte = key.charCodeAt(byteIndex),
+      mask = critbit & 0xff;
+
+  return (1 + (byte | mask)) >> 8;
+}
+
+/**
+ * Helper returning the packed address of byte + mask or -1 if strings
+ * are identical.
+ *
+ * @param  {string} a      - First key.
+ * @param  {string} b      - Second key.
+ * @return {number}        - Packed address of byte + mask.
+ */
+function findCriticalBit(a, b) {
+  var i = 0,
+      tmp;
+
+  // Swapping so a is the shortest
+  if (a.length > b.length) {
+    tmp = b;
+    b = a;
+    a = tmp;
+  }
+
+  var l = a.length,
+      mask;
+
+  while (i < l) {
+    if (a[i] !== b[i]) {
+      mask = bitwise.criticalBit8Mask(
+        a.charCodeAt(i),
+        b.charCodeAt(i)
+      );
+
+      return (i << 8) | mask;
+    }
+
+    i++;
+  }
+
+  // Strings are identical
+  if (a.length === b.length)
+    return -1;
+
+  // NOTE: x ^ 0 is the same as x
+  mask = bitwise.criticalBit8Mask(b.charCodeAt(i));
+
+  return (i << 8) | mask;
+}
+
+/**
+ * Class representing a crit-bit tree's internal node.
+ *
+ * @constructor
+ * @param {number} critbit - Packed address of byte + mask.
+ */
+function InternalNode(critbit) {
+  this.critbit = critbit;
+  this.left = null;
+  this.right = null;
+}
+
+/**
+ * Class representing a crit-bit tree's external node.
+ * Note that it is possible to replace those nodes by flat arrays.
+ *
+ * @constructor
+ * @param {string} key   - Node's key.
+ * @param {any}    value - Arbitrary value.
+ */
+function ExternalNode(key, value) {
+  this.key = key;
+  this.value = value;
+}
+
+/**
+ * CritBitTreeMap.
+ *
+ * @constructor
+ */
+function CritBitTreeMap() {
+
+  // Properties
+  this.root = null;
+  this.size = 0;
+
+  this.clear();
+}
+
+/**
+ * Method used to clear the CritBitTreeMap.
+ *
+ * @return {undefined}
+ */
+CritBitTreeMap.prototype.clear = function() {
+
+  // Properties
+  this.root = null;
+  this.size = 0;
+};
+
+/**
+ * Method used to set the value of the given key in the trie.
+ *
+ * @param  {string}         key   - Key to set.
+ * @param  {any}            value - Arbitrary value.
+ * @return {CritBitTreeMap}
+ */
+CritBitTreeMap.prototype.set = function(key, value) {
+
+  // Tree is empty
+  if (this.size === 0) {
+    this.root = new ExternalNode(key, value);
+    this.size++;
+
+    return this;
+  }
+
+  // Walk state
+  var node = this.root,
+      ancestors = [],
+      path = [],
+      ancestor,
+      parent,
+      child,
+      critbit,
+      internal,
+      left,
+      leftPath,
+      best,
+      dir,
+      i,
+      l;
+
+  // Walking the tree
+  while (true) {
+
+    // Traversing an internal node
+    if (node instanceof InternalNode) {
+      dir = getDirection(key, node.critbit);
+
+      // Going left & creating key if not yet there
+      if (dir === 0) {
+        if (!node.left) {
+          node.left = new ExternalNode(key, value);
+          return this;
+        }
+
+        ancestors.push(node);
+        path.push(true);
+
+        node = node.left;
+      }
+
+      // Going right & creating key if not yet there
+      else {
+        if (!node.right) {
+          node.right = new ExternalNode(key, value);
+          return this;
+        }
+
+        ancestors.push(node);
+        path.push(false);
+
+        node = node.right;
+      }
+    }
+
+    // Reaching an external node
+    else {
+
+      // 1. Creating a new external node
+      critbit = findCriticalBit(key, node.key);
+
+      // Key is identical, we just replace the value
+      if (critbit === -1) {
+        node.value = value;
+        return this;
+      }
+
+      this.size++;
+
+      internal = new InternalNode(critbit);
+
+      left = getDirection(key, critbit) === 0;
+
+      // TODO: maybe setting opposite pointer is not necessary
+      if (left) {
+        internal.left = new ExternalNode(key, value);
+        internal.right = node;
+      }
+      else {
+        internal.left = node;
+        internal.right = new ExternalNode(key, value);
+      }
+
+      // 2. Bubbling up
+      best = -1;
+      l = ancestors.length;
+
+      for (i = l - 1; i >= 0; i--) {
+        ancestor = ancestors[i];
+
+        if (ancestor.critbit > critbit)
+          continue;
+
+        best = i;
+        break;
+      }
+
+      // Do we need to attach to the root?
+      if (best < 0) {
+        this.root = internal;
+
+        // Need to rewire parent as child?
+        if (l > 0) {
+          parent = ancestors[0];
+
+          if (left)
+            internal.right = parent;
+          else
+            internal.left = parent;
+        }
+      }
+
+      // Simple case without rotation
+      else if (best === l - 1) {
+        parent = ancestors[best];
+        leftPath = path[best];
+
+        if (leftPath)
+          parent.left = internal;
+        else
+          parent.right = internal;
+      }
+
+      // Full rotation
+      else {
+        parent = ancestors[best];
+        leftPath = path[best];
+        child = ancestors[best + 1];
+
+        if (leftPath)
+          parent.left = internal;
+        else
+          parent.right = internal;
+
+        if (left)
+          internal.right = child;
+        else
+          internal.left = child;
+      }
+
+      return this;
+    }
+  }
+};
+
+/**
+ * Method used to get the value attached to the given key in the tree or
+ * undefined if not found.
+ *
+ * @param  {string} key   - Key to get.
+ * @return {any}
+ */
+CritBitTreeMap.prototype.get = function(key) {
+
+  // Walk state
+  var node = this.root,
+      dir;
+
+  // Walking the tree
+  while (true) {
+
+    // Dead end
+    if (node === null)
+      return;
+
+    // Traversing an internal node
+    if (node instanceof InternalNode) {
+      dir = getDirection(key, node.critbit);
+
+      node = dir ? node.right : node.left;
+    }
+
+    // Reaching an external node
+    else {
+      if (node.key !== key)
+        return;
+
+      return node.value;
+    }
+  }
+};
+
+/**
+ * Method used to return whether the given key exists in the tree.
+ *
+ * @param  {string} key - Key to test.
+ * @return {boolean}
+ */
+CritBitTreeMap.prototype.has = function(key) {
+
+  // Walk state
+  var node = this.root,
+      dir;
+
+  // Walking the tree
+  while (true) {
+
+    // Dead end
+    if (node === null)
+      return false;
+
+    // Traversing an internal node
+    if (node instanceof InternalNode) {
+      dir = getDirection(key, node.critbit);
+
+      node = dir ? node.right : node.left;
+    }
+
+    // Reaching an external node
+    else {
+      return node.key === key;
+    }
+  }
+};
+
+/**
+ * Method used to delete the given key from the tree and return whether the
+ * key did exist or not.
+ *
+ * @param  {string} key - Key to delete.
+ * @return {boolean}
+ */
+CritBitTreeMap.prototype.delete = function(key) {
+
+  // Walk state
+  var node = this.root,
+      dir;
+
+  var parent = null,
+      grandParent = null,
+      wentLeftForParent = false,
+      wentLeftForGrandparent = false;
+
+  // Walking the tree
+  while (true) {
+
+    // Dead end
+    if (node === null)
+      return false;
+
+    // Traversing an internal node
+    if (node instanceof InternalNode) {
+      dir = getDirection(key, node.critbit);
+
+      if (dir === 0) {
+        grandParent = parent;
+        wentLeftForGrandparent = wentLeftForParent;
+        parent = node;
+        wentLeftForParent = true;
+
+        node = node.left;
+      }
+      else {
+        grandParent = parent;
+        wentLeftForGrandparent = wentLeftForParent;
+        parent = node;
+        wentLeftForParent = false;
+
+        node = node.right;
+      }
+    }
+
+    // Reaching an external node
+    else {
+      if (key !== node.key)
+        return false;
+
+      this.size--;
+
+      // Rewiring
+      if (parent === null) {
+        this.root = null;
+      }
+
+      else if (grandParent === null) {
+        if (wentLeftForParent)
+          this.root = parent.right;
+        else
+          this.root = parent.left;
+      }
+
+      else {
+        if (wentLeftForGrandparent) {
+          if (wentLeftForParent) {
+            grandParent.left = parent.right;
+          }
+          else {
+            grandParent.left = parent.left;
+          }
+        }
+        else {
+          if (wentLeftForParent) {
+            grandParent.right = parent.right;
+          }
+          else {
+            grandParent.right = parent.left;
+          }
+        }
+      }
+
+      return true;
+    }
+  }
+};
+
+/**
+ * Method used to iterate over the tree in key order.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+CritBitTreeMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  // Inorder traversal of the tree
+  var current = this.root,
+      stack = [];
+
+  while (true) {
+
+    if (current !== null) {
+      stack.push(current);
+
+      current = current instanceof InternalNode ? current.left : null;
+    }
+
+    else {
+      if (stack.length > 0) {
+        current = stack.pop();
+
+        if (current instanceof ExternalNode)
+          callback.call(scope, current.value, current.key);
+
+        current = current instanceof InternalNode ? current.right : null;
+      }
+      else {
+        break;
+      }
+    }
+  }
+};
+
+/**
+ * Convenience known methods.
+ */
+CritBitTreeMap.prototype.inspect = function() {
+  return this;
+};
+
+if (typeof Symbol !== 'undefined')
+  CritBitTreeMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = CritBitTreeMap.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a CritBitTreeMap.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {CritBitTreeMap}
+ */
+// CritBitTreeMap.from = function(iterable) {
+
+// };
+
+/**
+ * Exporting.
+ */
+module.exports = CritBitTreeMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/default-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/default-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..186878c60eb2eb582c2f65652d97d04059cf0960
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/default-map.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Mnemonist DefaultMap Typings
+ * =============================
+ */
+export default class DefaultMap<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(factory: (key: K, index: number) => V);
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  has(key: K): boolean;
+  get(key: K): V;
+  peek(key: K): V | undefined;
+  forEach(callback: (value: V, key: K, map: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+
+  // Statics
+  static autoIncrement(): number;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/default-map.js b/libs/shared/graph-layout/node_modules/mnemonist/default-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbe41d776b7af7173cad26313f020f2bb72befb6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/default-map.js
@@ -0,0 +1,162 @@
+/**
+ * Mnemonist DefaultMap
+ * =====================
+ *
+ * JavaScript implementation of a default map that will return a constructed
+ * value any time one tries to access an inexisting key. It's quite similar
+ * to python's defaultdict.
+ */
+
+/**
+ * DefaultMap.
+ *
+ * @constructor
+ */
+function DefaultMap(factory) {
+  if (typeof factory !== 'function')
+    throw new Error('mnemonist/DefaultMap.constructor: expecting a function.');
+
+  this.items = new Map();
+  this.factory = factory;
+  this.size = 0;
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+DefaultMap.prototype.clear = function() {
+
+  // Properties
+  this.items.clear();
+  this.size = 0;
+};
+
+/**
+ * Method used to get the value set for given key. If the key does not exist,
+ * the value will be created using the provided factory.
+ *
+ * @param  {any} key - Target key.
+ * @return {any}
+ */
+DefaultMap.prototype.get = function(key) {
+  var value = this.items.get(key);
+
+  if (typeof value === 'undefined') {
+    value = this.factory(key, this.size);
+    this.items.set(key, value);
+    this.size++;
+  }
+
+  return value;
+};
+
+/**
+ * Method used to get the value set for given key. If the key does not exist,
+ * a value won't be created.
+ *
+ * @param  {any} key - Target key.
+ * @return {any}
+ */
+DefaultMap.prototype.peek = function(key) {
+  return this.items.get(key);
+};
+
+/**
+ * Method used to set a value for given key.
+ *
+ * @param  {any} key   - Target key.
+ * @param  {any} value - Value.
+ * @return {DefaultMap}
+ */
+DefaultMap.prototype.set = function(key, value) {
+  this.items.set(key, value);
+  this.size = this.items.size;
+
+  return this;
+};
+
+/**
+ * Method used to test the existence of a key in the map.
+ *
+ * @param  {any} key   - Target key.
+ * @return {boolean}
+ */
+DefaultMap.prototype.has = function(key) {
+  return this.items.has(key);
+};
+
+/**
+ * Method used to delete target key.
+ *
+ * @param  {any} key   - Target key.
+ * @return {boolean}
+ */
+DefaultMap.prototype.delete = function(key) {
+  var deleted = this.items.delete(key);
+
+  this.size = this.items.size;
+
+  return deleted;
+};
+
+/**
+ * Method used to iterate over each of the key/value pairs.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+DefaultMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  this.items.forEach(callback, scope);
+};
+
+/**
+ * Iterators.
+ */
+DefaultMap.prototype.entries = function() {
+  return this.items.entries();
+};
+
+DefaultMap.prototype.keys = function() {
+  return this.items.keys();
+};
+
+DefaultMap.prototype.values = function() {
+  return this.items.values();
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  DefaultMap.prototype[Symbol.iterator] = DefaultMap.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+DefaultMap.prototype.inspect = function() {
+  return this.items;
+};
+
+if (typeof Symbol !== 'undefined')
+  DefaultMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = DefaultMap.prototype.inspect;
+
+/**
+ * Typical factories.
+ */
+DefaultMap.autoIncrement = function() {
+  var i = 0;
+
+  return function() {
+    return i++;
+  };
+};
+
+/**
+ * Exporting.
+ */
+module.exports = DefaultMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..579a88379fcdee886218628a40e58e86c1730e22
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.d.ts
@@ -0,0 +1,18 @@
+/**
+ * Mnemonist DefaultWeakMap Typings
+ * ================================
+ */
+export default class DefaultWeakMap<K extends object, V> {
+
+  // Constructor
+  constructor(factory: (key: K) => V);
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  has(key: K): boolean;
+  get(key: K): V;
+  peek(key: K): V | undefined;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.js b/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa8931c2674f72a4f2d93874af223b6e57e97920
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/default-weak-map.js
@@ -0,0 +1,108 @@
+/**
+ * Mnemonist DefaultWeakMap
+ * =========================
+ *
+ * JavaScript implementation of a default weak map that will return a constructed
+ * value any time one tries to access an non-existing key. It is similar to
+ * DefaultMap but uses ES6 WeakMap that only holds weak reference to keys.
+ */
+
+/**
+ * DefaultWeakMap.
+ *
+ * @constructor
+ */
+function DefaultWeakMap(factory) {
+  if (typeof factory !== 'function')
+    throw new Error('mnemonist/DefaultWeakMap.constructor: expecting a function.');
+
+  this.items = new WeakMap();
+  this.factory = factory;
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+DefaultWeakMap.prototype.clear = function() {
+
+  // Properties
+  this.items = new WeakMap();
+};
+
+/**
+ * Method used to get the value set for given key. If the key does not exist,
+ * the value will be created using the provided factory.
+ *
+ * @param  {any} key - Target key.
+ * @return {any}
+ */
+DefaultWeakMap.prototype.get = function(key) {
+  var value = this.items.get(key);
+
+  if (typeof value === 'undefined') {
+    value = this.factory(key);
+    this.items.set(key, value);
+  }
+
+  return value;
+};
+
+/**
+ * Method used to get the value set for given key. If the key does not exist,
+ * a value won't be created.
+ *
+ * @param  {any} key - Target key.
+ * @return {any}
+ */
+DefaultWeakMap.prototype.peek = function(key) {
+  return this.items.get(key);
+};
+
+/**
+ * Method used to set a value for given key.
+ *
+ * @param  {any} key   - Target key.
+ * @param  {any} value - Value.
+ * @return {DefaultMap}
+ */
+DefaultWeakMap.prototype.set = function(key, value) {
+  this.items.set(key, value);
+  return this;
+};
+
+/**
+ * Method used to test the existence of a key in the map.
+ *
+ * @param  {any} key   - Target key.
+ * @return {boolean}
+ */
+DefaultWeakMap.prototype.has = function(key) {
+  return this.items.has(key);
+};
+
+/**
+ * Method used to delete target key.
+ *
+ * @param  {any} key   - Target key.
+ * @return {boolean}
+ */
+DefaultWeakMap.prototype.delete = function(key) {
+  return this.items.delete(key);
+};
+
+/**
+ * Convenience known methods.
+ */
+DefaultWeakMap.prototype.inspect = function() {
+  return this.items;
+};
+
+if (typeof Symbol !== 'undefined')
+  DefaultWeakMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = DefaultWeakMap.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = DefaultWeakMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb15ab003e4d4a56db0dd468b958103d6589c49c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.d.ts
@@ -0,0 +1,65 @@
+/**
+ * Mnemonist FibonacciHeap Typings
+ * ================================
+ */
+type FibonacciHeapComparator<T> = (a: T, b: T) => number;
+
+export default class FibonacciHeap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: FibonacciHeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  inspect(): any;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    comparator?: FibonacciHeapComparator<I>
+  ): FibonacciHeap<I>;
+}
+
+export class MinFibonacciHeap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: FibonacciHeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): FibonacciHeap<I>;
+}
+
+export class MaxFibonacciHeap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: FibonacciHeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): FibonacciHeap<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.js b/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.js
new file mode 100644
index 0000000000000000000000000000000000000000..f41334f8272b47d507e42e897c537abe560e3784
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fibonacci-heap.js
@@ -0,0 +1,320 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist Fibonacci Heap
+ * =========================
+ *
+ * Fibonacci heap implementation.
+ */
+var comparators = require('./utils/comparators.js'),
+    forEach = require('obliterator/foreach');
+
+var DEFAULT_COMPARATOR = comparators.DEFAULT_COMPARATOR,
+    reverseComparator = comparators.reverseComparator;
+
+/**
+ * Fibonacci Heap.
+ *
+ * @constructor
+ */
+function FibonacciHeap(comparator) {
+  this.clear();
+  this.comparator = comparator || DEFAULT_COMPARATOR;
+
+  if (typeof this.comparator !== 'function')
+    throw new Error('mnemonist/FibonacciHeap.constructor: given comparator should be a function.');
+}
+
+/**
+ * Method used to clear the heap.
+ *
+ * @return {undefined}
+ */
+FibonacciHeap.prototype.clear = function() {
+
+  // Properties
+  this.root = null;
+  this.min = null;
+  this.size = 0;
+};
+
+/**
+ * Function used to create a node.
+ *
+ * @param  {any}    item - Target item.
+ * @return {object}
+ */
+function createNode(item) {
+  return {
+    item: item,
+    degree: 0
+  };
+}
+
+/**
+ * Function used to merge the given node with the root list.
+ *
+ * @param {FibonacciHeap} heap - Target heap.
+ * @param {Node}          node - Target node.
+ */
+function mergeWithRoot(heap, node) {
+  if (!heap.root) {
+    heap.root = node;
+  }
+  else {
+    node.right = heap.root.right;
+    node.left = heap.root;
+    heap.root.right.left = node;
+    heap.root.right = node;
+  }
+}
+
+/**
+ * Method used to push an item into the heap.
+ *
+ * @param  {any}    item - Item to push.
+ * @return {number}
+ */
+FibonacciHeap.prototype.push = function(item) {
+  var node = createNode(item);
+  node.left = node;
+  node.right = node;
+  mergeWithRoot(this, node);
+
+  if (!this.min || this.comparator(node.item, this.min.item) <= 0)
+    this.min = node;
+
+  return ++this.size;
+};
+
+/**
+ * Method used to get the "first" item of the heap.
+ *
+ * @return {any}
+ */
+FibonacciHeap.prototype.peek = function() {
+  return this.min ? this.min.item : undefined;
+};
+
+/**
+ * Function used to consume the given linked list.
+ *
+ * @param {Node} head - Head node.
+ * @param {array}
+ */
+function consumeLinkedList(head) {
+  var nodes = [],
+      node = head,
+      flag = false;
+
+  while (true) {
+    if (node === head && flag)
+      break;
+    else if (node === head)
+      flag = true;
+
+    nodes.push(node);
+    node = node.right;
+  }
+
+  return nodes;
+}
+
+/**
+ * Function used to remove the target node from the root list.
+ *
+ * @param {FibonacciHeap} heap - Target heap.
+ * @param {Node}          node - Target node.
+ */
+function removeFromRoot(heap, node) {
+  if (heap.root === node)
+    heap.root = node.right;
+  node.left.right = node.right;
+  node.right.left = node.left;
+}
+
+/**
+ * Function used to merge the given node with the child list of a root node.
+ *
+ * @param {Node} parent - Parent node.
+ * @param {Node} node   - Target node.
+ */
+function mergeWithChild(parent, node) {
+  if (!parent.child) {
+    parent.child = node;
+  }
+  else {
+    node.right = parent.child.right;
+    node.left = parent.child;
+    parent.child.right.left = node;
+    parent.child.right = node;
+  }
+}
+
+/**
+ * Function used to link one node to another in the root list.
+ *
+ * @param {FibonacciHeap} heap - Target heap.
+ * @param {Node}          y - Y node.
+ * @param {Node}          x - X node.
+ */
+function link(heap, y, x) {
+  removeFromRoot(heap, y);
+  y.left = y;
+  y.right = y;
+  mergeWithChild(x, y);
+  x.degree++;
+  y.parent = x;
+}
+
+/**
+ * Function used to consolidate the heap.
+ *
+ * @param {FibonacciHeap} heap - Target heap.
+ */
+function consolidate(heap) {
+  var A = new Array(heap.size),
+      nodes = consumeLinkedList(heap.root),
+      i, l, x, y, d, t;
+
+  for (i = 0, l = nodes.length; i < l; i++) {
+    x = nodes[i];
+    d = x.degree;
+
+    while (A[d]) {
+      y = A[d];
+
+      if (heap.comparator(x.item, y.item) > 0) {
+        t = x;
+        x = y;
+        y = t;
+      }
+
+      link(heap, y, x);
+      A[d] = null;
+      d++;
+    }
+
+    A[d] = x;
+  }
+
+  for (i = 0; i < heap.size; i++) {
+    if (A[i] && heap.comparator(A[i].item, heap.min.item) <= 0)
+      heap.min = A[i];
+  }
+}
+
+/**
+ * Method used to retrieve & remove the "first" item of the heap.
+ *
+ * @return {any}
+ */
+FibonacciHeap.prototype.pop = function() {
+  if (!this.size)
+    return undefined;
+
+  var z = this.min;
+
+  if (z.child) {
+    var nodes = consumeLinkedList(z.child),
+        node,
+        i,
+        l;
+
+    for (i = 0, l = nodes.length; i < l; i++) {
+      node = nodes[i];
+
+      mergeWithRoot(this, node);
+      delete node.parent;
+    }
+  }
+
+  removeFromRoot(this, z);
+
+  if (z === z.right) {
+    this.min = null;
+    this.root = null;
+  }
+  else {
+    this.min = z.right;
+    consolidate(this);
+  }
+
+  this.size--;
+
+  return z.item;
+};
+
+/**
+ * Convenience known methods.
+ */
+FibonacciHeap.prototype.inspect = function() {
+  var proxy = {
+    size: this.size
+  };
+
+  if (this.min && 'item' in this.min)
+    proxy.top = this.min.item;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: FibonacciHeap,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  FibonacciHeap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FibonacciHeap.prototype.inspect;
+
+/**
+ * Fibonacci Maximum Heap.
+ *
+ * @constructor
+ */
+function MaxFibonacciHeap(comparator) {
+  this.clear();
+  this.comparator = comparator || DEFAULT_COMPARATOR;
+
+  if (typeof this.comparator !== 'function')
+    throw new Error('mnemonist/FibonacciHeap.constructor: given comparator should be a function.');
+
+  this.comparator = reverseComparator(this.comparator);
+}
+
+MaxFibonacciHeap.prototype = FibonacciHeap.prototype;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a heap.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} comparator - Custom comparator function.
+ * @return {FibonacciHeap}
+ */
+FibonacciHeap.from = function(iterable, comparator) {
+  var heap = new FibonacciHeap(comparator);
+
+  forEach(iterable, function(value) {
+    heap.push(value);
+  });
+
+  return heap;
+};
+
+MaxFibonacciHeap.from = function(iterable, comparator) {
+  var heap = new MaxFibonacciHeap(comparator);
+
+  forEach(iterable, function(value) {
+    heap.push(value);
+  });
+
+  return heap;
+};
+
+/**
+ * Exporting.
+ */
+FibonacciHeap.MinFibonacciHeap = FibonacciHeap;
+FibonacciHeap.MaxFibonacciHeap = MaxFibonacciHeap;
+module.exports = FibonacciHeap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-critbit-tree-map.js b/libs/shared/graph-layout/node_modules/mnemonist/fixed-critbit-tree-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..9658fee3a9c2391212336b44354ffb09d264aa02
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-critbit-tree-map.js
@@ -0,0 +1,427 @@
+/* eslint no-constant-condition: 0 */
+
+/* eslint-disable */
+
+/**
+ * Mnemonist FixedFixedCritBitTreeMap
+ * ===================================
+ *
+ * TODO...
+ *
+ * [References]:
+ * https://cr.yp.to/critbit.html
+ * https://www.imperialviolet.org/binary/critbit.pdf
+ */
+var bitwise = require('./utils/bitwise.js'),
+    typed = require('./utils/typed-arrays.js');
+
+/**
+ * Helpers.
+ */
+
+/**
+ * Helper returning the direction we need to take given a key and an
+ * encoded critbit.
+ *
+ * @param  {string} key     - Target key.
+ * @param  {number} critbit - Packed address of byte + mask.
+ * @return {number}         - 0, left or 1, right.
+ */
+function getDirection(key, critbit) {
+  var byteIndex = critbit >> 8;
+
+  if (byteIndex > key.length - 1)
+    return 0;
+
+  var byte = key.charCodeAt(byteIndex),
+      mask = critbit & 0xff;
+
+  return byte & mask;
+}
+
+/**
+ * Helper returning the packed address of byte + mask or -1 if strings
+ * are identical.
+ *
+ * @param  {string} a      - First key.
+ * @param  {string} b      - Second key.
+ * @return {number}        - Packed address of byte + mask.
+ */
+function findCriticalBit(a, b) {
+  var i = 0,
+      tmp;
+
+  // Swapping so a is the shortest
+  if (a.length > b.length) {
+    tmp = b;
+    b = a;
+    a = tmp;
+  }
+
+  var l = a.length,
+      mask;
+
+  while (i < l) {
+    if (a[i] !== b[i]) {
+      mask = bitwise.msb8(
+        a.charCodeAt(i) ^ b.charCodeAt(i)
+      );
+
+      return (i << 8) | mask;
+    }
+
+    i++;
+  }
+
+  // Strings are identical
+  if (a.length === b.length)
+    return -1;
+
+  // NOTE: x ^ 0 is the same as x
+  mask = bitwise.msb8(b.charCodeAt(i));
+
+  return (i << 8) | mask;
+}
+
+/**
+ * FixedCritBitTreeMap.
+ *
+ * @constructor
+ */
+function FixedCritBitTreeMap(capacity) {
+
+  if (typeof capacity !== 'number' || capacity <= 0)
+    throw new Error('mnemonist/fixed-critbit-tree-map: `capacity` should be a positive number.');
+
+  // Properties
+  this.capacity = capacity;
+  this.offset = 0;
+  this.root = 0;
+  this.size = 0;
+
+  var PointerArray = typed.getSignedPointerArray(capacity + 1);
+
+  this.keys = new Array(capacity);
+  this.values = new Array(capacity);
+  this.lefts = new PointerArray(capacity - 1);
+  this.rights = new PointerArray(capacity - 1);
+  this.critbits = new Uint32Array(capacity);
+}
+
+/**
+ * Method used to clear the FixedCritBitTreeMap.
+ *
+ * @return {undefined}
+ */
+FixedCritBitTreeMap.prototype.clear = function() {
+
+  // Properties
+  // TODO...
+  this.root = null;
+  this.size = 0;
+};
+
+/**
+ * Method used to set the value of the given key in the trie.
+ *
+ * @param  {string}         key   - Key to set.
+ * @param  {any}            value - Arbitrary value.
+ * @return {FixedCritBitTreeMap}
+ */
+FixedCritBitTreeMap.prototype.set = function(key, value) {
+  var pointer;
+
+  // TODO: yell if capacity is already full!
+
+  // Tree is empty
+  if (this.size === 0) {
+    this.keys[0] = key;
+    this.values[0] = value;
+
+    this.size++;
+
+    this.root = -1;
+
+    return this;
+  }
+
+  // Walk state
+  var pointer = this.root,
+      newPointer,
+      leftOrRight,
+      opposite,
+      ancestors = [],
+      path = [],
+      ancestor,
+      parent,
+      child,
+      critbit,
+      internal,
+      best,
+      dir,
+      i,
+      l;
+
+  // Walking the tree
+  while (true) {
+
+    // Traversing an internal node
+    if (pointer > 0) {
+      pointer -= 1;
+
+      // Choosing the correct direction
+      dir = getDirection(key, this.critbits[pointer]);
+
+      leftOrRight = dir === 0 ? this.lefts : this.rights;
+      newPointer = leftOrRight[pointer];
+
+      if (newPointer === 0) {
+
+        // Creating a fitting external node
+        pointer = this.size++;
+        leftOrRight[newPointer] = -(pointer + 1);
+        this.keys[pointer] = key;
+        this.values[pointer] = value;
+        return this;
+      }
+
+      ancestors.push(pointer);
+      path.push(dir);
+      pointer = newPointer;
+    }
+
+    // Reaching an external node
+    else {
+      pointer = -pointer;
+      pointer -= 1;
+
+      // 1. Creating a new external node
+      critbit = findCriticalBit(key, this.keys[pointer]);
+
+      // Key is identical, we just replace the value
+      if (critbit === -1) {
+        this.values[pointer] = value;
+        return this;
+      }
+
+      internal = this.offset++;
+      newPointer = this.size++;
+
+      this.keys[newPointer] = key;
+      this.values[newPointer] = value;
+
+      this.critbits[internal] = critbit;
+
+      dir = getDirection(key, critbit);
+      leftOrRight = dir === 0 ? this.lefts : this.rights;
+      opposite = dir === 0 ? this.rights : this.lefts;
+
+      leftOrRight[internal] = -(newPointer + 1);
+      opposite[internal] = -(pointer + 1);
+
+      // 2. Bubbling up
+      best = -1;
+      l = ancestors.length;
+
+      for (i = l - 1; i >= 0; i--) {
+        ancestor = ancestors[i];
+
+        // TODO: this can be made faster
+        if ((this.critbits[ancestor] >> 8) > (critbit >> 8)) {
+          continue;
+        }
+        else if ((this.critbits[ancestor] >> 8) === (critbit >> 8)) {
+          if ((this.critbits[ancestor] & 0xff) < (critbit & 0xff))
+            continue;
+        }
+
+        best = i;
+        break;
+      }
+
+      // Do we need to attach to the root?
+      if (best < 0) {
+        this.root = internal + 1;
+
+        // Need to rewire parent as child?
+        if (l > 0) {
+          parent = ancestors[0];
+
+          opposite[internal] = parent + 1;
+        }
+      }
+
+      // Simple case without rotation
+      else if (best === l - 1) {
+        parent = ancestors[best];
+        dir = path[best];
+
+        leftOrRight = dir === 0 ? this.lefts : this.rights;
+
+        leftOrRight[parent] = internal + 1;
+      }
+
+      // Full rotation
+      else {
+        parent = ancestors[best];
+        dir = path[best];
+        child = ancestors[best + 1];
+
+        opposite[internal] = child + 1;
+
+        leftOrRight = dir === 0 ? this.lefts : this.rights;
+
+        leftOrRight[parent] = internal + 1;
+      }
+
+      return this;
+    }
+  }
+};
+
+/**
+ * Method used to get the value attached to the given key in the tree or
+ * undefined if not found.
+ *
+ * @param  {string} key   - Key to get.
+ * @return {any}
+ */
+FixedCritBitTreeMap.prototype.get = function(key) {
+
+  // Walk state
+  var pointer = this.root,
+      dir;
+
+  // Walking the tree
+  while (true) {
+
+    // Dead end
+    if (pointer === 0)
+      return;
+
+    // Traversing an internal node
+    if (pointer > 0) {
+      pointer -= 1;
+      dir = getDirection(key, this.critbits[pointer]);
+
+      pointer = dir === 0 ? this.lefts[pointer] : this.rights[pointer];
+    }
+
+    // Reaching an external node
+    else {
+      pointer = -pointer;
+      pointer -= 1;
+
+      if (this.keys[pointer] !== key)
+        return;
+
+      return this.values[pointer];
+    }
+  }
+};
+
+/**
+ * Method used to return whether the given key exists in the tree.
+ *
+ * @param  {string} key - Key to test.
+ * @return {boolean}
+ */
+FixedCritBitTreeMap.prototype.has = function(key) {
+
+  // Walk state
+  var pointer = this.root,
+      dir;
+
+  // Walking the tree
+  while (true) {
+
+    // Dead end
+    if (pointer === 0)
+      return false;
+
+    // Traversing an internal node
+    if (pointer > 0) {
+      pointer -= 1;
+      dir = getDirection(key, this.critbits[pointer]);
+
+      pointer = dir === 0 ? this.lefts[pointer] : this.rights[pointer];
+    }
+
+    // Reaching an external node
+    else {
+      pointer = -pointer;
+      pointer -= 1;
+
+      return this.keys[pointer] === key;
+    }
+  }
+};
+
+/**
+ * Method used to iterate over the tree in key order.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+FixedCritBitTreeMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  // Inorder traversal of the tree
+  var current = this.root,
+      stack = [],
+      p;
+
+  while (true) {
+
+    if (current !== 0) {
+      stack.push(current);
+
+      current = current > 0 ? this.lefts[current - 1] : 0;
+    }
+
+    else {
+      if (stack.length > 0) {
+        current = stack.pop();
+
+        if (current < 0) {
+          p = -current;
+          p -= 1;
+
+          callback.call(scope, this.values[p], this.keys[p]);
+        }
+
+        current = current > 0 ? this.rights[current - 1] : 0;
+      }
+      else {
+        break;
+      }
+    }
+  }
+};
+
+/**
+ * Convenience known methods.
+ */
+FixedCritBitTreeMap.prototype.inspect = function() {
+  return this;
+};
+
+if (typeof Symbol !== 'undefined')
+  FixedCritBitTreeMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedCritBitTreeMap.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a FixedCritBitTreeMap.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {FixedCritBitTreeMap}
+ */
+// FixedCritBitTreeMap.from = function(iterable) {
+
+// };
+
+/**
+ * Exporting.
+ */
+module.exports = FixedCritBitTreeMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e6b9084e32a5d640c833dc428dc0510683a862b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.d.ts
@@ -0,0 +1,34 @@
+/**
+ * Mnemonist FixedDeque Typings
+ * =============================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+export default class FixedDeque<T> implements Iterable<T> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  unshift(item: T): number;
+  pop(): T | undefined;
+  shift(): T | undefined;
+  peekFirst(): T | undefined;
+  peekLast(): T | undefined;
+  get(index: number): T | undefined;
+  forEach(callback: (item: T, index: number, buffer: this) => void, scope?: any): void;
+  toArray(): Iterable<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}, ArrayClass: IArrayLikeConstructor, capacity?: number): FixedDeque<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.js b/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b298580b016c80a846255631122c06b5e578266
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-deque.js
@@ -0,0 +1,351 @@
+/**
+ * Mnemonist FixedDeque
+ * =====================
+ *
+ * Fixed capacity double-ended queue implemented as ring deque.
+ */
+var iterables = require('./utils/iterables.js'),
+    Iterator = require('obliterator/iterator');
+
+/**
+ * FixedDeque.
+ *
+ * @constructor
+ */
+function FixedDeque(ArrayClass, capacity) {
+
+  if (arguments.length < 2)
+    throw new Error('mnemonist/fixed-deque: expecting an Array class and a capacity.');
+
+  if (typeof capacity !== 'number' || capacity <= 0)
+    throw new Error('mnemonist/fixed-deque: `capacity` should be a positive number.');
+
+  this.ArrayClass = ArrayClass;
+  this.capacity = capacity;
+  this.items = new ArrayClass(this.capacity);
+  this.clear();
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+FixedDeque.prototype.clear = function() {
+
+  // Properties
+  this.start = 0;
+  this.size = 0;
+};
+
+/**
+ * Method used to append a value to the deque.
+ *
+ * @param  {any}    item - Item to append.
+ * @return {number}      - Returns the new size of the deque.
+ */
+FixedDeque.prototype.push = function(item) {
+  if (this.size === this.capacity)
+    throw new Error('mnemonist/fixed-deque.push: deque capacity (' + this.capacity + ') exceeded!');
+
+  var index = (this.start + this.size) % this.capacity;
+
+  this.items[index] = item;
+
+  return ++this.size;
+};
+
+/**
+ * Method used to prepend a value to the deque.
+ *
+ * @param  {any}    item - Item to prepend.
+ * @return {number}      - Returns the new size of the deque.
+ */
+FixedDeque.prototype.unshift = function(item) {
+  if (this.size === this.capacity)
+    throw new Error('mnemonist/fixed-deque.unshift: deque capacity (' + this.capacity + ') exceeded!');
+
+  var index = this.start - 1;
+
+  if (this.start === 0)
+    index = this.capacity - 1;
+
+  this.items[index] = item;
+  this.start = index;
+
+  return ++this.size;
+};
+
+/**
+ * Method used to pop the deque.
+ *
+ * @return {any} - Returns the popped item.
+ */
+FixedDeque.prototype.pop = function() {
+  if (this.size === 0)
+    return;
+
+  const index = (this.start + this.size - 1) % this.capacity;
+
+  this.size--;
+
+  return this.items[index];
+};
+
+/**
+ * Method used to shift the deque.
+ *
+ * @return {any} - Returns the shifted item.
+ */
+FixedDeque.prototype.shift = function() {
+  if (this.size === 0)
+    return;
+
+  var index = this.start;
+
+  this.size--;
+  this.start++;
+
+  if (this.start === this.capacity)
+    this.start = 0;
+
+  return this.items[index];
+};
+
+/**
+ * Method used to peek the first value of the deque.
+ *
+ * @return {any}
+ */
+FixedDeque.prototype.peekFirst = function() {
+  if (this.size === 0)
+    return;
+
+  return this.items[this.start];
+};
+
+/**
+ * Method used to peek the last value of the deque.
+ *
+ * @return {any}
+ */
+FixedDeque.prototype.peekLast = function() {
+  if (this.size === 0)
+    return;
+
+  var index = this.start + this.size - 1;
+
+  if (index > this.capacity)
+    index -= this.capacity;
+
+  return this.items[index];
+};
+
+/**
+ * Method used to get the desired value of the deque.
+ *
+ * @param  {number} index
+ * @return {any}
+ */
+FixedDeque.prototype.get = function(index) {
+  if (this.size === 0)
+    return;
+
+  index = this.start + index;
+
+  if (index > this.capacity)
+    index -= this.capacity;
+
+  return this.items[index];
+};
+
+/**
+ * Method used to iterate over the deque.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+FixedDeque.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  while (j < l) {
+    callback.call(scope, this.items[i], j, this);
+    i++;
+    j++;
+
+    if (i === c)
+      i = 0;
+  }
+};
+
+/**
+ * Method used to convert the deque to a JavaScript array.
+ *
+ * @return {array}
+ */
+// TODO: optional array class as argument?
+FixedDeque.prototype.toArray = function() {
+
+  // Optimization
+  var offset = this.start + this.size;
+
+  if (offset < this.capacity)
+    return this.items.slice(this.start, offset);
+
+  var array = new this.ArrayClass(this.size),
+      c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  while (j < l) {
+    array[j] = this.items[i];
+    i++;
+    j++;
+
+    if (i === c)
+      i = 0;
+  }
+
+  return array;
+};
+
+/**
+ * Method used to create an iterator over the deque's values.
+ *
+ * @return {Iterator}
+ */
+FixedDeque.prototype.values = function() {
+  var items = this.items,
+      c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  return new Iterator(function() {
+    if (j >= l)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+
+    i++;
+    j++;
+
+    if (i === c)
+      i = 0;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over the deque's entries.
+ *
+ * @return {Iterator}
+ */
+FixedDeque.prototype.entries = function() {
+  var items = this.items,
+      c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  return new Iterator(function() {
+    if (j >= l)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+
+    i++;
+
+    if (i === c)
+      i = 0;
+
+    return {
+      value: [j++, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  FixedDeque.prototype[Symbol.iterator] = FixedDeque.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+FixedDeque.prototype.inspect = function() {
+  var array = this.toArray();
+
+  array.type = this.ArrayClass.name;
+  array.capacity = this.capacity;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: FixedDeque,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  FixedDeque.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedDeque.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a deque.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {number}   capacity   - Desired capacity.
+ * @return {FiniteStack}
+ */
+FixedDeque.from = function(iterable, ArrayClass, capacity) {
+  if (arguments.length < 3) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/fixed-deque.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+
+  var deque = new FixedDeque(ArrayClass, capacity);
+
+  if (iterables.isArrayLike(iterable)) {
+    var i, l;
+
+    for (i = 0, l = iterable.length; i < l; i++)
+      deque.items[i] = iterable[i];
+
+    deque.size = l;
+
+    return deque;
+  }
+
+  iterables.forEach(iterable, function(value) {
+    deque.push(value);
+  });
+
+  return deque;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = FixedDeque;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..668c55648464ee4845c778dc79d09b6c4523ffbd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.d.ts
@@ -0,0 +1,25 @@
+/**
+ * Mnemonist FixedReverseHeap Typings
+ * ===================================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+type HeapComparator<T> = (a: T, b: T) => number;
+
+export default class FixedReverseHeap<T> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, comparator: HeapComparator<T>, capacity: number);
+  constructor(ArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  consume(): Iterable<T>;
+  toArray(): Iterable<T>;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.js b/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.js
new file mode 100644
index 0000000000000000000000000000000000000000..197aac408db7e8f83e55c440d3135d86dadeb34d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-reverse-heap.js
@@ -0,0 +1,209 @@
+/**
+ * Mnemonist Fixed Reverse Heap
+ * =============================
+ *
+ * Static heap implementation with fixed capacity. It's a "reverse" heap
+ * because it stores the elements in reverse so we can replace the worst
+ * item in logarithmic time. As such, one cannot pop this heap but can only
+ * consume it at the end. This structure is very efficient when trying to
+ * find the n smallest/largest items from a larger query (k nearest neigbors
+ * for instance).
+ */
+var comparators = require('./utils/comparators.js'),
+    Heap = require('./heap.js');
+
+var DEFAULT_COMPARATOR = comparators.DEFAULT_COMPARATOR,
+    reverseComparator = comparators.reverseComparator;
+
+/**
+ * Helper functions.
+ */
+
+/**
+ * Function used to sift up.
+ *
+ * @param {function} compare - Comparison function.
+ * @param {array}    heap    - Array storing the heap's data.
+ * @param {number}   size    - Heap's true size.
+ * @param {number}   i       - Index.
+ */
+function siftUp(compare, heap, size, i) {
+  var endIndex = size,
+      startIndex = i,
+      item = heap[i],
+      childIndex = 2 * i + 1,
+      rightIndex;
+
+  while (childIndex < endIndex) {
+    rightIndex = childIndex + 1;
+
+    if (
+      rightIndex < endIndex &&
+      compare(heap[childIndex], heap[rightIndex]) >= 0
+    ) {
+      childIndex = rightIndex;
+    }
+
+    heap[i] = heap[childIndex];
+    i = childIndex;
+    childIndex = 2 * i + 1;
+  }
+
+  heap[i] = item;
+  Heap.siftDown(compare, heap, startIndex, i);
+}
+
+/**
+ * Fully consumes the given heap.
+ *
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {function} compare    - Comparison function.
+ * @param  {array}    heap       - Array storing the heap's data.
+ * @param  {number}   size       - True size of the heap.
+ * @return {array}
+ */
+function consume(ArrayClass, compare, heap, size) {
+  var l = size,
+      i = l;
+
+  var array = new ArrayClass(size),
+      lastItem,
+      item;
+
+  while (i > 0) {
+    lastItem = heap[--i];
+
+    if (i !== 0) {
+      item = heap[0];
+      heap[0] = lastItem;
+      siftUp(compare, heap, --size, 0);
+      lastItem = item;
+    }
+
+    array[i] = lastItem;
+  }
+
+  return array;
+}
+
+/**
+ * Binary Minimum FixedReverseHeap.
+ *
+ * @constructor
+ * @param {function} ArrayClass - The class of array to use.
+ * @param {function} comparator - Comparator function.
+ * @param {number}   capacity   - Maximum number of items to keep.
+ */
+function FixedReverseHeap(ArrayClass, comparator, capacity) {
+
+  // Comparator can be omitted
+  if (arguments.length === 2) {
+    capacity = comparator;
+    comparator = null;
+  }
+
+  this.ArrayClass = ArrayClass;
+  this.capacity = capacity;
+
+  this.items = new ArrayClass(capacity);
+  this.clear();
+  this.comparator = comparator || DEFAULT_COMPARATOR;
+
+  if (typeof capacity !== 'number' && capacity <= 0)
+    throw new Error('mnemonist/FixedReverseHeap.constructor: capacity should be a number > 0.');
+
+  if (typeof this.comparator !== 'function')
+    throw new Error('mnemonist/FixedReverseHeap.constructor: given comparator should be a function.');
+
+  this.comparator = reverseComparator(this.comparator);
+}
+
+/**
+ * Method used to clear the heap.
+ *
+ * @return {undefined}
+ */
+FixedReverseHeap.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+};
+
+/**
+ * Method used to push an item into the heap.
+ *
+ * @param  {any}    item - Item to push.
+ * @return {number}
+ */
+FixedReverseHeap.prototype.push = function(item) {
+
+  // Still some place
+  if (this.size < this.capacity) {
+    this.items[this.size] = item;
+    Heap.siftDown(this.comparator, this.items, 0, this.size);
+    this.size++;
+  }
+
+  // Heap is full, we need to replace worst item
+  else {
+
+    if (this.comparator(item, this.items[0]) > 0)
+      Heap.replace(this.comparator, this.items, item);
+  }
+
+  return this.size;
+};
+
+/**
+ * Method used to peek the worst item in the heap.
+ *
+ * @return {any}
+ */
+FixedReverseHeap.prototype.peek = function() {
+  return this.items[0];
+};
+
+/**
+ * Method used to consume the heap fully and return its items as a sorted array.
+ *
+ * @return {array}
+ */
+FixedReverseHeap.prototype.consume = function() {
+  var items = consume(this.ArrayClass, this.comparator, this.items, this.size);
+  this.size = 0;
+
+  return items;
+};
+
+/**
+ * Method used to convert the heap to an array. Note that it basically clone
+ * the heap and consumes it completely. This is hardly performant.
+ *
+ * @return {array}
+ */
+FixedReverseHeap.prototype.toArray = function() {
+  return consume(this.ArrayClass, this.comparator, this.items.slice(0, this.size), this.size);
+};
+
+/**
+ * Convenience known methods.
+ */
+FixedReverseHeap.prototype.inspect = function() {
+  var proxy = this.toArray();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: FixedReverseHeap,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  FixedReverseHeap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedReverseHeap.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = FixedReverseHeap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99658534e09101f58a8a957d396638fa6981ef19
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.d.ts
@@ -0,0 +1,36 @@
+/**
+ * Mnemonist FixedStack Typings
+ * =============================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+export default class FixedStack<T> implements Iterable<T> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  pop(): T | undefined;
+  peek(): T | undefined;
+  forEach(callback: (item: T, index: number, stack: this) => void, scope?: any): void;
+  toArray(): Iterable<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  toString(): string;
+  toJSON(): Iterable<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    ArrayClass: IArrayLikeConstructor,
+    capacity?: number
+  ): FixedStack<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.js b/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5b5f48ba306c248421f2a5305896bdc2658e6c3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fixed-stack.js
@@ -0,0 +1,242 @@
+/**
+ * Mnemonist FixedStack
+ * =====================
+ *
+ * The fixed stack is a stack whose capacity is defined beforehand and that
+ * cannot be exceeded. This class is really useful when combined with
+ * byte arrays to save up some memory and avoid memory re-allocation, hence
+ * speeding up computations.
+ *
+ * This has however a downside: you need to know the maximum size you stack
+ * can have during your iteration (which is not too difficult to compute when
+ * performing, say, a DFS on a balanced binary tree).
+ */
+var Iterator = require('obliterator/iterator'),
+    iterables = require('./utils/iterables.js');
+
+/**
+ * FixedStack
+ *
+ * @constructor
+ * @param {function} ArrayClass - Array class to use.
+ * @param {number}   capacity   - Desired capacity.
+ */
+function FixedStack(ArrayClass, capacity) {
+
+  if (arguments.length < 2)
+    throw new Error('mnemonist/fixed-stack: expecting an Array class and a capacity.');
+
+  if (typeof capacity !== 'number' || capacity <= 0)
+    throw new Error('mnemonist/fixed-stack: `capacity` should be a positive number.');
+
+  this.capacity = capacity;
+  this.ArrayClass = ArrayClass;
+  this.items = new this.ArrayClass(this.capacity);
+  this.clear();
+}
+
+/**
+ * Method used to clear the stack.
+ *
+ * @return {undefined}
+ */
+FixedStack.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+};
+
+/**
+ * Method used to add an item to the stack.
+ *
+ * @param  {any}    item - Item to add.
+ * @return {number}
+ */
+FixedStack.prototype.push = function(item) {
+  if (this.size === this.capacity)
+    throw new Error('mnemonist/fixed-stack.push: stack capacity (' + this.capacity + ') exceeded!');
+
+  this.items[this.size++] = item;
+  return this.size;
+};
+
+/**
+ * Method used to retrieve & remove the last item of the stack.
+ *
+ * @return {any}
+ */
+FixedStack.prototype.pop = function() {
+  if (this.size === 0)
+    return;
+
+  return this.items[--this.size];
+};
+
+/**
+ * Method used to get the last item of the stack.
+ *
+ * @return {any}
+ */
+FixedStack.prototype.peek = function() {
+  return this.items[this.size - 1];
+};
+
+/**
+ * Method used to iterate over the stack.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+FixedStack.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = 0, l = this.items.length; i < l; i++)
+    callback.call(scope, this.items[l - i - 1], i, this);
+};
+
+/**
+ * Method used to convert the stack to a JavaScript array.
+ *
+ * @return {array}
+ */
+FixedStack.prototype.toArray = function() {
+  var array = new this.ArrayClass(this.size),
+      l = this.size - 1,
+      i = this.size;
+
+  while (i--)
+    array[i] = this.items[l - i];
+
+  return array;
+};
+
+/**
+ * Method used to create an iterator over a stack's values.
+ *
+ * @return {Iterator}
+ */
+FixedStack.prototype.values = function() {
+  var items = this.items,
+      l = this.size,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[l - i - 1];
+    i++;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a stack's entries.
+ *
+ * @return {Iterator}
+ */
+FixedStack.prototype.entries = function() {
+  var items = this.items,
+      l = this.size,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[l - i - 1];
+
+    return {
+      value: [i++, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  FixedStack.prototype[Symbol.iterator] = FixedStack.prototype.values;
+
+
+/**
+ * Convenience known methods.
+ */
+FixedStack.prototype.toString = function() {
+  return this.toArray().join(',');
+};
+
+FixedStack.prototype.toJSON = function() {
+  return this.toArray();
+};
+
+FixedStack.prototype.inspect = function() {
+  var array = this.toArray();
+
+  array.type = this.ArrayClass.name;
+  array.capacity = this.capacity;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: FixedStack,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  FixedStack.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedStack.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a stack.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {number}   capacity   - Desired capacity.
+ * @return {FixedStack}
+ */
+FixedStack.from = function(iterable, ArrayClass, capacity) {
+
+  if (arguments.length < 3) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/fixed-stack.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+
+  var stack = new FixedStack(ArrayClass, capacity);
+
+  if (iterables.isArrayLike(iterable)) {
+    var i, l;
+
+    for (i = 0, l = iterable.length; i < l; i++)
+      stack.items[i] = iterable[i];
+
+    stack.size = l;
+
+    return stack;
+  }
+
+  iterables.forEach(iterable, function(value) {
+    stack.push(value);
+  });
+
+  return stack;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = FixedStack;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a1644dcbb1bc9ba6db0f9953e01cdb4149dc267
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.d.ts
@@ -0,0 +1,33 @@
+/**
+ * Mnemonist FuzzyMap Typings
+ * ==========================
+ */
+type HashFunction<K> = (key: any) => K;
+type HashFunctionsTuple<K> = [HashFunction<K>, HashFunction<K>];
+
+export default class FuzzyMap<K, V> implements Iterable<V> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(hashFunction: HashFunction<K>);
+  constructor(hashFunctionsTuple: HashFunctionsTuple<K>);
+
+  // Methods
+  clear(): void;
+  add(key: V): this;
+  set(key: K, value: V): this;
+  get(key: any): V | undefined;
+  has(key: any): boolean;
+  forEach(callback: (value: V, key: V) => void, scope?: this): void;
+  values(): IterableIterator<V>;
+  [Symbol.iterator](): IterableIterator<V>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    hashFunction: HashFunction<I> | HashFunctionsTuple<I>,
+  ): FuzzyMap<I, J>;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.js b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0d52e14e0a93f2bad8d3561dafe2524204e9635
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-map.js
@@ -0,0 +1,185 @@
+/**
+ * Mnemonist Fuzzy Map
+ * ====================
+ *
+ * The fuzzy map is a map whose keys are processed by a function before
+ * read/write operations. This can often result in multiple keys accessing
+ * the same resource (example: a map with lowercased keys).
+ */
+var forEach = require('obliterator/foreach');
+
+var identity = function(x) {
+  return x;
+};
+
+/**
+ * FuzzyMap.
+ *
+ * @constructor
+ * @param {array|function} descriptor - Hash functions descriptor.
+ */
+function FuzzyMap(descriptor) {
+  this.items = new Map();
+  this.clear();
+
+  if (Array.isArray(descriptor)) {
+    this.writeHashFunction = descriptor[0];
+    this.readHashFunction = descriptor[1];
+  }
+  else {
+    this.writeHashFunction = descriptor;
+    this.readHashFunction = descriptor;
+  }
+
+  if (!this.writeHashFunction)
+    this.writeHashFunction = identity;
+  if (!this.readHashFunction)
+    this.readHashFunction = identity;
+
+  if (typeof this.writeHashFunction !== 'function')
+    throw new Error('mnemonist/FuzzyMap.constructor: invalid hash function given.');
+
+  if (typeof this.readHashFunction !== 'function')
+    throw new Error('mnemonist/FuzzyMap.constructor: invalid hash function given.');
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+FuzzyMap.prototype.clear = function() {
+  this.items.clear();
+
+  // Properties
+  this.size = 0;
+};
+
+/**
+ * Method used to add an item to the FuzzyMap.
+ *
+ * @param  {any} item - Item to add.
+ * @return {FuzzyMap}
+ */
+FuzzyMap.prototype.add = function(item) {
+  var key = this.writeHashFunction(item);
+
+  this.items.set(key, item);
+  this.size = this.items.size;
+
+  return this;
+};
+
+/**
+ * Method used to set an item in the FuzzyMap using the given key.
+ *
+ * @param  {any} key  - Key to use.
+ * @param  {any} item - Item to add.
+ * @return {FuzzyMap}
+ */
+FuzzyMap.prototype.set = function(key, item) {
+  key = this.writeHashFunction(key);
+
+  this.items.set(key, item);
+  this.size = this.items.size;
+
+  return this;
+};
+
+/**
+ * Method used to retrieve an item from the FuzzyMap.
+ *
+ * @param  {any} key - Key to use.
+ * @return {any}
+ */
+FuzzyMap.prototype.get = function(key) {
+  key = this.readHashFunction(key);
+
+  return this.items.get(key);
+};
+
+/**
+ * Method used to test the existence of an item in the map.
+ *
+ * @param  {any} key - Key to check.
+ * @return {boolean}
+ */
+FuzzyMap.prototype.has = function(key) {
+  key = this.readHashFunction(key);
+
+  return this.items.has(key);
+};
+
+/**
+ * Method used to iterate over each of the FuzzyMap's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+FuzzyMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  this.items.forEach(function(value) {
+    callback.call(scope, value, value);
+  });
+};
+
+/**
+ * Method returning an iterator over the FuzzyMap's values.
+ *
+ * @return {FuzzyMapIterator}
+ */
+FuzzyMap.prototype.values = function() {
+  return this.items.values();
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  FuzzyMap.prototype[Symbol.iterator] = FuzzyMap.prototype.values;
+
+/**
+ * Convenience known method.
+ */
+FuzzyMap.prototype.inspect = function() {
+  var array = Array.from(this.items.values());
+
+  Object.defineProperty(array, 'constructor', {
+    value: FuzzyMap,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  FuzzyMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FuzzyMap.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable}       iterable   - Target iterable.
+ * @param  {array|function} descriptor - Hash functions descriptor.
+ * @param  {boolean}        useSet     - Whether to use #.set or #.add
+ * @return {FuzzyMap}
+ */
+FuzzyMap.from = function(iterable, descriptor, useSet) {
+  var map = new FuzzyMap(descriptor);
+
+  forEach(iterable, function(value, key) {
+    if (useSet)
+      map.set(key, value);
+    else
+      map.add(value);
+  });
+
+  return map;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = FuzzyMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..62b8250ecebc7bda19d8a8f0139ab49d106ffd94
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.d.ts
@@ -0,0 +1,36 @@
+/**
+ * Mnemonist FuzzyMultiMap Typings
+ * ================================
+ */
+type HashFunction<K> = (key: any) => K;
+type HashFunctionsTuple<K> = [HashFunction<K>, HashFunction<K>];
+type FuzzyMultiMapContainer = ArrayConstructor | SetConstructor;
+
+export default class FuzzyMultiMap<K, V> implements Iterable<V> {
+
+  // Members
+  dimension: number;
+  size: number;
+
+  // Constructor
+  constructor(hashFunction: HashFunction<K>, Container?: FuzzyMultiMapContainer);
+  constructor(hashFunctions: HashFunctionsTuple<K>, Container?: FuzzyMultiMapContainer);
+
+  // Methods
+  clear(): void;
+  add(value: V): this;
+  set(key: K, value: V): this;
+  get(key: any): Array<V> | Set<V> | undefined;
+  has(key: any): boolean;
+  forEach(callback: (value: V, key: V) => void, scope?: any): void;
+  values(): IterableIterator<V>;
+  [Symbol.iterator](): IterableIterator<V>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    hashFunction: HashFunction<I> | HashFunctionsTuple<I>,
+    Container?: FuzzyMultiMapContainer
+  ): FuzzyMultiMap<I, J>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.js b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..78b2b083685229f391e8752cb9471ce5187e60bb
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/fuzzy-multi-map.js
@@ -0,0 +1,196 @@
+/**
+ * Mnemonist FuzzyMultiMap
+ * ========================
+ *
+ * Same as the fuzzy map but relying on a MultiMap rather than a Map.
+ */
+var MultiMap = require('./multi-map.js'),
+    forEach = require('obliterator/foreach');
+
+var identity = function(x) {
+  return x;
+};
+
+/**
+ * FuzzyMultiMap.
+ *
+ * @constructor
+ * @param {array|function} descriptor - Hash functions descriptor.
+ * @param {function}       Container  - Container to use.
+ */
+function FuzzyMultiMap(descriptor, Container) {
+  this.items = new MultiMap(Container);
+  this.clear();
+
+  if (Array.isArray(descriptor)) {
+    this.writeHashFunction = descriptor[0];
+    this.readHashFunction = descriptor[1];
+  }
+  else {
+    this.writeHashFunction = descriptor;
+    this.readHashFunction = descriptor;
+  }
+
+  if (!this.writeHashFunction)
+    this.writeHashFunction = identity;
+  if (!this.readHashFunction)
+    this.readHashFunction = identity;
+
+  if (typeof this.writeHashFunction !== 'function')
+    throw new Error('mnemonist/FuzzyMultiMap.constructor: invalid hash function given.');
+
+  if (typeof this.readHashFunction !== 'function')
+    throw new Error('mnemonist/FuzzyMultiMap.constructor: invalid hash function given.');
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+FuzzyMultiMap.prototype.clear = function() {
+  this.items.clear();
+
+  // Properties
+  this.size = 0;
+  this.dimension = 0;
+};
+
+/**
+ * Method used to add an item to the index.
+ *
+ * @param  {any} item - Item to add.
+ * @return {FuzzyMultiMap}
+ */
+FuzzyMultiMap.prototype.add = function(item) {
+  var key = this.writeHashFunction(item);
+
+  this.items.set(key, item);
+  this.size = this.items.size;
+  this.dimension = this.items.dimension;
+
+  return this;
+};
+
+/**
+ * Method used to set an item in the index using the given key.
+ *
+ * @param  {any} key  - Key to use.
+ * @param  {any} item - Item to add.
+ * @return {FuzzyMultiMap}
+ */
+FuzzyMultiMap.prototype.set = function(key, item) {
+  key = this.writeHashFunction(key);
+
+  this.items.set(key, item);
+  this.size = this.items.size;
+  this.dimension = this.items.dimension;
+
+  return this;
+};
+
+/**
+ * Method used to retrieve an item from the index.
+ *
+ * @param  {any} key - Key to use.
+ * @return {any}
+ */
+FuzzyMultiMap.prototype.get = function(key) {
+  key = this.readHashFunction(key);
+
+  return this.items.get(key);
+};
+
+/**
+ * Method used to test the existence of an item in the map.
+ *
+ * @param  {any} key - Key to check.
+ * @return {boolean}
+ */
+FuzzyMultiMap.prototype.has = function(key) {
+  key = this.readHashFunction(key);
+
+  return this.items.has(key);
+};
+
+/**
+ * Method used to iterate over each of the index's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+FuzzyMultiMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  this.items.forEach(function(value) {
+    callback.call(scope, value, value);
+  });
+};
+
+/**
+ * Method returning an iterator over the index's values.
+ *
+ * @return {FuzzyMultiMapIterator}
+ */
+FuzzyMultiMap.prototype.values = function() {
+  return this.items.values();
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  FuzzyMultiMap.prototype[Symbol.iterator] = FuzzyMultiMap.prototype.values;
+
+/**
+ * Convenience known method.
+ */
+FuzzyMultiMap.prototype.inspect = function() {
+  var array = Array.from(this);
+
+  Object.defineProperty(array, 'constructor', {
+    value: FuzzyMultiMap,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  FuzzyMultiMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = FuzzyMultiMap.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable}       iterable   - Target iterable.
+ * @param  {array|function} descriptor - Hash functions descriptor.
+ * @param  {function}       Container  - Container to use.
+ * @param  {boolean}        useSet     - Whether to use #.set or #.add
+ * @return {FuzzyMultiMap}
+ */
+FuzzyMultiMap.from = function(iterable, descriptor, Container, useSet) {
+  if (arguments.length === 3) {
+    if (typeof Container === 'boolean') {
+      useSet = Container;
+      Container = Array;
+    }
+  }
+
+  var map = new FuzzyMultiMap(descriptor, Container);
+
+  forEach(iterable, function(value, key) {
+    if (useSet)
+      map.set(key, value);
+    else
+      map.add(value);
+  });
+
+  return map;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = FuzzyMultiMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eb56f7c973b7bef3a1d3ea1161250887d58285db
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.d.ts
@@ -0,0 +1,32 @@
+/**
+ * Mnemonist HashedArrayTree Typings
+ * ==================================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+type HashedArrayTreeOptions = {
+  initialCapacity?: number;
+  initialLength?: number;
+  blockSize?: number;
+}
+
+export default class HashedArrayTree<T> {
+
+  // Members
+  blockSize: number;
+  capacity: number;
+  length: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, capacity: number);
+  constructor(ArrayClass: IArrayLikeConstructor, options: HashedArrayTreeOptions);
+
+  // Methods
+  set(index: number, value: T): this;
+  get(index: number): T | undefined;
+  grow(capacity: number): this;
+  resize(length: number): this;
+  push(value: T): number;
+  pop(): T | undefined;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.js b/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..a51667c9ac3c456c0190b9e1ff66e961742a1057
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/hashed-array-tree.js
@@ -0,0 +1,209 @@
+/**
+ * Mnemonist HashedArrayTree
+ * ==========================
+ *
+ * Abstract implementation of a hashed array tree representing arrays growing
+ * dynamically.
+ */
+
+/**
+ * Defaults.
+ */
+var DEFAULT_BLOCK_SIZE = 1024;
+
+/**
+ * Helpers.
+ */
+function powerOfTwo(x) {
+  return (x & (x - 1)) === 0;
+}
+
+/**
+ * HashedArrayTree.
+ *
+ * @constructor
+ * @param {function}      ArrayClass           - An array constructor.
+ * @param {number|object} initialCapacityOrOptions - Self-explanatory.
+ */
+function HashedArrayTree(ArrayClass, initialCapacityOrOptions) {
+  if (arguments.length < 1)
+    throw new Error('mnemonist/hashed-array-tree: expecting at least a byte array constructor.');
+
+  var initialCapacity = initialCapacityOrOptions || 0,
+      blockSize = DEFAULT_BLOCK_SIZE,
+      initialLength = 0;
+
+  if (typeof initialCapacityOrOptions === 'object') {
+    initialCapacity = initialCapacityOrOptions.initialCapacity || 0;
+    initialLength = initialCapacityOrOptions.initialLength || 0;
+    blockSize = initialCapacityOrOptions.blockSize || DEFAULT_BLOCK_SIZE;
+  }
+
+  if (!blockSize || !powerOfTwo(blockSize))
+    throw new Error('mnemonist/hashed-array-tree: block size should be a power of two.');
+
+  var capacity = Math.max(initialLength, initialCapacity),
+      initialBlocks = Math.ceil(capacity / blockSize);
+
+  this.ArrayClass = ArrayClass;
+  this.length = initialLength;
+  this.capacity = initialBlocks * blockSize;
+  this.blockSize = blockSize;
+  this.offsetMask = blockSize - 1;
+  this.blockMask = Math.log2(blockSize);
+
+  // Allocating initial blocks
+  this.blocks = new Array(initialBlocks);
+
+  for (var i = 0; i < initialBlocks; i++)
+    this.blocks[i] = new this.ArrayClass(this.blockSize);
+}
+
+/**
+ * Method used to set a value.
+ *
+ * @param  {number} index - Index to edit.
+ * @param  {any}    value - Value.
+ * @return {HashedArrayTree}
+ */
+HashedArrayTree.prototype.set = function(index, value) {
+
+  // Out of bounds?
+  if (this.length < index)
+    throw new Error('HashedArrayTree(' + this.ArrayClass.name + ').set: index out of bounds.');
+
+  var block = index >> this.blockMask,
+      i = index & this.offsetMask;
+
+  this.blocks[block][i] = value;
+
+  return this;
+};
+
+/**
+ * Method used to get a value.
+ *
+ * @param  {number} index - Index to retrieve.
+ * @return {any}
+ */
+HashedArrayTree.prototype.get = function(index) {
+  if (this.length < index)
+    return;
+
+  var block = index >> this.blockMask,
+      i = index & this.offsetMask;
+
+  return this.blocks[block][i];
+};
+
+/**
+ * Method used to grow the array.
+ *
+ * @param  {number}          capacity - Optional capacity to accomodate.
+ * @return {HashedArrayTree}
+ */
+HashedArrayTree.prototype.grow = function(capacity) {
+  if (typeof capacity !== 'number')
+    capacity = this.capacity + this.blockSize;
+
+  if (this.capacity >= capacity)
+    return this;
+
+  while (this.capacity < capacity) {
+    this.blocks.push(new this.ArrayClass(this.blockSize));
+    this.capacity += this.blockSize;
+  }
+
+  return this;
+};
+
+/**
+ * Method used to resize the array. Won't deallocate.
+ *
+ * @param  {number}       length - Target length.
+ * @return {HashedArrayTree}
+ */
+HashedArrayTree.prototype.resize = function(length) {
+  if (length === this.length)
+    return this;
+
+  if (length < this.length) {
+    this.length = length;
+    return this;
+  }
+
+  this.length = length;
+  this.grow(length);
+
+  return this;
+};
+
+/**
+ * Method used to push a value into the array.
+ *
+ * @param  {any}    value - Value to push.
+ * @return {number}       - Length of the array.
+ */
+HashedArrayTree.prototype.push = function(value) {
+  if (this.capacity === this.length)
+    this.grow();
+
+  var index = this.length;
+
+  var block = index >> this.blockMask,
+      i = index & this.offsetMask;
+
+  this.blocks[block][i] = value;
+
+  return ++this.length;
+};
+
+/**
+ * Method used to pop the last value of the array.
+ *
+ * @return {number} - The popped value.
+ */
+HashedArrayTree.prototype.pop = function() {
+  if (this.length === 0)
+    return;
+
+  var lastBlock = this.blocks[this.blocks.length - 1];
+
+  var i = (--this.length) & this.offsetMask;
+
+  return lastBlock[i];
+};
+
+/**
+ * Convenience known methods.
+ */
+HashedArrayTree.prototype.inspect = function() {
+  var proxy = new this.ArrayClass(this.length),
+      block;
+
+  for (var i = 0, l = this.length; i < l; i++) {
+    block = i >> this.blockMask;
+    proxy[i] = this.blocks[block][i & this.offsetMask];
+  }
+
+  proxy.type = this.ArrayClass.name;
+  proxy.items = this.length;
+  proxy.capacity = this.capacity;
+  proxy.blockSize = this.blockSize;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: HashedArrayTree,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  HashedArrayTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = HashedArrayTree.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = HashedArrayTree;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/heap.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/heap.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c6aa219c34bf134c40f977be3a036a2ccdfd5c71
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/heap.d.ts
@@ -0,0 +1,84 @@
+/**
+ * Mnemonist Heap Typings
+ * =======================
+ */
+type HeapComparator<T> = (a: T, b: T) => number;
+
+export default class Heap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: HeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  replace(item: T): T | undefined;
+  pushpop(item: T): T | undefined;
+  toArray(): Array<T>;
+  consume(): Array<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    comparator?: HeapComparator<I>
+  ): Heap<I>;
+}
+
+export class MinHeap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: HeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  replace(item: T): T | undefined;
+  pushpop(item: T): T | undefined;
+  toArray(): Array<T>;
+  consume(): Array<T>;
+  inspect(): any;
+}
+
+export class MaxHeap<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(comparator?: HeapComparator<T>);
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  peek(): T | undefined;
+  pop(): T | undefined;
+  replace(item: T): T | undefined;
+  pushpop(item: T): T | undefined;
+  toArray(): Array<T>;
+  consume(): Array<T>;
+  inspect(): any;
+}
+
+// Static helpers
+export function push<T>(comparator: HeapComparator<T>, heap: Array<T>, item: T): void;
+export function pop<T>(comparator: HeapComparator<T>, heap: Array<T>): T;
+export function replace<T>(comparator: HeapComparator<T>, heap: Array<T>, item: T): T;
+export function pushpop<T>(comparator: HeapComparator<T>, heap: Array<T>, item: T): T;
+export function heapify<T>(comparator: HeapComparator<T>, array: Array<T>): void;
+export function consume<T>(comparator: HeapComparator<T>, heap: Array<T>): Array<T>;
+
+export function nsmallest<T>(comparator: HeapComparator<T>, n: number, values: Iterable<T>): Array<T>;
+export function nsmallest<T>(n: number, values: Iterable<T>): Array<T>;
+export function nlargest<T>(comparator: HeapComparator<T>, n: number, values: Iterable<T>): Array<T>;
+export function nlargest<T>(n: number, values: Iterable<T>): Array<T>;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/heap.js b/libs/shared/graph-layout/node_modules/mnemonist/heap.js
new file mode 100644
index 0000000000000000000000000000000000000000..90eb971c1b2fa3ebc9f1db63b9dc1848207dd936
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/heap.js
@@ -0,0 +1,576 @@
+/**
+ * Mnemonist Binary Heap
+ * ======================
+ *
+ * Binary heap implementation.
+ */
+var forEach = require('obliterator/foreach'),
+    comparators = require('./utils/comparators.js'),
+    iterables = require('./utils/iterables.js');
+
+var DEFAULT_COMPARATOR = comparators.DEFAULT_COMPARATOR,
+    reverseComparator = comparators.reverseComparator;
+
+/**
+ * Heap helper functions.
+ */
+
+/**
+ * Function used to sift down.
+ *
+ * @param {function} compare    - Comparison function.
+ * @param {array}    heap       - Array storing the heap's data.
+ * @param {number}   startIndex - Starting index.
+ * @param {number}   i          - Index.
+ */
+function siftDown(compare, heap, startIndex, i) {
+  var item = heap[i],
+      parentIndex,
+      parent;
+
+  while (i > startIndex) {
+    parentIndex = (i - 1) >> 1;
+    parent = heap[parentIndex];
+
+    if (compare(item, parent) < 0) {
+      heap[i] = parent;
+      i = parentIndex;
+      continue;
+    }
+
+    break;
+  }
+
+  heap[i] = item;
+}
+
+/**
+ * Function used to sift up.
+ *
+ * @param {function} compare - Comparison function.
+ * @param {array}    heap    - Array storing the heap's data.
+ * @param {number}   i       - Index.
+ */
+function siftUp(compare, heap, i) {
+  var endIndex = heap.length,
+      startIndex = i,
+      item = heap[i],
+      childIndex = 2 * i + 1,
+      rightIndex;
+
+  while (childIndex < endIndex) {
+    rightIndex = childIndex + 1;
+
+    if (
+      rightIndex < endIndex &&
+      compare(heap[childIndex], heap[rightIndex]) >= 0
+    ) {
+      childIndex = rightIndex;
+    }
+
+    heap[i] = heap[childIndex];
+    i = childIndex;
+    childIndex = 2 * i + 1;
+  }
+
+  heap[i] = item;
+  siftDown(compare, heap, startIndex, i);
+}
+
+/**
+ * Function used to push an item into a heap represented by a raw array.
+ *
+ * @param {function} compare - Comparison function.
+ * @param {array}    heap    - Array storing the heap's data.
+ * @param {any}      item    - Item to push.
+ */
+function push(compare, heap, item) {
+  heap.push(item);
+  siftDown(compare, heap, 0, heap.length - 1);
+}
+
+/**
+ * Function used to pop an item from a heap represented by a raw array.
+ *
+ * @param  {function} compare - Comparison function.
+ * @param  {array}    heap    - Array storing the heap's data.
+ * @return {any}
+ */
+function pop(compare, heap) {
+  var lastItem = heap.pop();
+
+  if (heap.length !== 0) {
+    var item = heap[0];
+    heap[0] = lastItem;
+    siftUp(compare, heap, 0);
+
+    return item;
+  }
+
+  return lastItem;
+}
+
+/**
+ * Function used to pop the heap then push a new value into it, thus "replacing"
+ * it.
+ *
+ * @param  {function} compare - Comparison function.
+ * @param  {array}    heap    - Array storing the heap's data.
+ * @param  {any}      item    - The item to push.
+ * @return {any}
+ */
+function replace(compare, heap, item) {
+  if (heap.length === 0)
+    throw new Error('mnemonist/heap.replace: cannot pop an empty heap.');
+
+  var popped = heap[0];
+  heap[0] = item;
+  siftUp(compare, heap, 0);
+
+  return popped;
+}
+
+/**
+ * Function used to push an item in the heap then pop the heap and return the
+ * popped value.
+ *
+ * @param  {function} compare - Comparison function.
+ * @param  {array}    heap    - Array storing the heap's data.
+ * @param  {any}      item    - The item to push.
+ * @return {any}
+ */
+function pushpop(compare, heap, item) {
+  var tmp;
+
+  if (heap.length !== 0 && compare(heap[0], item) < 0) {
+    tmp = heap[0];
+    heap[0] = item;
+    item = tmp;
+    siftUp(compare, heap, 0);
+  }
+
+  return item;
+}
+
+/**
+ * Converts and array into an abstract heap in linear time.
+ *
+ * @param {function} compare - Comparison function.
+ * @param {array}    array   - Target array.
+ */
+function heapify(compare, array) {
+  var n = array.length,
+      l = n >> 1,
+      i = l;
+
+  while (--i >= 0)
+    siftUp(compare, array, i);
+}
+
+/**
+ * Fully consumes the given heap.
+ *
+ * @param  {function} compare - Comparison function.
+ * @param  {array}    heap    - Array storing the heap's data.
+ * @return {array}
+ */
+function consume(compare, heap) {
+  var l = heap.length,
+      i = 0;
+
+  var array = new Array(l);
+
+  while (i < l)
+    array[i++] = pop(compare, heap);
+
+  return array;
+}
+
+/**
+ * Function used to retrieve the n smallest items from the given iterable.
+ *
+ * @param {function} compare  - Comparison function.
+ * @param {number}   n        - Number of top items to retrieve.
+ * @param {any}      iterable - Arbitrary iterable.
+ * @param {array}
+ */
+function nsmallest(compare, n, iterable) {
+  if (arguments.length === 2) {
+    iterable = n;
+    n = compare;
+    compare = DEFAULT_COMPARATOR;
+  }
+
+  var reverseCompare = reverseComparator(compare);
+
+  var i, l, v;
+
+  var min = Infinity;
+
+  var result;
+
+  // If n is equal to 1, it's just a matter of finding the minimum
+  if (n === 1) {
+    if (iterables.isArrayLike(iterable)) {
+      for (i = 0, l = iterable.length; i < l; i++) {
+        v = iterable[i];
+
+        if (min === Infinity || compare(v, min) < 0)
+          min = v;
+      }
+
+      result = new iterable.constructor(1);
+      result[0] = min;
+
+      return result;
+    }
+
+    forEach(iterable, function(value) {
+      if (min === Infinity || compare(value, min) < 0)
+        min = value;
+    });
+
+    return [min];
+  }
+
+  if (iterables.isArrayLike(iterable)) {
+
+    // If n > iterable length, we just clone and sort
+    if (n >= iterable.length)
+      return iterable.slice().sort(compare);
+
+    result = iterable.slice(0, n);
+    heapify(reverseCompare, result);
+
+    for (i = n, l = iterable.length; i < l; i++)
+      if (reverseCompare(iterable[i], result[0]) > 0)
+        replace(reverseCompare, result, iterable[i]);
+
+    // NOTE: if n is over some number, it becomes faster to consume the heap
+    return result.sort(compare);
+  }
+
+  // Correct for size
+  var size = iterables.guessLength(iterable);
+
+  if (size !== null && size < n)
+    n = size;
+
+  result = new Array(n);
+  i = 0;
+
+  forEach(iterable, function(value) {
+    if (i < n) {
+      result[i] = value;
+    }
+    else {
+      if (i === n)
+        heapify(reverseCompare, result);
+
+      if (reverseCompare(value, result[0]) > 0)
+        replace(reverseCompare, result, value);
+    }
+
+    i++;
+  });
+
+  if (result.length > i)
+    result.length = i;
+
+  // NOTE: if n is over some number, it becomes faster to consume the heap
+  return result.sort(compare);
+}
+
+/**
+ * Function used to retrieve the n largest items from the given iterable.
+ *
+ * @param {function} compare  - Comparison function.
+ * @param {number}   n        - Number of top items to retrieve.
+ * @param {any}      iterable - Arbitrary iterable.
+ * @param {array}
+ */
+function nlargest(compare, n, iterable) {
+  if (arguments.length === 2) {
+    iterable = n;
+    n = compare;
+    compare = DEFAULT_COMPARATOR;
+  }
+
+  var reverseCompare = reverseComparator(compare);
+
+  var i, l, v;
+
+  var max = -Infinity;
+
+  var result;
+
+  // If n is equal to 1, it's just a matter of finding the maximum
+  if (n === 1) {
+    if (iterables.isArrayLike(iterable)) {
+      for (i = 0, l = iterable.length; i < l; i++) {
+        v = iterable[i];
+
+        if (max === -Infinity || compare(v, max) > 0)
+          max = v;
+      }
+
+      result = new iterable.constructor(1);
+      result[0] = max;
+
+      return result;
+    }
+
+    forEach(iterable, function(value) {
+      if (max === -Infinity || compare(value, max) > 0)
+        max = value;
+    });
+
+    return [max];
+  }
+
+  if (iterables.isArrayLike(iterable)) {
+
+    // If n > iterable length, we just clone and sort
+    if (n >= iterable.length)
+      return iterable.slice().sort(reverseCompare);
+
+    result = iterable.slice(0, n);
+    heapify(compare, result);
+
+    for (i = n, l = iterable.length; i < l; i++)
+      if (compare(iterable[i], result[0]) > 0)
+        replace(compare, result, iterable[i]);
+
+    // NOTE: if n is over some number, it becomes faster to consume the heap
+    return result.sort(reverseCompare);
+  }
+
+  // Correct for size
+  var size = iterables.guessLength(iterable);
+
+  if (size !== null && size < n)
+    n = size;
+
+  result = new Array(n);
+  i = 0;
+
+  forEach(iterable, function(value) {
+    if (i < n) {
+      result[i] = value;
+    }
+    else {
+      if (i === n)
+        heapify(compare, result);
+
+      if (compare(value, result[0]) > 0)
+        replace(compare, result, value);
+    }
+
+    i++;
+  });
+
+  if (result.length > i)
+    result.length = i;
+
+  // NOTE: if n is over some number, it becomes faster to consume the heap
+  return result.sort(reverseCompare);
+}
+
+/**
+ * Binary Minimum Heap.
+ *
+ * @constructor
+ * @param {function} comparator - Comparator function to use.
+ */
+function Heap(comparator) {
+  this.clear();
+  this.comparator = comparator || DEFAULT_COMPARATOR;
+
+  if (typeof this.comparator !== 'function')
+    throw new Error('mnemonist/Heap.constructor: given comparator should be a function.');
+}
+
+/**
+ * Method used to clear the heap.
+ *
+ * @return {undefined}
+ */
+Heap.prototype.clear = function() {
+
+  // Properties
+  this.items = [];
+  this.size = 0;
+};
+
+/**
+ * Method used to push an item into the heap.
+ *
+ * @param  {any}    item - Item to push.
+ * @return {number}
+ */
+Heap.prototype.push = function(item) {
+  push(this.comparator, this.items, item);
+  return ++this.size;
+};
+
+/**
+ * Method used to retrieve the "first" item of the heap.
+ *
+ * @return {any}
+ */
+Heap.prototype.peek = function() {
+  return this.items[0];
+};
+
+/**
+ * Method used to retrieve & remove the "first" item of the heap.
+ *
+ * @return {any}
+ */
+Heap.prototype.pop = function() {
+  if (this.size !== 0)
+    this.size--;
+
+  return pop(this.comparator, this.items);
+};
+
+/**
+ * Method used to pop the heap, then push an item and return the popped
+ * item.
+ *
+ * @param  {any} item - Item to push into the heap.
+ * @return {any}
+ */
+Heap.prototype.replace = function(item) {
+  return replace(this.comparator, this.items, item);
+};
+
+/**
+ * Method used to push the heap, the pop it and return the pooped item.
+ *
+ * @param  {any} item - Item to push into the heap.
+ * @return {any}
+ */
+Heap.prototype.pushpop = function(item) {
+  return pushpop(this.comparator, this.items, item);
+};
+
+/**
+ * Method used to consume the heap fully and return its items as a sorted array.
+ *
+ * @return {array}
+ */
+Heap.prototype.consume = function() {
+  this.size = 0;
+  return consume(this.comparator, this.items);
+};
+
+/**
+ * Method used to convert the heap to an array. Note that it basically clone
+ * the heap and consumes it completely. This is hardly performant.
+ *
+ * @return {array}
+ */
+Heap.prototype.toArray = function() {
+  return consume(this.comparator, this.items.slice());
+};
+
+/**
+ * Convenience known methods.
+ */
+Heap.prototype.inspect = function() {
+  var proxy = this.toArray();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: Heap,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  Heap.prototype[Symbol.for('nodejs.util.inspect.custom')] = Heap.prototype.inspect;
+
+/**
+ * Binary Maximum Heap.
+ *
+ * @constructor
+ * @param {function} comparator - Comparator function to use.
+ */
+function MaxHeap(comparator) {
+  this.clear();
+  this.comparator = comparator || DEFAULT_COMPARATOR;
+
+  if (typeof this.comparator !== 'function')
+    throw new Error('mnemonist/MaxHeap.constructor: given comparator should be a function.');
+
+  this.comparator = reverseComparator(this.comparator);
+}
+
+MaxHeap.prototype = Heap.prototype;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a heap.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} comparator - Custom comparator function.
+ * @return {Heap}
+ */
+Heap.from = function(iterable, comparator) {
+  var heap = new Heap(comparator);
+
+  var items;
+
+  // If iterable is an array, we can be clever about it
+  if (iterables.isArrayLike(iterable))
+    items = iterable.slice();
+  else
+    items = iterables.toArray(iterable);
+
+  heapify(heap.comparator, items);
+  heap.items = items;
+  heap.size = items.length;
+
+  return heap;
+};
+
+MaxHeap.from = function(iterable, comparator) {
+  var heap = new MaxHeap(comparator);
+
+  var items;
+
+  // If iterable is an array, we can be clever about it
+  if (iterables.isArrayLike(iterable))
+    items = iterable.slice();
+  else
+    items = iterables.toArray(iterable);
+
+  heapify(heap.comparator, items);
+  heap.items = items;
+  heap.size = items.length;
+
+  return heap;
+};
+
+/**
+ * Exporting.
+ */
+Heap.siftUp = siftUp;
+Heap.siftDown = siftDown;
+Heap.push = push;
+Heap.pop = pop;
+Heap.replace = replace;
+Heap.pushpop = pushpop;
+Heap.heapify = heapify;
+Heap.consume = consume;
+
+Heap.nsmallest = nsmallest;
+Heap.nlargest = nlargest;
+
+Heap.MinHeap = Heap;
+Heap.MaxHeap = MaxHeap;
+
+module.exports = Heap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/index.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..abaef49204d033d5a113dc89d5c9a3a203ac852a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/index.d.ts
@@ -0,0 +1,48 @@
+/**
+ * Mnemonist Typings
+ * ==================
+ *
+ * Gathering the library's typings.
+ */
+import * as set from './set';
+
+export {default as BiMap, InverseMap} from './bi-map';
+export {default as BitSet} from './bit-set';
+export {default as BitVector} from './bit-vector';
+export {default as BKTree} from './bk-tree';
+export {default as BloomFilter} from './bloom-filter';
+export {default as CircularBuffer} from './circular-buffer';
+export {default as DefaultMap} from './default-map';
+export {default as DefaultWeakMap} from './default-weak-map';
+export {default as FixedDeque} from './fixed-deque';
+export {default as FibonacciHeap, MinFibonacciHeap, MaxFibonacciHeap} from './fibonacci-heap';
+export {default as FixedReverseHeap} from './fixed-reverse-heap';
+export {default as FixedStack} from './fixed-stack';
+export {default as FuzzyMap} from './fuzzy-map';
+export {default as FuzzyMultiMap} from './fuzzy-multi-map';
+export {default as HashedArrayTree} from './hashed-array-tree';
+export {default as Heap, MinHeap, MaxHeap} from './heap';
+export {default as InvertedIndex} from './inverted-index';
+export {default as KDTree} from './kd-tree';
+export {default as LinkedList} from './linked-list';
+export {default as LRUCache} from './lru-cache';
+export {default as LRUCacheWithDelete} from './lru-cache-with-delete';
+export {default as LRUMap} from './lru-map';
+export {default as LRUMapWithDelete} from './lru-map-with-delete';
+export {default as MultiMap} from './multi-map';
+export {default as MultiSet} from './multi-set';
+export {default as PassjoinIndex} from './passjoin-index';
+export {default as Queue} from './queue';
+export {set};
+export {default as SparseQueueSet} from './sparse-queue-set';
+export {default as SparseMap} from './sparse-map';
+export {default as SparseSet} from './sparse-set';
+export {default as Stack} from './stack';
+export {default as StaticDisjointSet} from './static-disjoint-set';
+export {default as StaticIntervalTree} from './static-interval-tree';
+export {default as SuffixArray, GeneralizedSuffixArray} from './suffix-array';
+export {default as SymSpell} from './symspell';
+export {default as Trie} from './trie';
+export {default as TrieMap} from './trie-map';
+export {default as Vector, Uint8Vector, Uint8ClampedVector, Int8Vector, Uint16Vector, Int16Vector, Uint32Vector, Int32Vector, Float32Vector, Float64Array} from './vector';
+export {default as VPTree} from './vp-tree';
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/index.js b/libs/shared/graph-layout/node_modules/mnemonist/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..75bf74e91e7c038c0d0e6236a057315208ad0226
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/index.js
@@ -0,0 +1,58 @@
+/**
+ * Mnemonist Library Endpoint
+ * ===========================
+ *
+ * Exporting every data structure through a unified endpoint. Consumers
+ * of this library should prefer the modular access though.
+ */
+var Heap = require('./heap.js'),
+    FibonacciHeap = require('./fibonacci-heap.js'),
+    SuffixArray = require('./suffix-array.js');
+
+module.exports = {
+  BiMap: require('./bi-map.js'),
+  BitSet: require('./bit-set.js'),
+  BitVector: require('./bit-vector.js'),
+  BloomFilter: require('./bloom-filter.js'),
+  BKTree: require('./bk-tree.js'),
+  CircularBuffer: require('./circular-buffer.js'),
+  DefaultMap: require('./default-map.js'),
+  DefaultWeakMap: require('./default-weak-map.js'),
+  FixedDeque: require('./fixed-deque.js'),
+  StaticDisjointSet: require('./static-disjoint-set.js'),
+  FibonacciHeap: FibonacciHeap,
+  MinFibonacciHeap: FibonacciHeap.MinFibonacciHeap,
+  MaxFibonacciHeap: FibonacciHeap.MaxFibonacciHeap,
+  FixedReverseHeap: require('./fixed-reverse-heap.js'),
+  FuzzyMap: require('./fuzzy-map.js'),
+  FuzzyMultiMap: require('./fuzzy-multi-map.js'),
+  HashedArrayTree: require('./hashed-array-tree.js'),
+  Heap: Heap,
+  MinHeap: Heap.MinHeap,
+  MaxHeap: Heap.MaxHeap,
+  StaticIntervalTree: require('./static-interval-tree.js'),
+  InvertedIndex: require('./inverted-index.js'),
+  KDTree: require('./kd-tree.js'),
+  LinkedList: require('./linked-list.js'),
+  LRUCache: require('./lru-cache.js'),
+  LRUCacheWithDelete: require('./lru-cache-with-delete.js'),
+  LRUMap: require('./lru-map.js'),
+  LRUMapWithDelete: require('./lru-map-with-delete.js'),
+  MultiMap: require('./multi-map.js'),
+  MultiSet: require('./multi-set.js'),
+  PassjoinIndex: require('./passjoin-index.js'),
+  Queue: require('./queue.js'),
+  FixedStack: require('./fixed-stack.js'),
+  Stack: require('./stack.js'),
+  SuffixArray: SuffixArray,
+  GeneralizedSuffixArray: SuffixArray.GeneralizedSuffixArray,
+  Set: require('./set.js'),
+  SparseQueueSet: require('./sparse-queue-set.js'),
+  SparseMap: require('./sparse-map.js'),
+  SparseSet: require('./sparse-set.js'),
+  SymSpell: require('./symspell.js'),
+  Trie: require('./trie.js'),
+  TrieMap: require('./trie-map.js'),
+  Vector: require('./vector.js'),
+  VPTree: require('./vp-tree.js')
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4596ff83a1a0be5c624a39ba136a10c47c993003
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.d.ts
@@ -0,0 +1,33 @@
+/**
+ * Mnemonist InvertedIndex Typings
+ * ================================
+ */
+type Tokenizer = (key: any) => Array<string>;
+type TokenizersTuple = [Tokenizer, Tokenizer];
+
+export default class InvertedIndex<D> implements Iterable<D> {
+
+  // Members
+  dimension: number;
+  size: number;
+
+  // Constructor
+  constructor(tokenizer?: Tokenizer);
+  constructor(tokenizers?: TokenizersTuple);
+
+  // Methods
+  clear(): void;
+  add(document: D): this;
+  get(query: any): Array<D>;
+  forEach(callback: (document: D, index: number, invertedIndex: this) => void, scope?: any): void;
+  documents(): IterableIterator<D>;
+  tokens(): IterableIterator<string>;
+  [Symbol.iterator](): IterableIterator<D>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    tokenizer?: Tokenizer | TokenizersTuple
+  ): InvertedIndex<I>;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.js b/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a352d19881c9a549697388e5533a0059a1d9c87b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/inverted-index.js
@@ -0,0 +1,249 @@
+/**
+ * Mnemonist Inverted Index
+ * =========================
+ *
+ * JavaScript implementation of an inverted index.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach'),
+    helpers = require('./utils/merge.js');
+
+function identity(x) {
+  return x;
+}
+
+/**
+ * InvertedIndex.
+ *
+ * @constructor
+ * @param {function} tokenizer - Tokenizer function.
+ */
+function InvertedIndex(descriptor) {
+  this.clear();
+
+  if (Array.isArray(descriptor)) {
+    this.documentTokenizer = descriptor[0];
+    this.queryTokenizer = descriptor[1];
+  }
+  else {
+    this.documentTokenizer = descriptor;
+    this.queryTokenizer = descriptor;
+  }
+
+  if (!this.documentTokenizer)
+    this.documentTokenizer = identity;
+  if (!this.queryTokenizer)
+    this.queryTokenizer = identity;
+
+  if (typeof this.documentTokenizer !== 'function')
+    throw new Error('mnemonist/InvertedIndex.constructor: document tokenizer is not a function.');
+
+  if (typeof this.queryTokenizer !== 'function')
+    throw new Error('mnemonist/InvertedIndex.constructor: query tokenizer is not a function.');
+}
+
+/**
+ * Method used to clear the InvertedIndex.
+ *
+ * @return {undefined}
+ */
+InvertedIndex.prototype.clear = function() {
+
+  // Properties
+  this.items = [];
+  this.mapping = new Map();
+  this.size = 0;
+  this.dimension = 0;
+};
+
+/**
+ * Method used to add a document to the index.
+ *
+ * @param  {any} doc - Item to add.
+ * @return {InvertedIndex}
+ */
+InvertedIndex.prototype.add = function(doc) {
+
+  // Increasing size
+  this.size++;
+
+  // Storing document
+  var key = this.items.length;
+  this.items.push(doc);
+
+  // Tokenizing the document
+  var tokens = this.documentTokenizer(doc);
+
+  if (!Array.isArray(tokens))
+    throw new Error('mnemonist/InvertedIndex.add: tokenizer function should return an array of tokens.');
+
+  // Indexing
+  var done = new Set(),
+      token,
+      container;
+
+  for (var i = 0, l = tokens.length; i < l; i++) {
+    token = tokens[i];
+
+    if (done.has(token))
+      continue;
+
+    done.add(token);
+
+    container = this.mapping.get(token);
+
+    if (!container) {
+      container = [];
+      this.mapping.set(token, container);
+    }
+
+    container.push(key);
+  }
+
+  this.dimension = this.mapping.size;
+
+  return this;
+};
+
+/**
+ * Method used to query the index in a AND fashion.
+ *
+ * @param  {any} query - Query
+ * @return {Set}       - Intersection of documents matching the query.
+ */
+InvertedIndex.prototype.get = function(query) {
+
+  // Early termination
+  if (!this.size)
+    return [];
+
+  // First we need to tokenize the query
+  var tokens = this.queryTokenizer(query);
+
+  if (!Array.isArray(tokens))
+    throw new Error('mnemonist/InvertedIndex.query: tokenizer function should return an array of tokens.');
+
+  if (!tokens.length)
+    return [];
+
+  var results = this.mapping.get(tokens[0]),
+      c,
+      i,
+      l;
+
+  if (typeof results === 'undefined' || results.length === 0)
+    return [];
+
+  if (tokens.length > 1) {
+    for (i = 1, l = tokens.length; i < l; i++) {
+      c = this.mapping.get(tokens[i]);
+
+      if (typeof c === 'undefined' || c.length === 0)
+        return [];
+
+      results = helpers.intersectionUniqueArrays(results, c);
+    }
+  }
+
+  var docs = new Array(results.length);
+
+  for (i = 0, l = docs.length; i < l; i++)
+    docs[i] = this.items[results[i]];
+
+  return docs;
+};
+
+/**
+ * Method used to iterate over each of the documents.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+InvertedIndex.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = 0, l = this.documents.length; i < l; i++)
+    callback.call(scope, this.documents[i], i, this);
+};
+
+/**
+ * Method returning an iterator over the index's documents.
+ *
+ * @return {Iterator}
+ */
+InvertedIndex.prototype.documents = function() {
+  var documents = this.items,
+      l = documents.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+      var value = documents[i++];
+
+      return {
+        value: value,
+        done: false
+      };
+  });
+};
+
+/**
+ * Method returning an iterator over the index's tokens.
+ *
+ * @return {Iterator}
+ */
+InvertedIndex.prototype.tokens = function() {
+  return this.mapping.keys();
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  InvertedIndex.prototype[Symbol.iterator] = InvertedIndex.prototype.documents;
+
+/**
+ * Convenience known methods.
+ */
+InvertedIndex.prototype.inspect = function() {
+  var array = this.items.slice();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: InvertedIndex,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  InvertedIndex.prototype[Symbol.for('nodejs.util.inspect.custom')] = InvertedIndex.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a InvertedIndex.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} tokenizer - Tokenizer function.
+ * @return {InvertedIndex}
+ */
+InvertedIndex.from = function(iterable, descriptor) {
+  var index = new InvertedIndex(descriptor);
+
+  forEach(iterable, function(doc) {
+    index.add(doc);
+  });
+
+  return index;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = InvertedIndex;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a4adc66608e6d2d109e0d1978ec50636c7cbd886
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.d.ts
@@ -0,0 +1,25 @@
+/**
+ * Mnemonist KDTree Typings
+ * =========================
+ */
+import {IArrayLike} from './utils/types';
+
+export default class KDTree<V> {
+
+  // Members
+  dimensions: number;
+  size: number;
+  visited: number;
+
+  // Methods
+  nearestNeighbor(point: Array<number>): V;
+  kNearestNeighbors(k: number, point: Array<number>): Array<V>;
+  linearKNearestNeighbors(k: number, point: Array<number>): Array<V>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<[I, Array<number>]>, dimensions: number): KDTree<I>;
+  static fromAxes(axes: IArrayLike): KDTree<number>;
+  static fromAxes<I>(axes: IArrayLike, labels: Array<I>): KDTree<I>;
+}
+
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.js b/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe5d1ca7b6a165a11e23ae8e7a766084e529fbfe
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/kd-tree.js
@@ -0,0 +1,447 @@
+/**
+ * Mnemonist KDTree
+ * =================
+ *
+ * Low-level JavaScript implementation of a k-dimensional tree.
+ */
+var iterables = require('./utils/iterables.js');
+var typed = require('./utils/typed-arrays.js');
+var createTupleComparator = require('./utils/comparators.js').createTupleComparator;
+var FixedReverseHeap = require('./fixed-reverse-heap.js');
+var inplaceQuickSortIndices = require('./sort/quick.js').inplaceQuickSortIndices;
+
+/**
+ * Helper function used to compute the squared distance between a query point
+ * and an indexed points whose values are stored in a tree's axes.
+ *
+ * Note that squared distance is used instead of euclidean to avoid
+ * costly sqrt computations.
+ *
+ * @param  {number} dimensions - Number of dimensions.
+ * @param  {array}  axes       - Axes data.
+ * @param  {number} pivot      - Pivot.
+ * @param  {array}  point      - Query point.
+ * @return {number}
+ */
+function squaredDistanceAxes(dimensions, axes, pivot, b) {
+  var d;
+
+  var dist = 0,
+      step;
+
+  for (d = 0; d < dimensions; d++) {
+    step = axes[d][pivot] - b[d];
+    dist += step * step;
+  }
+
+  return dist;
+}
+
+/**
+ * Helper function used to reshape input data into low-level axes data.
+ *
+ * @param  {number} dimensions - Number of dimensions.
+ * @param  {array}  data       - Data in the shape [label, [x, y, z...]]
+ * @return {object}
+ */
+function reshapeIntoAxes(dimensions, data) {
+  var l = data.length;
+
+  var axes = new Array(dimensions),
+      labels = new Array(l),
+      axis;
+
+  var PointerArray = typed.getPointerArray(l);
+
+  var ids = new PointerArray(l);
+
+  var d, i, row;
+
+  var f = true;
+
+  for (d = 0; d < dimensions; d++) {
+    axis = new Float64Array(l);
+
+    for (i = 0; i < l; i++) {
+      row = data[i];
+      axis[i] = row[1][d];
+
+      if (f) {
+        labels[i] = row[0];
+        ids[i] = i;
+      }
+    }
+
+    f = false;
+    axes[d] = axis;
+  }
+
+  return {axes: axes, ids: ids, labels: labels};
+}
+
+/**
+ * Helper function used to build a kd-tree from axes data.
+ *
+ * @param  {number} dimensions - Number of dimensions.
+ * @param  {array}  axes       - Axes.
+ * @param  {array}  ids        - Indices to sort.
+ * @param  {array}  labels     - Point labels.
+ * @return {object}
+ */
+function buildTree(dimensions, axes, ids, labels) {
+  var l = labels.length;
+
+  // NOTE: +1 because we need to keep 0 as null pointer
+  var PointerArray = typed.getPointerArray(l + 1);
+
+  // Building the tree
+  var pivots = new PointerArray(l),
+      lefts = new PointerArray(l),
+      rights = new PointerArray(l);
+
+  var stack = [[0, 0, ids.length, -1, 0]],
+      step,
+      parent,
+      direction,
+      median,
+      pivot,
+      lo,
+      hi;
+
+  var d, i = 0;
+
+  while (stack.length !== 0) {
+    step = stack.pop();
+
+    d = step[0];
+    lo = step[1];
+    hi = step[2];
+    parent = step[3];
+    direction = step[4];
+
+    inplaceQuickSortIndices(axes[d], ids, lo, hi);
+
+    l = hi - lo;
+    median = lo + (l >>> 1); // Fancy floor(l / 2)
+    pivot = ids[median];
+    pivots[i] = pivot;
+
+    if (parent > -1) {
+      if (direction === 0)
+        lefts[parent] = i + 1;
+      else
+        rights[parent] = i + 1;
+    }
+
+    d = (d + 1) % dimensions;
+
+    // Right
+    if (median !== lo && median !== hi - 1) {
+      stack.push([d, median + 1, hi, i, 1]);
+    }
+
+    // Left
+    if (median !== lo) {
+      stack.push([d, lo, median, i, 0]);
+    }
+
+    i++;
+  }
+
+  return {
+    axes: axes,
+    labels: labels,
+    pivots: pivots,
+    lefts: lefts,
+    rights: rights
+  };
+}
+
+/**
+ * KDTree.
+ *
+ * @constructor
+ */
+function KDTree(dimensions, build) {
+  this.dimensions = dimensions;
+  this.visited = 0;
+
+  this.axes = build.axes;
+  this.labels = build.labels;
+
+  this.pivots = build.pivots;
+  this.lefts = build.lefts;
+  this.rights = build.rights;
+
+  this.size = this.labels.length;
+}
+
+/**
+ * Method returning the query's nearest neighbor.
+ *
+ * @param  {array}  query - Query point.
+ * @return {any}
+ */
+KDTree.prototype.nearestNeighbor = function(query) {
+  var bestDistance = Infinity,
+      best = null;
+
+  var dimensions = this.dimensions,
+      axes = this.axes,
+      pivots = this.pivots,
+      lefts = this.lefts,
+      rights = this.rights;
+
+  var visited = 0;
+
+  function recurse(d, node) {
+    visited++;
+
+    var left = lefts[node],
+        right = rights[node],
+        pivot = pivots[node];
+
+    var dist = squaredDistanceAxes(
+      dimensions,
+      axes,
+      pivot,
+      query
+    );
+
+    if (dist < bestDistance) {
+      best = pivot;
+      bestDistance = dist;
+
+      if (dist === 0)
+        return;
+    }
+
+    var dx = axes[d][pivot] - query[d];
+
+    d = (d + 1) % dimensions;
+
+    // Going the correct way?
+    if (dx > 0) {
+      if (left !== 0)
+        recurse(d, left - 1);
+    }
+    else {
+      if (right !== 0)
+        recurse(d, right - 1);
+    }
+
+    // Going the other way?
+    if (dx * dx < bestDistance) {
+      if (dx > 0) {
+        if (right !== 0)
+          recurse(d, right - 1);
+      }
+      else {
+        if (left !== 0)
+          recurse(d, left - 1);
+      }
+    }
+  }
+
+  recurse(0, 0);
+
+  this.visited = visited;
+  return this.labels[best];
+};
+
+var KNN_HEAP_COMPARATOR_3 = createTupleComparator(3);
+var KNN_HEAP_COMPARATOR_2 = createTupleComparator(2);
+
+/**
+ * Method returning the query's k nearest neighbors.
+ *
+ * @param  {number} k     - Number of nearest neighbor to retrieve.
+ * @param  {array}  query - Query point.
+ * @return {array}
+ */
+
+// TODO: can do better by improving upon static-kdtree here
+KDTree.prototype.kNearestNeighbors = function(k, query) {
+  if (k <= 0)
+    throw new Error('mnemonist/kd-tree.kNearestNeighbors: k should be a positive number.');
+
+  k = Math.min(k, this.size);
+
+  if (k === 1)
+    return [this.nearestNeighbor(query)];
+
+  var heap = new FixedReverseHeap(Array, KNN_HEAP_COMPARATOR_3, k);
+
+  var dimensions = this.dimensions,
+      axes = this.axes,
+      pivots = this.pivots,
+      lefts = this.lefts,
+      rights = this.rights;
+
+  var visited = 0;
+
+  function recurse(d, node) {
+    var left = lefts[node],
+        right = rights[node],
+        pivot = pivots[node];
+
+    var dist = squaredDistanceAxes(
+      dimensions,
+      axes,
+      pivot,
+      query
+    );
+
+    heap.push([dist, visited++, pivot]);
+
+    var point = query[d],
+        split = axes[d][pivot],
+        dx = point - split;
+
+    d = (d + 1) % dimensions;
+
+    // Going the correct way?
+    if (point < split) {
+      if (left !== 0) {
+        recurse(d, left - 1);
+      }
+    }
+    else {
+      if (right !== 0) {
+        recurse(d, right - 1);
+      }
+    }
+
+    // Going the other way?
+    if (dx * dx < heap.peek()[0] || heap.size < k) {
+      if (point < split) {
+        if (right !== 0) {
+          recurse(d, right - 1);
+        }
+      }
+      else {
+        if (left !== 0) {
+          recurse(d, left - 1);
+        }
+      }
+    }
+  }
+
+  recurse(0, 0);
+
+  this.visited = visited;
+
+  var best = heap.consume();
+
+  for (var i = 0; i < best.length; i++)
+    best[i] = this.labels[best[i][2]];
+
+  return best;
+};
+
+/**
+ * Method returning the query's k nearest neighbors by linear search.
+ *
+ * @param  {number} k     - Number of nearest neighbor to retrieve.
+ * @param  {array}  query - Query point.
+ * @return {array}
+ */
+KDTree.prototype.linearKNearestNeighbors = function(k, query) {
+  if (k <= 0)
+    throw new Error('mnemonist/kd-tree.kNearestNeighbors: k should be a positive number.');
+
+  k = Math.min(k, this.size);
+
+  var heap = new FixedReverseHeap(Array, KNN_HEAP_COMPARATOR_2, k);
+
+  var i, l, dist;
+
+  for (i = 0, l = this.size; i < l; i++) {
+    dist = squaredDistanceAxes(
+      this.dimensions,
+      this.axes,
+      this.pivots[i],
+      query
+    );
+
+    heap.push([dist, i]);
+  }
+
+  var best = heap.consume();
+
+  for (i = 0; i < best.length; i++)
+    best[i] = this.labels[this.pivots[best[i][1]]];
+
+  return best;
+};
+
+/**
+ * Convenience known methods.
+ */
+KDTree.prototype.inspect = function() {
+  var dummy = new Map();
+
+  dummy.dimensions = this.dimensions;
+
+  Object.defineProperty(dummy, 'constructor', {
+    value: KDTree,
+    enumerable: false
+  });
+
+  var i, j, point;
+
+  for (i = 0; i < this.size; i++) {
+    point = new Array(this.dimensions);
+
+    for (j = 0; j < this.dimensions; j++)
+      point[j] = this.axes[j][i];
+
+    dummy.set(this.labels[i], point);
+  }
+
+  return dummy;
+};
+
+if (typeof Symbol !== 'undefined')
+  KDTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = KDTree.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {number}   dimensions - Space dimensions.
+ * @return {KDTree}
+ */
+KDTree.from = function(iterable, dimensions) {
+  var data = iterables.toArray(iterable);
+
+  var reshaped = reshapeIntoAxes(dimensions, data);
+
+  var result = buildTree(dimensions, reshaped.axes, reshaped.ids, reshaped.labels);
+
+  return new KDTree(dimensions, result);
+};
+
+/**
+ * Static @.from function building a KDTree from given axes.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {number}   dimensions - Space dimensions.
+ * @return {KDTree}
+ */
+KDTree.fromAxes = function(axes, labels) {
+  if (!labels)
+    labels = typed.indices(axes[0].length);
+
+  var dimensions = axes.length;
+
+  var result = buildTree(axes.length, axes, typed.indices(labels.length), labels);
+
+  return new KDTree(dimensions, result);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = KDTree;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/linked-list.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/linked-list.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4eec48c703dc0cf18ded73cf1636db5f59f8a455
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/linked-list.d.ts
@@ -0,0 +1,29 @@
+/**
+ * Mnemonist LinkedList Typings
+ * =============================
+ */
+export default class LinkedList<T> implements Iterable<T> {
+
+  // Members
+  size: number;
+
+  // Methods
+  clear(): void;
+  first(): T | undefined;
+  last(): T | undefined;
+  peek(): T | undefined;
+  push(value: T): number;
+  shift(): T | undefined;
+  unshift(value: T): number;
+  forEach(callback: (value: T, index: number, list: this) => void, scope?: any): void;
+  toArray(): Array<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  toString(): string;
+  toJSON(): Array<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): LinkedList<I>;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/linked-list.js b/libs/shared/graph-layout/node_modules/mnemonist/linked-list.js
new file mode 100644
index 0000000000000000000000000000000000000000..17dca06d6134866e631cecbbf58f64d5b00096f7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/linked-list.js
@@ -0,0 +1,261 @@
+/**
+ * Mnemonist Linked List
+ * ======================
+ *
+ * Singly linked list implementation. Uses raw JavaScript objects as nodes
+ * as benchmarks proved it was the fastest thing to do.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach');
+
+/**
+ * Linked List.
+ *
+ * @constructor
+ */
+function LinkedList() {
+  this.clear();
+}
+
+/**
+ * Method used to clear the list.
+ *
+ * @return {undefined}
+ */
+LinkedList.prototype.clear = function() {
+
+  // Properties
+  this.head = null;
+  this.tail = null;
+  this.size = 0;
+};
+
+/**
+ * Method used to get the first item of the list.
+ *
+ * @return {any}
+ */
+LinkedList.prototype.first = function() {
+  return this.head ? this.head.item : undefined;
+};
+LinkedList.prototype.peek = LinkedList.prototype.first;
+
+/**
+ * Method used to get the last item of the list.
+ *
+ * @return {any}
+ */
+LinkedList.prototype.last = function() {
+  return this.tail ? this.tail.item : undefined;
+};
+
+/**
+ * Method used to add an item at the end of the list.
+ *
+ * @param  {any}    item - The item to add.
+ * @return {number}
+ */
+LinkedList.prototype.push = function(item) {
+  var node = {item: item, next: null};
+
+  if (!this.head) {
+    this.head = node;
+    this.tail = node;
+  }
+  else {
+    this.tail.next = node;
+    this.tail = node;
+  }
+
+  this.size++;
+
+  return this.size;
+};
+
+/**
+ * Method used to add an item at the beginning of the list.
+ *
+ * @param  {any}    item - The item to add.
+ * @return {number}
+ */
+LinkedList.prototype.unshift = function(item) {
+  var node = {item: item, next: null};
+
+  if (!this.head) {
+    this.head = node;
+    this.tail = node;
+  }
+  else {
+    if (!this.head.next)
+      this.tail = this.head;
+    node.next = this.head;
+    this.head = node;
+  }
+
+  this.size++;
+
+  return this.size;
+};
+
+/**
+ * Method used to retrieve & remove the first item of the list.
+ *
+ * @return {any}
+ */
+LinkedList.prototype.shift = function() {
+  if (!this.size)
+    return undefined;
+
+  var node = this.head;
+
+  this.head = node.next;
+  this.size--;
+
+  return node.item;
+};
+
+/**
+ * Method used to iterate over the list.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+LinkedList.prototype.forEach = function(callback, scope) {
+  if (!this.size)
+    return;
+
+  scope = arguments.length > 1 ? scope : this;
+
+  var n = this.head,
+      i = 0;
+
+  while (n) {
+    callback.call(scope, n.item, i, this);
+    n = n.next;
+    i++;
+  }
+};
+
+/**
+ * Method used to convert the list into an array.
+ *
+ * @return {array}
+ */
+LinkedList.prototype.toArray = function() {
+  if (!this.size)
+    return [];
+
+  var array = new Array(this.size);
+
+  for (var i = 0, l = this.size, n = this.head; i < l; i++) {
+    array[i] = n.item;
+    n = n.next;
+  }
+
+  return array;
+};
+
+/**
+ * Method used to create an iterator over a list's values.
+ *
+ * @return {Iterator}
+ */
+LinkedList.prototype.values = function() {
+  var n = this.head;
+
+  return new Iterator(function() {
+    if (!n)
+      return {
+        done: true
+      };
+
+    var value = n.item;
+    n = n.next;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a list's entries.
+ *
+ * @return {Iterator}
+ */
+LinkedList.prototype.entries = function() {
+  var n = this.head,
+      i = 0;
+
+  return new Iterator(function() {
+    if (!n)
+      return {
+        done: true
+      };
+
+    var value = n.item;
+    n = n.next;
+    i++;
+
+    return {
+      value: [i - 1, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  LinkedList.prototype[Symbol.iterator] = LinkedList.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+LinkedList.prototype.toString = function() {
+  return this.toArray().join(',');
+};
+
+LinkedList.prototype.toJSON = function() {
+  return this.toArray();
+};
+
+LinkedList.prototype.inspect = function() {
+  var array = this.toArray();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: LinkedList,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  LinkedList.prototype[Symbol.for('nodejs.util.inspect.custom')] = LinkedList.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a list.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @return {LinkedList}
+ */
+LinkedList.from = function(iterable) {
+  var list = new LinkedList();
+
+  forEach(iterable, function(value) {
+    list.push(value);
+  });
+
+  return list;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = LinkedList;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..30b06d1b7409b78880a1a041e095abca66485a67
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.d.ts
@@ -0,0 +1,13 @@
+/**
+ * Mnemonist LRUCacheWithDelete Typings
+ * =====================================
+ */
+ import LRUCache from './lru-cache';
+
+ export default class LRUCacheWithDelete<K, V> extends LRUCache<K, V> {
+
+   delete(key: K): boolean;
+
+   remove<T>(key: K, missing?: T): V | T;
+
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.js b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1d66867b63411f5da55984023df6116c0037811
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache-with-delete.js
@@ -0,0 +1,287 @@
+/**
+ * Mnemonist LRUCacheWithDelete
+ * =============================
+ *
+ * An extension of LRUCache with delete functionality.
+ */
+
+var LRUCache = require('./lru-cache.js'),
+    forEach = require('obliterator/foreach'),
+    typed = require('./utils/typed-arrays.js'),
+    iterables = require('./utils/iterables.js');
+
+// The only complication with deleting items is that the LRU's
+// performance depends on having a fixed-size list of pointers; the
+// doubly-linked-list is happy to expand and contract.
+//
+// On delete, we record the position of the former item's pointer in a
+// list of "holes" in the pointer array. On insert, if there is a hole
+// the new pointer slots in to fill the hole; otherwise, it is
+// appended as usual. (Note: we are only talking here about the
+// internal pointer list. setting or getting an item promotes it
+// to the top of the LRU ranking no matter what came before)
+
+function LRUCacheWithDelete(Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    LRUCache.call(this, Keys);
+  }
+  else {
+    LRUCache.call(this, Keys, Values, capacity);
+  }
+  var PointerArray = typed.getPointerArray(this.capacity);
+  this.deleted = new PointerArray(this.capacity);
+  this.deletedSize = 0;
+}
+
+for (var k in LRUCache.prototype)
+  LRUCacheWithDelete.prototype[k] = LRUCache.prototype[k];
+if (typeof Symbol !== 'undefined')
+  LRUCacheWithDelete.prototype[Symbol.iterator] = LRUCache.prototype[Symbol.iterator];
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+ LRUCacheWithDelete.prototype.clear = function() {
+  LRUCache.prototype.clear.call(this);
+  this.deletedSize = 0;
+};
+
+/**
+ * Method used to set the value for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {undefined}
+ */
+LRUCacheWithDelete.prototype.set = function(key, value) {
+
+  var pointer = this.items[key];
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    this.V[pointer] = value;
+
+    return;
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    if (this.deletedSize > 0) {
+      // If there is a "hole" in the pointer list, reuse it
+      pointer = this.deleted[--this.deletedSize];
+    }
+    else {
+      // otherwise append to the pointer list
+      pointer = this.size;
+    }
+    this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    delete this.items[this.K[pointer]];
+  }
+
+  // Storing key & value
+  this.items[key] = pointer;
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+};
+
+/**
+ * Method used to set the value for the given key in the cache
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {{evicted: boolean, key: any, value: any}} An object containing the
+ * key and value of an item that was overwritten or evicted in the set
+ * operation, as well as a boolean indicating whether it was evicted due to
+ * limited capacity. Return value is null if nothing was evicted or overwritten
+ * during the set operation.
+ */
+LRUCacheWithDelete.prototype.setpop = function(key, value) {
+  var oldValue = null;
+  var oldKey = null;
+
+  var pointer = this.items[key];
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    oldValue = this.V[pointer];
+    this.V[pointer] = value;
+    return {evicted: false, key: key, value: oldValue};
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    if (this.deletedSize > 0) {
+      // If there is a "hole" in the pointer list, reuse it
+      pointer = this.deleted[--this.deletedSize];
+    }
+    else {
+      // otherwise append to the pointer list
+      pointer = this.size;
+    }
+    this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    oldValue = this.V[pointer];
+    oldKey = this.K[pointer];
+    delete this.items[this.K[pointer]];
+  }
+
+  // Storing key & value
+  this.items[key] = pointer;
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+
+  // Return object if eviction took place, otherwise return null
+  if (oldKey) {
+    return {evicted: true, key: oldKey, value: oldValue};
+  }
+  else {
+    return null;
+  }
+};
+
+/**
+ * Method used to delete the entry for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @return {boolean}   - true if the item was present
+ */
+LRUCacheWithDelete.prototype.delete = function(key) {
+
+  var pointer = this.items[key];
+
+  if (typeof pointer === 'undefined') {
+    return false;
+  }
+
+  delete this.items[key];
+
+  if (this.size === 1) {
+    this.size = 0;
+    this.head = 0;
+    this.tail = 0;
+    this.deletedSize = 0;
+    return true;
+  }
+
+  var previous = this.backward[pointer],
+      next = this.forward[pointer];
+
+  if (this.head === pointer) {
+    this.head = next;
+  }
+  if (this.tail === pointer) {
+    this.tail = previous;
+  }
+
+  this.forward[previous] = next;
+  this.backward[next] = previous;
+
+  this.size--;
+  this.deleted[this.deletedSize++] = pointer;
+
+  return true;
+};
+
+/**
+ * Method used to remove and return the value for the given key in the cache.
+ *
+ * @param  {any} key                 - Key.
+ * @param  {any} [missing=undefined] - Value to return if item is absent
+ * @return {any} The value, if present; the missing indicator if absent
+ */
+LRUCacheWithDelete.prototype.remove = function(key, missing = undefined) {
+
+  var pointer = this.items[key];
+
+  if (typeof pointer === 'undefined') {
+    return missing;
+  }
+
+  var dead = this.V[pointer];
+  delete this.items[key];
+
+  if (this.size === 1) {
+    this.size = 0;
+    this.head = 0;
+    this.tail = 0;
+    this.deletedSize = 0;
+    return dead;
+  }
+
+  var previous = this.backward[pointer],
+      next = this.forward[pointer];
+
+  if (this.head === pointer) {
+    this.head = next;
+  }
+  if (this.tail === pointer) {
+    this.tail = previous;
+  }
+
+  this.forward[previous] = next;
+  this.backward[next] = previous;
+
+  this.size--;
+  this.deleted[this.deletedSize++] = pointer;
+
+  return dead;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} Keys     - Array class for storing keys.
+ * @param  {function} Values   - Array class for storing values.
+ * @param  {number}   capacity - Cache's capacity.
+ * @return {LRUCacheWithDelete}
+ */
+ LRUCacheWithDelete.from = function(iterable, Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/lru-cache.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+  else if (arguments.length === 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  var cache = new LRUCacheWithDelete(Keys, Values, capacity);
+
+  forEach(iterable, function(value, key) {
+    cache.set(key, value);
+  });
+
+  return cache;
+};
+
+module.exports = LRUCacheWithDelete;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..45b61e0be94d4ba953c7535c7d1356a9bf1fe304
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.d.ts
@@ -0,0 +1,43 @@
+/**
+ * Mnemonist LRUCache Typings
+ * ===========================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+export default class LRUCache<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(capacity: number);
+  constructor(KeyArrayClass: IArrayLikeConstructor, ValueArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  setpop(key: K, value: V): {evicted: boolean, key: K, value: V};
+  get(key: K): V | undefined;
+  peek(key: K): V | undefined;
+  has(key: K): boolean;
+  forEach(callback: (value: V, key: K, cache: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    KeyArrayClass: IArrayLikeConstructor,
+    ValueArrayClass: IArrayLikeConstructor,
+    capacity?: number
+  ): LRUCache<I, J>;
+
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    capacity?: number
+  ): LRUCache<I, J>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.js b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.js
new file mode 100644
index 0000000000000000000000000000000000000000..d225caebfb91064fe0869a0f7df2eaf425d96bbf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-cache.js
@@ -0,0 +1,436 @@
+/**
+ * Mnemonist LRUCache
+ * ===================
+ *
+ * JavaScript implementation of the LRU Cache data structure. To save up
+ * memory and allocations this implementation represents its underlying
+ * doubly-linked list as static arrays and pointers. Thus, memory is allocated
+ * only once at instantiation and JS objects are never created to serve as
+ * pointers. This also means this implementation does not trigger too many
+ * garbage collections.
+ *
+ * Note that to save up memory, a LRU Cache can be implemented using a singly
+ * linked list by storing predecessors' pointers as hashmap values.
+ * However, this means more hashmap lookups and would probably slow the whole
+ * thing down. What's more, pointers are not the things taking most space in
+ * memory.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach'),
+    typed = require('./utils/typed-arrays.js'),
+    iterables = require('./utils/iterables.js');
+
+/**
+ * LRUCache.
+ *
+ * @constructor
+ * @param {function} Keys     - Array class for storing keys.
+ * @param {function} Values   - Array class for storing values.
+ * @param {number}   capacity - Desired capacity.
+ */
+function LRUCache(Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  this.capacity = capacity;
+
+  if (typeof this.capacity !== 'number' || this.capacity <= 0)
+    throw new Error('mnemonist/lru-cache: capacity should be positive number.');
+  else if (!isFinite(this.capacity) || Math.floor(this.capacity) !== this.capacity)
+      throw new Error('mnemonist/lru-cache: capacity should be a finite positive integer.');
+
+  var PointerArray = typed.getPointerArray(capacity);
+
+  this.forward = new PointerArray(capacity);
+  this.backward = new PointerArray(capacity);
+  this.K = typeof Keys === 'function' ? new Keys(capacity) : new Array(capacity);
+  this.V = typeof Values === 'function' ? new Values(capacity) : new Array(capacity);
+
+  // Properties
+  this.size = 0;
+  this.head = 0;
+  this.tail = 0;
+  this.items = {};
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+LRUCache.prototype.clear = function() {
+  this.size = 0;
+  this.head = 0;
+  this.tail = 0;
+  this.items = {};
+};
+
+/**
+ * Method used to splay a value on top.
+ *
+ * @param  {number}   pointer - Pointer of the value to splay on top.
+ * @return {LRUCache}
+ */
+LRUCache.prototype.splayOnTop = function(pointer) {
+  var oldHead = this.head;
+
+  if (this.head === pointer)
+    return this;
+
+  var previous = this.backward[pointer],
+      next = this.forward[pointer];
+
+  if (this.tail === pointer) {
+    this.tail = previous;
+  }
+  else {
+    this.backward[next] = previous;
+  }
+
+  this.forward[previous] = next;
+
+  this.backward[oldHead] = pointer;
+  this.head = pointer;
+  this.forward[pointer] = oldHead;
+
+  return this;
+};
+
+/**
+ * Method used to set the value for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {undefined}
+ */
+LRUCache.prototype.set = function(key, value) {
+
+  var pointer = this.items[key];
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    this.V[pointer] = value;
+
+    return;
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    pointer = this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    delete this.items[this.K[pointer]];
+  }
+
+  // Storing key & value
+  this.items[key] = pointer;
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+};
+
+/**
+ * Method used to set the value for the given key in the cache
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {{evicted: boolean, key: any, value: any}} An object containing the
+ * key and value of an item that was overwritten or evicted in the set
+ * operation, as well as a boolean indicating whether it was evicted due to
+ * limited capacity. Return value is null if nothing was evicted or overwritten
+ * during the set operation.
+ */
+LRUCache.prototype.setpop = function(key, value) {
+  var oldValue = null;
+  var oldKey = null;
+
+  var pointer = this.items[key];
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    oldValue = this.V[pointer];
+    this.V[pointer] = value;
+    return {evicted: false, key: key, value: oldValue};
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    pointer = this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    oldValue = this.V[pointer];
+    oldKey = this.K[pointer];
+    delete this.items[this.K[pointer]];
+  }
+
+  // Storing key & value
+  this.items[key] = pointer;
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+
+  // Return object if eviction took place, otherwise return null
+  if (oldKey) {
+    return {evicted: true, key: oldKey, value: oldValue};
+  }
+  else {
+    return null;
+  }
+};
+
+/**
+ * Method used to check whether the key exists in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @return {boolean}
+ */
+LRUCache.prototype.has = function(key) {
+  return key in this.items;
+};
+
+/**
+ * Method used to get the value attached to the given key. Will move the
+ * related key to the front of the underlying linked list.
+ *
+ * @param  {any} key   - Key.
+ * @return {any}
+ */
+LRUCache.prototype.get = function(key) {
+  var pointer = this.items[key];
+
+  if (typeof pointer === 'undefined')
+    return;
+
+  this.splayOnTop(pointer);
+
+  return this.V[pointer];
+};
+
+/**
+ * Method used to get the value attached to the given key. Does not modify
+ * the ordering of the underlying linked list.
+ *
+ * @param  {any} key   - Key.
+ * @return {any}
+ */
+LRUCache.prototype.peek = function(key) {
+    var pointer = this.items[key];
+
+    if (typeof pointer === 'undefined')
+        return;
+
+    return this.V[pointer];
+};
+
+/**
+ * Method used to iterate over the cache's entries using a callback.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+LRUCache.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var i = 0,
+      l = this.size;
+
+  var pointer = this.head,
+      keys = this.K,
+      values = this.V,
+      forward = this.forward;
+
+  while (i < l) {
+
+    callback.call(scope, values[pointer], keys[pointer], this);
+    pointer = forward[pointer];
+
+    i++;
+  }
+};
+
+/**
+ * Method used to create an iterator over the cache's keys from most
+ * recently used to least recently used.
+ *
+ * @return {Iterator}
+ */
+LRUCache.prototype.keys = function() {
+  var i = 0,
+      l = this.size;
+
+  var pointer = this.head,
+      keys = this.K,
+      forward = this.forward;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    var key = keys[pointer];
+
+    i++;
+
+    if (i < l)
+      pointer = forward[pointer];
+
+    return {
+      done: false,
+      value: key
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over the cache's values from most
+ * recently used to least recently used.
+ *
+ * @return {Iterator}
+ */
+LRUCache.prototype.values = function() {
+  var i = 0,
+      l = this.size;
+
+  var pointer = this.head,
+      values = this.V,
+      forward = this.forward;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    var value = values[pointer];
+
+    i++;
+
+    if (i < l)
+      pointer = forward[pointer];
+
+    return {
+      done: false,
+      value: value
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over the cache's entries from most
+ * recently used to least recently used.
+ *
+ * @return {Iterator}
+ */
+LRUCache.prototype.entries = function() {
+  var i = 0,
+      l = this.size;
+
+  var pointer = this.head,
+      keys = this.K,
+      values = this.V,
+      forward = this.forward;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    var key = keys[pointer],
+        value = values[pointer];
+
+    i++;
+
+    if (i < l)
+      pointer = forward[pointer];
+
+    return {
+      done: false,
+      value: [key, value]
+    };
+  });
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  LRUCache.prototype[Symbol.iterator] = LRUCache.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+LRUCache.prototype.inspect = function() {
+  var proxy = new Map();
+
+  var iterator = this.entries(),
+      step;
+
+  while ((step = iterator.next(), !step.done))
+    proxy.set(step.value[0], step.value[1]);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: LRUCache,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  LRUCache.prototype[Symbol.for('nodejs.util.inspect.custom')] = LRUCache.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} Keys     - Array class for storing keys.
+ * @param  {function} Values   - Array class for storing values.
+ * @param  {number}   capacity - Cache's capacity.
+ * @return {LRUCache}
+ */
+LRUCache.from = function(iterable, Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/lru-cache.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+  else if (arguments.length === 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  var cache = new LRUCache(Keys, Values, capacity);
+
+  forEach(iterable, function(value, key) {
+    cache.set(key, value);
+  });
+
+  return cache;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = LRUCache;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e50ba06611316c7aa0736ac5a442d2015e0198ad
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.d.ts
@@ -0,0 +1,13 @@
+/**
+ * Mnemonist LRUMapWithDelete Typings
+ * ===================================
+ */
+ import LRUMap from './lru-map';
+
+ export default class LRUMapWithDelete<K, V> extends LRUMap<K, V> {
+
+   delete(key: K): boolean;
+
+   remove<T>(key: K, missing?: T): V | T;
+
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.js b/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..3bb1a973f239196d65136823c3db705f50ae0616
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-map-with-delete.js
@@ -0,0 +1,287 @@
+/**
+ * Mnemonist LRUMapWithDelete
+ * ===========================
+ *
+ * An extension of LRUMap with delete functionality.
+ */
+
+var LRUMap = require('./lru-map.js'),
+    forEach = require('obliterator/foreach'),
+    typed = require('./utils/typed-arrays.js'),
+    iterables = require('./utils/iterables.js');
+
+// The only complication with deleting items is that the LRU's
+// performance depends on having a fixed-size list of pointers; the
+// doubly-linked-list is happy to expand and contract.
+//
+// On delete, we record the position of the former item's pointer in a
+// list of "holes" in the pointer array. On insert, if there is a hole
+// the new pointer slots in to fill the hole; otherwise, it is
+// appended as usual. (Note: we are only talking here about the
+// internal pointer list. setting or getting an item promotes it
+// to the top of the LRU ranking no matter what came before)
+
+function LRUMapWithDelete(Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    LRUMap.call(this, Keys);
+  }
+  else {
+    LRUMap.call(this, Keys, Values, capacity);
+  }
+  var PointerArray = typed.getPointerArray(this.capacity);
+  this.deleted = new PointerArray(this.capacity);
+  this.deletedSize = 0;
+}
+
+for (var k in LRUMap.prototype)
+  LRUMapWithDelete.prototype[k] = LRUMap.prototype[k];
+if (typeof Symbol !== 'undefined')
+  LRUMapWithDelete.prototype[Symbol.iterator] = LRUMap.prototype[Symbol.iterator];
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+ LRUMapWithDelete.prototype.clear = function() {
+  LRUMap.prototype.clear.call(this);
+  this.deletedSize = 0;
+};
+
+/**
+ * Method used to set the value for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {undefined}
+ */
+LRUMapWithDelete.prototype.set = function(key, value) {
+
+  var pointer = this.items.get(key);
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    this.V[pointer] = value;
+
+    return;
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    if (this.deletedSize > 0) {
+      // If there is a "hole" in the pointer list, reuse it
+      pointer = this.deleted[--this.deletedSize];
+    }
+    else {
+      // otherwise append to the pointer list
+      pointer = this.size;
+    }
+    this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    this.items.delete(this.K[pointer]);
+  }
+
+  // Storing key & value
+  this.items.set(key, pointer);
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+};
+
+/**
+ * Method used to set the value for the given key in the cache
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {{evicted: boolean, key: any, value: any}} An object containing the
+ * key and value of an item that was overwritten or evicted in the set
+ * operation, as well as a boolean indicating whether it was evicted due to
+ * limited capacity. Return value is null if nothing was evicted or overwritten
+ * during the set operation.
+ */
+LRUMapWithDelete.prototype.setpop = function(key, value) {
+  var oldValue = null;
+  var oldKey = null;
+
+  var pointer = this.items.get(key);
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    oldValue = this.V[pointer];
+    this.V[pointer] = value;
+    return {evicted: false, key: key, value: oldValue};
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    if (this.deletedSize > 0) {
+      // If there is a "hole" in the pointer list, reuse it
+      pointer = this.deleted[--this.deletedSize];
+    }
+    else {
+      // otherwise append to the pointer list
+      pointer = this.size;
+    }
+    this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    oldValue = this.V[pointer];
+    oldKey = this.K[pointer];
+    this.items.delete(this.K[pointer]);
+  }
+
+  // Storing key & value
+  this.items.set(key, pointer);
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+
+  // Return object if eviction took place, otherwise return null
+  if (oldKey) {
+    return {evicted: true, key: oldKey, value: oldValue};
+  }
+  else {
+    return null;
+  }
+};
+
+/**
+ * Method used to delete the entry for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @return {boolean}   - true if the item was present
+ */
+LRUMapWithDelete.prototype.delete = function(key) {
+
+  var pointer = this.items.get(key);
+
+  if (typeof pointer === 'undefined') {
+    return false;
+  }
+
+  this.items.delete(key);
+
+  if (this.size === 1) {
+    this.size = 0;
+    this.head = 0;
+    this.tail = 0;
+    this.deletedSize = 0;
+    return true;
+  }
+
+  var previous = this.backward[pointer],
+      next = this.forward[pointer];
+
+  if (this.head === pointer) {
+    this.head = next;
+  }
+  if (this.tail === pointer) {
+    this.tail = previous;
+  }
+
+  this.forward[previous] = next;
+  this.backward[next] = previous;
+
+  this.size--;
+  this.deleted[this.deletedSize++] = pointer;
+
+  return true;
+};
+
+/**
+ * Method used to remove and return the value for the given key in the cache.
+ *
+ * @param  {any} key                 - Key.
+ * @param  {any} [missing=undefined] - Value to return if item is absent
+ * @return {any} The value, if present; the missing indicator if absent
+ */
+LRUMapWithDelete.prototype.remove = function(key, missing = undefined) {
+
+  var pointer = this.items.get(key);
+
+  if (typeof pointer === 'undefined') {
+    return missing;
+  }
+
+  var dead = this.V[pointer];
+  this.items.delete(key);
+
+  if (this.size === 1) {
+    this.size = 0;
+    this.head = 0;
+    this.tail = 0;
+    this.deletedSize = 0;
+    return dead;
+  }
+
+  var previous = this.backward[pointer],
+      next = this.forward[pointer];
+
+  if (this.head === pointer) {
+    this.head = next;
+  }
+  if (this.tail === pointer) {
+    this.tail = previous;
+  }
+
+  this.forward[previous] = next;
+  this.backward[next] = previous;
+
+  this.size--;
+  this.deleted[this.deletedSize++] = pointer;
+
+  return dead;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} Keys     - Array class for storing keys.
+ * @param  {function} Values   - Array class for storing values.
+ * @param  {number}   capacity - Cache's capacity.
+ * @return {LRUMapWithDelete}
+ */
+ LRUMapWithDelete.from = function(iterable, Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/lru-map.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+  else if (arguments.length === 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  var cache = new LRUMapWithDelete(Keys, Values, capacity);
+
+  forEach(iterable, function(value, key) {
+    cache.set(key, value);
+  });
+
+  return cache;
+};
+
+module.exports = LRUMapWithDelete;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/lru-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0943543affaf48e059a9d2aece35134d3e1d7551
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-map.d.ts
@@ -0,0 +1,43 @@
+/**
+ * Mnemonist LRUMap Typings
+ * =========================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+export default class LRUMap<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  capacity: number;
+  size: number;
+
+  // Constructor
+  constructor(capacity: number);
+  constructor(KeyArrayClass: IArrayLikeConstructor, ValueArrayClass: IArrayLikeConstructor, capacity: number);
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  setpop(key: K, value: V): {evicted: boolean, key: K, value: V};
+  get(key: K): V | undefined;
+  peek(key: K): V | undefined;
+  has(key: K): boolean;
+  forEach(callback: (value: V, key: K, cache: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    KeyArrayClass: IArrayLikeConstructor,
+    ValueArrayClass: IArrayLikeConstructor,
+    capacity?: number
+  ): LRUMap<I, J>;
+
+  static from<I, J>(
+    iterable: Iterable<[I, J]> | {[key: string]: J},
+    capacity?: number
+  ): LRUMap<I, J>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/lru-map.js b/libs/shared/graph-layout/node_modules/mnemonist/lru-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..87f7aa388997d30aea3d1d18a0338d930cc75ecf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/lru-map.js
@@ -0,0 +1,261 @@
+/**
+ * Mnemonist LRUMap
+ * =================
+ *
+ * Variant of the LRUCache class that leverages an ES6 Map instead of an object.
+ * It might be faster for some use case but it is still hard to understand
+ * when a Map can outperform an object in v8.
+ */
+var LRUCache = require('./lru-cache.js'),
+    forEach = require('obliterator/foreach'),
+    typed = require('./utils/typed-arrays.js'),
+    iterables = require('./utils/iterables.js');
+
+/**
+ * LRUMap.
+ *
+ * @constructor
+ * @param {function} Keys     - Array class for storing keys.
+ * @param {function} Values   - Array class for storing values.
+ * @param {number}   capacity - Desired capacity.
+ */
+function LRUMap(Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  this.capacity = capacity;
+
+  if (typeof this.capacity !== 'number' || this.capacity <= 0)
+    throw new Error('mnemonist/lru-map: capacity should be positive number.');
+  else if (!isFinite(this.capacity) || Math.floor(this.capacity) !== this.capacity)
+    throw new Error('mnemonist/lru-map: capacity should be a finite positive integer.');
+
+  var PointerArray = typed.getPointerArray(capacity);
+
+  this.forward = new PointerArray(capacity);
+  this.backward = new PointerArray(capacity);
+  this.K = typeof Keys === 'function' ? new Keys(capacity) : new Array(capacity);
+  this.V = typeof Values === 'function' ? new Values(capacity) : new Array(capacity);
+
+  // Properties
+  this.size = 0;
+  this.head = 0;
+  this.tail = 0;
+  this.items = new Map();
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+LRUMap.prototype.clear = function() {
+  this.size = 0;
+  this.head = 0;
+  this.tail = 0;
+  this.items.clear();
+};
+
+/**
+ * Method used to set the value for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {undefined}
+ */
+LRUMap.prototype.set = function(key, value) {
+
+  var pointer = this.items.get(key);
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    this.V[pointer] = value;
+
+    return;
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    pointer = this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    this.items.delete(this.K[pointer]);
+  }
+
+  // Storing key & value
+  this.items.set(key, pointer);
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+};
+
+/**
+ * Method used to set the value for the given key in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @param  {any} value - Value.
+ * @return {{evicted: boolean, key: any, value: any}} An object containing the
+ * key and value of an item that was overwritten or evicted in the set
+ * operation, as well as a boolean indicating whether it was evicted due to
+ * limited capacity. Return value is null if nothing was evicted or overwritten
+ * during the set operation.
+ */
+LRUMap.prototype.setpop = function(key, value) {
+  var oldValue = null;
+  var oldKey = null;
+
+  var pointer = this.items.get(key);
+
+  // The key already exists, we just need to update the value and splay on top
+  if (typeof pointer !== 'undefined') {
+    this.splayOnTop(pointer);
+    oldValue = this.V[pointer];
+    this.V[pointer] = value;
+    return {evicted: false, key: key, value: oldValue};
+  }
+
+  // The cache is not yet full
+  if (this.size < this.capacity) {
+    pointer = this.size++;
+  }
+
+  // Cache is full, we need to drop the last value
+  else {
+    pointer = this.tail;
+    this.tail = this.backward[pointer];
+    oldValue = this.V[pointer];
+    oldKey = this.K[pointer];
+    this.items.delete(this.K[pointer]);
+  }
+
+  // Storing key & value
+  this.items.set(key, pointer);
+  this.K[pointer] = key;
+  this.V[pointer] = value;
+
+  // Moving the item at the front of the list
+  this.forward[pointer] = this.head;
+  this.backward[this.head] = pointer;
+  this.head = pointer;
+
+  // Return object if eviction took place, otherwise return null
+  if (oldKey) {
+    return {evicted: true, key: oldKey, value: oldValue};
+  }
+  else {
+    return null;
+  }
+};
+
+/**
+ * Method used to check whether the key exists in the cache.
+ *
+ * @param  {any} key   - Key.
+ * @return {boolean}
+ */
+LRUMap.prototype.has = function(key) {
+  return this.items.has(key);
+};
+
+/**
+ * Method used to get the value attached to the given key. Will move the
+ * related key to the front of the underlying linked list.
+ *
+ * @param  {any} key   - Key.
+ * @return {any}
+ */
+LRUMap.prototype.get = function(key) {
+  var pointer = this.items.get(key);
+
+  if (typeof pointer === 'undefined')
+    return;
+
+  this.splayOnTop(pointer);
+
+  return this.V[pointer];
+};
+
+/**
+ * Method used to get the value attached to the given key. Does not modify
+ * the ordering of the underlying linked list.
+ *
+ * @param  {any} key   - Key.
+ * @return {any}
+ */
+LRUMap.prototype.peek = function(key) {
+  var pointer = this.items.get(key);
+
+  if (typeof pointer === 'undefined')
+    return;
+
+  return this.V[pointer];
+};
+
+/**
+ * Methods that can be reused as-is from LRUCache.
+ */
+LRUMap.prototype.splayOnTop = LRUCache.prototype.splayOnTop;
+LRUMap.prototype.forEach = LRUCache.prototype.forEach;
+LRUMap.prototype.keys = LRUCache.prototype.keys;
+LRUMap.prototype.values = LRUCache.prototype.values;
+LRUMap.prototype.entries = LRUCache.prototype.entries;
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  LRUMap.prototype[Symbol.iterator] = LRUMap.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+LRUMap.prototype.inspect = LRUCache.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} Keys     - Array class for storing keys.
+ * @param  {function} Values   - Array class for storing values.
+ * @param  {number}   capacity - Cache's capacity.
+ * @return {LRUMap}
+ */
+LRUMap.from = function(iterable, Keys, Values, capacity) {
+  if (arguments.length < 2) {
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/lru-cache.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+  else if (arguments.length === 2) {
+    capacity = Keys;
+    Keys = null;
+    Values = null;
+  }
+
+  var cache = new LRUMap(Keys, Values, capacity);
+
+  forEach(iterable, function(value, key) {
+    cache.set(key, value);
+  });
+
+  return cache;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = LRUMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/multi-array.js b/libs/shared/graph-layout/node_modules/mnemonist/multi-array.js
new file mode 100644
index 0000000000000000000000000000000000000000..c165b553cc79a651fb94aae773e447e3a670af17
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/multi-array.js
@@ -0,0 +1,447 @@
+/**
+ * Mnemonist MultiArray
+ * =====================
+ *
+ * Memory-efficient representation of an array of arrays. In JavaScript and
+ * most high-level languages, creating objects has a cost. This implementation
+ * is therefore able to represent nested containers without needing to create
+ * objects. This works by storing singly linked lists in a single flat array.
+ * However, this means that this structure comes with some read/write
+ * overhead but consume very few memory.
+ *
+ * This structure should be particularly suited to indices that will need to
+ * merge arrays anyway when queried and that are quite heavily hit (such as
+ * an inverted index or a quad tree).
+ *
+ * Note: the implementation does not require to keep track of head pointers
+ * but this comes with some advantages such as not needing to offset pointers
+ * by 1 and being able to perform in-order iteration. This remains quite lean
+ * in memory and does not hinder performance whatsoever.
+ */
+var typed = require('./utils/typed-arrays.js'),
+    Vector = require('./vector.js'),
+    Iterator = require('obliterator/iterator');
+
+var PointerVector = Vector.PointerVector;
+
+/**
+ * MultiArray.
+ *
+ * @constructor
+ */
+function MultiArray(Container, capacity) {
+  this.capacity = capacity || null;
+  this.Container = Container || Array;
+  this.hasFixedCapacity = this.capacity !== null;
+
+  if (typeof this.Container !== 'function')
+    throw new Error('mnemonist/multi-array.constructor: container should be a function.');
+
+  this.clear();
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+MultiArray.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.dimension = 0;
+
+  // NOTE: #.heads, #.tails & #.lengths have a length equal to the dimension of
+  // the array, while #.pointers has a length equal to its size.
+
+  // Storage
+  if (this.hasFixedCapacity) {
+    var capacity = this.capacity;
+
+    var PointerArray = typed.getPointerArray(capacity);
+
+    var policy = function(currentCapacity) {
+      var newCapacity = Math.max(1, Math.ceil(currentCapacity * 1.5));
+
+      // Clamping max allocation
+      return Math.min(newCapacity, capacity);
+    };
+
+    var initialCapacity = Math.max(8, capacity);
+
+    this.tails = new Vector(PointerArray, {policy: policy, initialCapacity: initialCapacity});
+    this.lengths = new Vector(PointerArray, {policy: policy, initialCapacity: initialCapacity});
+    this.pointers = new PointerArray(capacity);
+
+    this.items = new this.Container(capacity);
+  }
+  else {
+
+    this.tails = new PointerVector();
+    this.lengths = new PointerVector();
+    this.pointers = new PointerVector();
+
+    this.items = new this.Container();
+  }
+};
+
+/**
+ * Method used to add an item to the container at the given index.
+ *
+ * @param  {number} index - Index of the container.
+ * @param  {any}    item  - Item to add.
+ * @return {MultiArray}
+ */
+MultiArray.prototype.set = function(index, item) {
+  var pointer = this.size;
+
+  // TODO: this can be factorized!
+
+  if (this.hasFixedCapacity) {
+
+    if (index >= this.capacity || this.size === this.capacity)
+      throw new Error('mnemonist/multi-array: attempting to allocate further than capacity.');
+
+    // This linked list does not exist yet. Let's create it
+    if (index >= this.dimension) {
+
+      // We may be required to grow the vectors
+      this.dimension = index + 1;
+      this.tails.grow(this.dimension);
+      this.lengths.grow(this.dimension);
+
+      this.tails.resize(this.dimension);
+      this.lengths.resize(this.dimension);
+
+      this.lengths.array[index] = 1;
+    }
+
+    // Appending to the list
+    else {
+      this.pointers[pointer] = this.tails.array[index];
+      this.lengths.array[index]++;
+    }
+
+    this.tails.array[index] = pointer;
+    this.items[pointer] = item;
+  }
+  else {
+
+    // This linked list does not exist yet. Let's create it
+    if (index >= this.dimension) {
+
+      // We may be required to grow the vectors
+      this.dimension = index + 1;
+      this.tails.grow(this.dimension);
+      this.lengths.grow(this.dimension);
+
+      this.tails.resize(this.dimension);
+      this.lengths.resize(this.dimension);
+
+      this.pointers.push(0);
+      this.lengths.array[index] = 1;
+    }
+
+    // Appending to the list
+    else {
+      this.pointers.push(this.tails.array[index]);
+      this.lengths.array[index]++;
+    }
+
+    this.tails.array[index] = pointer;
+    this.items.push(item);
+  }
+
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to push a new container holding the given value.
+ * Note: it might be useful to make this function able to take an iterable
+ * or variadic someday. For the time being it's just a convenience for
+ * implementing compact multi maps and such.
+ *
+ * @param  {any} item  - Item to add.
+ * @return {MultiArray}
+ */
+MultiArray.prototype.push = function(item) {
+  var pointer = this.size,
+      index = this.dimension;
+
+  if (this.hasFixedCapacity) {
+
+    if (index >= this.capacity || this.size === this.capacity)
+      throw new Error('mnemonist/multi-array: attempting to allocate further than capacity.');
+
+    this.items[pointer] = item;
+  }
+  else {
+    this.items.push(item);
+    this.pointers.push(0);
+  }
+
+  this.lengths.push(1);
+  this.tails.push(pointer);
+
+  this.dimension++;
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to get the desired container.
+ *
+ * @param  {number} index - Index of the container.
+ * @return {array}
+ */
+MultiArray.prototype.get = function(index) {
+  if (index >= this.dimension)
+    return;
+
+  var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array;
+
+  var pointer = this.tails.array[index],
+      length = this.lengths.array[index],
+      i = length;
+
+  var array = new this.Container(length);
+
+  while (i !== 0) {
+    array[--i] = this.items[pointer];
+    pointer = pointers[pointer];
+  }
+
+  return array;
+};
+
+/**
+ * Method used to check if a container exists at the given index.
+ *
+ * @param  {number} index - Index of the container.
+ * @return {boolean}
+ */
+MultiArray.prototype.has = function(index) {
+  return index < this.dimension;
+};
+
+/**
+ * Method used to get the size of the container stored at given index.
+ *
+ * @param  {number} index - Index of the container.
+ * @return {number}
+ */
+MultiArray.prototype.multiplicity = function(index) {
+  if (index >= this.dimension)
+    return 0;
+
+  return this.lengths.array[index];
+};
+MultiArray.prototype.count = MultiArray.prototype.multiplicity;
+
+/**
+ * Method used to iterate over the structure's containers.
+ *
+ * @return {Iterator}
+ */
+MultiArray.prototype.containers = function() {
+  var self = this,
+      l = this.dimension,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    return {value: self.get(i++)};
+  });
+};
+
+/**
+ * Method used to iterate over the structure's associations.
+ *
+ * @return {Iterator}
+ */
+MultiArray.prototype.associations = function() {
+  var self = this,
+      l = this.dimension,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    var data = {value: [i, self.get(i)]};
+
+    i++;
+
+    return data;
+  });
+};
+
+/**
+ * Method used to iterate over the structure's values in the global insertion
+ * order.
+ *
+ * @param  {number}   [index] - Optionally, iterate over the values of a single
+ *                              container at index.
+ * @return {Iterator}
+ */
+MultiArray.prototype.values = function(index) {
+  var items = this.items,
+      length,
+      i = 0;
+
+  if (typeof index === 'number') {
+    if (index >= this.dimension)
+      return Iterator.empty();
+
+    length = this.lengths.array[index];
+    items = this.items;
+
+    var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array;
+
+    if (length === 0)
+      return Iterator.empty();
+
+    var pointer = this.tails.array[index],
+        v;
+
+    return new Iterator(function() {
+      if (i === length)
+        return {done: true};
+
+      i++;
+      v = items[pointer];
+      pointer = pointers[pointer];
+
+      return {done: false, value: v};
+    });
+  }
+
+  length = this.size;
+
+  return new Iterator(function() {
+    if (i >= length)
+      return {done: true};
+
+    return {done: false, value: items[i++]};
+  });
+};
+
+/**
+ * Method used to iterate over the structure's entries.
+ *
+ * @return {Iterator}
+ */
+MultiArray.prototype.entries = function() {
+  if (this.size === 0)
+    return Iterator.empty();
+
+  var inContainer = false,
+      pointer,
+      length,
+      i = 0,
+      j = 0,
+      l = this.dimension,
+      v;
+
+  var pointers = this.hasFixedCapacity ? this.pointers : this.pointers.array,
+      items = this.items,
+      tails = this.tails.array,
+      lengths = this.lengths.array;
+
+  var iterator = new Iterator(function next() {
+    if (!inContainer) {
+
+      if (i >= l)
+        return {done: true};
+
+      length = lengths[i];
+      pointer = tails[i];
+      i++;
+
+      if (length === 0)
+        return next();
+
+      j = 0;
+      inContainer = true;
+    }
+
+    if (j === length) {
+      inContainer = false;
+      return next();
+    }
+
+    v = items[pointer];
+
+    // TODO: guard for out-of-bounds
+    pointer = pointers[pointer];
+
+    j++;
+
+    return {
+      done: false,
+      value: [i - 1, v]
+    };
+  });
+
+  return iterator;
+};
+
+/**
+ * Method used to iterate over the structure's keys.
+ *
+ * @return {Iterator}
+ */
+MultiArray.prototype.keys = function() {
+  var i = 0,
+      l = this.dimension;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {done: true};
+
+    return {done: false, value: i++};
+  });
+};
+
+/**
+ * Convenience known methods.
+ */
+MultiArray.prototype.inspect = function() {
+  var proxy = new Array(this.dimension),
+      i,
+      l;
+
+  for (i = 0, l = this.dimension; i < l; i++)
+    proxy[i] = Array.from(this.get(i));
+
+  if (this.hasFixedCapacity) {
+    proxy.type = this.Container.name;
+    proxy.capacity = this.capacity;
+  }
+
+  proxy.size = this.size;
+  proxy.dimension = this.dimension;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: MultiArray,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  MultiArray.prototype[Symbol.for('nodejs.util.inspect.custom')] = MultiArray.prototype.inspect;
+
+// TODO: .from
+
+/**
+ * Exporting.
+ */
+module.exports = MultiArray;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/multi-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/multi-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e4c85435627a2fcfeb75923105938e2a95686dff
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/multi-map.d.ts
@@ -0,0 +1,47 @@
+/**
+ * Mnemonist MultiMap Typings
+ * ===========================
+ */
+
+interface MultiMap<K, V, C extends V[] | Set<V> = V[]> extends Iterable<[K, V]> {
+
+  // Members
+  dimension: number;
+  size: number;
+
+  // Methods
+  clear(): void;
+  set(key: K, value: V): this;
+  delete(key: K): boolean;
+  remove(key: K, value: V): boolean;
+  has(key: K): boolean;
+  get(key: K): C | undefined;
+  multiplicity(key: K): number;
+  forEach(callback: (value: V, key: K, map: this) => void, scope?: any): void;
+  forEachAssociation(callback: (value: C, key: K, map: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[K, V]>;
+  containers(): IterableIterator<C>;
+  associations(): IterableIterator<[K, C]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+  toJSON(): any;
+}
+
+interface MultiMapConstructor {
+  new <K, V>(container: SetConstructor): MultiMap<K, V, Set<V>>;
+  new <K, V>(container?: ArrayConstructor): MultiMap<K, V, V[]>;
+
+  from<K, V>(
+    iterable: Iterable<[K, V]> | {[key: string]: V},
+    Container: SetConstructor
+  ): MultiMap<K, V, Set<V>>;
+  from<K, V>(
+    iterable: Iterable<[K, V]> | {[key: string]: V},
+    Container?: ArrayConstructor
+  ): MultiMap<K, V, V[]>;
+}
+
+declare const MultiMap: MultiMapConstructor;
+export default MultiMap;
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/multi-map.js b/libs/shared/graph-layout/node_modules/mnemonist/multi-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b36e1551dd5f50f9807724bc2893bb12ba27804
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/multi-map.js
@@ -0,0 +1,408 @@
+/**
+ * Mnemonist MultiMap
+ * ===================
+ *
+ * Implementation of a MultiMap with custom container.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach');
+
+/**
+ * MultiMap.
+ *
+ * @constructor
+ */
+function MultiMap(Container) {
+
+  this.Container = Container || Array;
+  this.items = new Map();
+  this.clear();
+
+  Object.defineProperty(this.items, 'constructor', {
+    value: MultiMap,
+    enumerable: false
+  });
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+MultiMap.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.dimension = 0;
+  this.items.clear();
+};
+
+/**
+ * Method used to set a value.
+ *
+ * @param  {any}      key   - Key.
+ * @param  {any}      value - Value to add.
+ * @return {MultiMap}
+ */
+MultiMap.prototype.set = function(key, value) {
+  var container = this.items.get(key),
+      sizeBefore;
+
+  if (!container) {
+    this.dimension++;
+    container = new this.Container();
+    this.items.set(key, container);
+  }
+
+  if (this.Container === Set) {
+    sizeBefore = container.size;
+    container.add(value);
+
+    if (sizeBefore < container.size)
+      this.size++;
+  }
+  else {
+    container.push(value);
+    this.size++;
+  }
+
+  return this;
+};
+
+/**
+ * Method used to delete the given key.
+ *
+ * @param  {any}     key - Key to delete.
+ * @return {boolean}
+ */
+MultiMap.prototype.delete = function(key) {
+  var container = this.items.get(key);
+
+  if (!container)
+    return false;
+
+  this.size -= (this.Container === Set ? container.size : container.length);
+  this.dimension--;
+  this.items.delete(key);
+
+  return true;
+};
+
+/**
+ * Method used to delete the remove an item in the container stored at the
+ * given key.
+ *
+ * @param  {any}     key - Key to delete.
+ * @return {boolean}
+ */
+MultiMap.prototype.remove = function(key, value) {
+  var container = this.items.get(key),
+      wasDeleted,
+      index;
+
+  if (!container)
+    return false;
+
+  if (this.Container === Set) {
+    wasDeleted = container.delete(value);
+
+    if (wasDeleted)
+      this.size--;
+
+    if (container.size === 0) {
+      this.items.delete(key);
+      this.dimension--;
+    }
+
+    return wasDeleted;
+  }
+  else {
+    index = container.indexOf(value);
+
+    if (index === -1)
+      return false;
+
+    this.size--;
+
+    if (container.length === 1) {
+      this.items.delete(key);
+      this.dimension--;
+
+      return true;
+    }
+
+    container.splice(index, 1);
+
+    return true;
+  }
+};
+
+/**
+ * Method used to return whether the given keys exists in the map.
+ *
+ * @param  {any}     key - Key to check.
+ * @return {boolean}
+ */
+MultiMap.prototype.has = function(key) {
+  return this.items.has(key);
+};
+
+/**
+ * Method used to return the container stored at the given key or `undefined`.
+ *
+ * @param  {any}     key - Key to get.
+ * @return {boolean}
+ */
+MultiMap.prototype.get = function(key) {
+  return this.items.get(key);
+};
+
+/**
+ * Method used to return the multiplicity of the given key, meaning the number
+ * of times it is set, or, more trivially, the size of the attached container.
+ *
+ * @param  {any}     key - Key to check.
+ * @return {number}
+ */
+MultiMap.prototype.multiplicity = function(key) {
+  var container = this.items.get(key);
+
+  if (typeof container === 'undefined')
+    return 0;
+
+  return this.Container === Set ? container.size : container.length;
+};
+MultiMap.prototype.count = MultiMap.prototype.multiplicity;
+
+/**
+ * Method used to iterate over each of the key/value pairs.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+MultiMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  // Inner iteration function is created here to avoid creating it in the loop
+  var key;
+  function inner(value) {
+    callback.call(scope, value, key);
+  }
+
+  this.items.forEach(function(container, k) {
+    key = k;
+    container.forEach(inner);
+  });
+};
+
+/**
+ * Method used to iterate over each of the associations.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+MultiMap.prototype.forEachAssociation = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  this.items.forEach(callback, scope);
+};
+
+/**
+ * Method returning an iterator over the map's keys.
+ *
+ * @return {Iterator}
+ */
+MultiMap.prototype.keys = function() {
+  return this.items.keys();
+};
+
+/**
+ * Method returning an iterator over the map's keys.
+ *
+ * @return {Iterator}
+ */
+MultiMap.prototype.values = function() {
+  var iterator = this.items.values(),
+      inContainer = false,
+      countainer,
+      step,
+      i,
+      l;
+
+  if (this.Container === Set)
+    return new Iterator(function next() {
+      if (!inContainer) {
+        step = iterator.next();
+
+        if (step.done)
+          return {done: true};
+
+        inContainer = true;
+        countainer = step.value.values();
+      }
+
+      step = countainer.next();
+
+      if (step.done) {
+        inContainer = false;
+        return next();
+      }
+
+      return {
+        done: false,
+        value: step.value
+      };
+    });
+
+  return new Iterator(function next() {
+    if (!inContainer) {
+      step = iterator.next();
+
+      if (step.done)
+        return {done: true};
+
+      inContainer = true;
+      countainer = step.value;
+      i = 0;
+      l = countainer.length;
+    }
+
+    if (i >= l) {
+      inContainer = false;
+      return next();
+    }
+
+    return {
+      done: false,
+      value: countainer[i++]
+    };
+  });
+};
+
+/**
+ * Method returning an iterator over the map's entries.
+ *
+ * @return {Iterator}
+ */
+MultiMap.prototype.entries = function() {
+  var iterator = this.items.entries(),
+      inContainer = false,
+      countainer,
+      step,
+      key,
+      i,
+      l;
+
+  if (this.Container === Set)
+    return new Iterator(function next() {
+      if (!inContainer) {
+        step = iterator.next();
+
+        if (step.done)
+          return {done: true};
+
+        inContainer = true;
+        key = step.value[0];
+        countainer = step.value[1].values();
+      }
+
+      step = countainer.next();
+
+      if (step.done) {
+        inContainer = false;
+        return next();
+      }
+
+      return {
+        done: false,
+        value: [key, step.value]
+      };
+    });
+
+  return new Iterator(function next() {
+    if (!inContainer) {
+      step = iterator.next();
+
+      if (step.done)
+        return {done: true};
+
+      inContainer = true;
+      key = step.value[0];
+      countainer = step.value[1];
+      i = 0;
+      l = countainer.length;
+    }
+
+    if (i >= l) {
+      inContainer = false;
+      return next();
+    }
+
+    return {
+      done: false,
+      value: [key, countainer[i++]]
+    };
+  });
+};
+
+/**
+ * Method returning an iterator over the map's containers.
+ *
+ * @return {Iterator}
+ */
+MultiMap.prototype.containers = function() {
+  return this.items.values();
+};
+
+/**
+ * Method returning an iterator over the map's associations.
+ *
+ * @return {Iterator}
+ */
+MultiMap.prototype.associations = function() {
+  return this.items.entries();
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  MultiMap.prototype[Symbol.iterator] = MultiMap.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+MultiMap.prototype.inspect = function() {
+  return this.items;
+};
+
+if (typeof Symbol !== 'undefined')
+  MultiMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = MultiMap.prototype.inspect;
+MultiMap.prototype.toJSON = function() {
+  return this.items;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {Class}    Container - Container.
+ * @return {MultiMap}
+ */
+MultiMap.from = function(iterable, Container) {
+  var map = new MultiMap(Container);
+
+  forEach(iterable, function(value, key) {
+    map.set(key, value);
+  });
+
+  return map;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = MultiMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/multi-set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/multi-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0e40bc40f3a17afbfa1612ed75a8976facf3d590
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/multi-set.d.ts
@@ -0,0 +1,37 @@
+/**
+ * Mnemonist MultiSet Typings
+ * ===========================
+ */
+export default class MultiSet<K> implements Iterable<K> {
+
+  // Members
+  dimension: number;
+  size: number;
+
+  // Methods
+  clear(): void;
+  add(key: K, count?: number): this;
+  set(key: K, count: number): this;
+  has(key: K): boolean;
+  delete(key: K): boolean;
+  remove(key: K, count?: number): void;
+  edit(a: K, b: K): this;
+  multiplicity(key: K): number;
+  count(key: K): number;
+  get(key: K): number;
+  frequency(key: K): number;
+  top(n: number): Array<[K, number]>;
+  forEach(callback: (value: K, key: K, set: this) => void, scope?: any): void;
+  forEachMultiplicity(callback: (value: number, key: K, set: this) => void, scope?: any): void;
+  keys(): IterableIterator<K>;
+  values(): IterableIterator<K>;
+  multiplicities(): IterableIterator<[K, number]>;
+  [Symbol.iterator](): IterableIterator<K>;
+  inspect(): any;
+  toJSON(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string]: I}): MultiSet<I>;
+  static isSubset<T>(a: MultiSet<T>, b: MultiSet<T>): boolean;
+  static isSuperset<T>(a: MultiSet<T>, b: MultiSet<T>): boolean;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/multi-set.js b/libs/shared/graph-layout/node_modules/mnemonist/multi-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..3206af7316620026c8768fb9254294a2f2343152
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/multi-set.js
@@ -0,0 +1,440 @@
+/**
+ * Mnemonist MultiSet
+ * ====================
+ *
+ * JavaScript implementation of a MultiSet.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach'),
+    FixedReverseHeap = require('./fixed-reverse-heap.js');
+
+/**
+ * Helpers.
+ */
+var MULTISET_ITEM_COMPARATOR = function(a, b) {
+  if (a[1] > b[1])
+    return -1;
+  if (a[1] < b[1])
+    return 1;
+
+  return 0;
+};
+
+// TODO: helper functions: union, intersection, sum, difference, subtract
+
+/**
+ * MultiSet.
+ *
+ * @constructor
+ */
+function MultiSet() {
+  this.items = new Map();
+
+  Object.defineProperty(this.items, 'constructor', {
+    value: MultiSet,
+    enumerable: false
+  });
+
+  this.clear();
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+MultiSet.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.dimension = 0;
+  this.items.clear();
+};
+
+/**
+ * Method used to add an item to the set.
+ *
+ * @param  {any}    item  - Item to add.
+ * @param  {number} count - Optional count.
+ * @return {MultiSet}
+ */
+MultiSet.prototype.add = function(item, count) {
+  if (count === 0)
+    return this;
+
+  if (count < 0)
+    return this.remove(item, -count);
+
+  count = count || 1;
+
+  if (typeof count !== 'number')
+    throw new Error('mnemonist/multi-set.add: given count should be a number.');
+
+  this.size += count;
+
+  const currentCount = this.items.get(item);
+
+  if (currentCount === undefined)
+    this.dimension++;
+  else
+    count += currentCount;
+
+  this.items.set(item, count);
+
+  return this;
+};
+
+/**
+ * Method used to set the multiplicity of an item in the set.
+ *
+ * @param  {any}    item  - Target item.
+ * @param  {number} count - Desired multiplicity.
+ * @return {MultiSet}
+ */
+MultiSet.prototype.set = function(item, count) {
+  var currentCount;
+
+  if (typeof count !== 'number')
+    throw new Error('mnemonist/multi-set.set: given count should be a number.');
+
+  // Setting an item to 0 or to a negative number means deleting it from the set
+  if (count <= 0) {
+    currentCount = this.items.get(item);
+
+    if (typeof currentCount !== 'undefined') {
+      this.size -= currentCount;
+      this.dimension--;
+    }
+
+    this.items.delete(item);
+    return this;
+  }
+
+  count = count || 1;
+
+  currentCount = this.items.get(item);
+
+  if (typeof currentCount === 'number') {
+    this.items.set(item, currentCount + count);
+  }
+  else {
+    this.dimension++;
+    this.items.set(item, count);
+  }
+
+  this.size += count;
+
+  return this;
+};
+
+/**
+ * Method used to return whether the item exists in the set.
+ *
+ * @param  {any} item  - Item to check.
+ * @return {boolan}
+ */
+MultiSet.prototype.has = function(item) {
+  return this.items.has(item);
+};
+
+/**
+ * Method used to delete an item from the set.
+ *
+ * @param  {any} item  - Item to delete.
+ * @return {boolan}
+ */
+MultiSet.prototype.delete = function(item) {
+  var count = this.items.get(item);
+
+  if (count === 0)
+    return false;
+
+  this.size -= count;
+  this.dimension--;
+  this.items.delete(item);
+
+  return true;
+};
+
+/**
+ * Method used to remove an item from the set.
+ *
+ * @param  {any} item  - Item to delete.
+ * @param  {number} count - Optional count.
+ * @return {undefined}
+ */
+MultiSet.prototype.remove = function(item, count) {
+  if (count === 0)
+    return;
+
+  if (count < 0)
+    return this.add(item, -count);
+
+  count = count || 1;
+
+  if (typeof count !== 'number')
+    throw new Error('mnemonist/multi-set.remove: given count should be a number.');
+
+  var currentCount = this.multiplicity(item),
+      newCount = Math.max(0, currentCount - count);
+
+  if (newCount === 0) {
+    this.delete(item);
+  }
+  else {
+    this.items.set(item, newCount);
+    this.size -= (currentCount - newCount);
+  }
+
+  return;
+};
+
+/**
+ * Method used to change a key into another one, merging counts if the target
+ * key already exists.
+ *
+ * @param  {any} a - From key.
+ * @param  {any} b - To key.
+ * @return {MultiSet}
+ */
+MultiSet.prototype.edit = function(a, b) {
+  var am = this.multiplicity(a);
+
+  // If a does not exist in the set, we can stop right there
+  if (am === 0)
+    return;
+
+  var bm = this.multiplicity(b);
+
+  this.items.set(b, am + bm);
+  this.items.delete(a);
+
+  return this;
+};
+
+/**
+ * Method used to return the multiplicity of the given item.
+ *
+ * @param  {any} item  - Item to get.
+ * @return {number}
+ */
+MultiSet.prototype.multiplicity = function(item) {
+  var count = this.items.get(item);
+
+  if (typeof count === 'undefined')
+    return 0;
+
+  return count;
+};
+MultiSet.prototype.get = MultiSet.prototype.multiplicity;
+MultiSet.prototype.count = MultiSet.prototype.multiplicity;
+
+/**
+ * Method used to return the frequency of the given item in the set.
+ *
+ * @param  {any} item - Item to get.
+ * @return {number}
+ */
+MultiSet.prototype.frequency = function(item) {
+  if (this.size === 0)
+    return 0;
+
+  var count = this.multiplicity(item);
+
+  return count / this.size;
+};
+
+/**
+ * Method used to return the n most common items from the set.
+ *
+ * @param  {number} n - Number of items to retrieve.
+ * @return {array}
+ */
+MultiSet.prototype.top = function(n) {
+  if (typeof n !== 'number' || n <= 0)
+    throw new Error('mnemonist/multi-set.top: n must be a number > 0.');
+
+  var heap = new FixedReverseHeap(Array, MULTISET_ITEM_COMPARATOR, n);
+
+  var iterator = this.items.entries(),
+      step;
+
+  while ((step = iterator.next(), !step.done))
+    heap.push(step.value);
+
+  return heap.consume();
+};
+
+/**
+ * Method used to iterate over the set's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+MultiSet.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var i;
+
+  this.items.forEach(function(multiplicity, value) {
+
+    for (i = 0; i < multiplicity; i++)
+      callback.call(scope, value, value);
+  });
+};
+
+/**
+ * Method used to iterate over the set's multiplicities.
+ *
+ * @param  {function}  callback - Function to call for each multiplicity.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+MultiSet.prototype.forEachMultiplicity = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  this.items.forEach(callback, scope);
+};
+
+/**
+ * Method returning an iterator over the set's keys. I.e. its unique values,
+ * in a sense.
+ *
+ * @return {Iterator}
+ */
+MultiSet.prototype.keys = function() {
+  return this.items.keys();
+};
+
+/**
+ * Method returning an iterator over the set's values.
+ *
+ * @return {Iterator}
+ */
+MultiSet.prototype.values = function() {
+  var iterator = this.items.entries(),
+      inContainer = false,
+      step,
+      value,
+      multiplicity,
+      i;
+
+  return new Iterator(function next() {
+    if (!inContainer) {
+      step = iterator.next();
+
+      if (step.done)
+        return {done: true};
+
+      inContainer = true;
+      value = step.value[0];
+      multiplicity = step.value[1];
+      i = 0;
+    }
+
+    if (i >= multiplicity) {
+      inContainer = false;
+      return next();
+    }
+
+    i++;
+
+    return {
+      done: false,
+      value: value
+    };
+  });
+};
+
+/**
+ * Method returning an iterator over the set's multiplicities.
+ *
+ * @return {Iterator}
+ */
+MultiSet.prototype.multiplicities = function() {
+  return this.items.entries();
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  MultiSet.prototype[Symbol.iterator] = MultiSet.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+MultiSet.prototype.inspect = function() {
+  return this.items;
+};
+
+if (typeof Symbol !== 'undefined')
+  MultiSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = MultiSet.prototype.inspect;
+MultiSet.prototype.toJSON = function() {
+  return this.items;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {MultiSet}
+ */
+MultiSet.from = function(iterable) {
+  var set = new MultiSet();
+
+  forEach(iterable, function(value) {
+    set.add(value);
+  });
+
+  return set;
+};
+
+/**
+ * Function returning whether the multiset A is a subset of the multiset B.
+ *
+ * @param  {MultiSet} A - First set.
+ * @param  {MultiSet} B - Second set.
+ * @return {boolean}
+ */
+MultiSet.isSubset = function(A, B) {
+  var iterator = A.multiplicities(),
+      step,
+      key,
+      mA;
+
+  // Shortcuts
+  if (A === B)
+    return true;
+
+  if (A.dimension > B.dimension)
+    return false;
+
+  while ((step = iterator.next(), !step.done)) {
+    key = step.value[0];
+    mA = step.value[1];
+
+    if (B.multiplicity(key) < mA)
+      return false;
+  }
+
+  return true;
+};
+
+/**
+ * Function returning whether the multiset A is a superset of the multiset B.
+ *
+ * @param  {MultiSet} A - First set.
+ * @param  {MultiSet} B - Second set.
+ * @return {boolean}
+ */
+MultiSet.isSuperset = function(A, B) {
+  return MultiSet.isSubset(B, A);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = MultiSet;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/package.json b/libs/shared/graph-layout/node_modules/mnemonist/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..476ab3eeebc04c4fc3c5f209e43840a9ab48b751
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/package.json
@@ -0,0 +1,103 @@
+{
+  "name": "mnemonist",
+  "version": "0.39.0",
+  "description": "Curated collection of data structures for the JavaScript/TypeScript.",
+  "scripts": {
+    "lint": "eslint ./*.js ./utils ./test",
+    "prepublishOnly": "npm run lint && npm test && npm run test:types",
+    "test": "mocha",
+    "test:types": "tsc --target es2015 --noEmit --noImplicitAny --noImplicitReturns ./test/types.ts"
+  },
+  "main": "./index.js",
+  "types": "./index.d.ts",
+  "files": [
+    "sort",
+    "utils",
+    "*.d.ts",
+    "*.js"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/yomguithereal/mnemonist.git"
+  },
+  "keywords": [
+    "bag",
+    "bimap",
+    "bit array",
+    "bit set",
+    "bit vector",
+    "bitset",
+    "bk tree",
+    "burkhard-keller tree",
+    "cache",
+    "circular buffer",
+    "counter",
+    "data structures",
+    "default map",
+    "deque",
+    "disjoint set",
+    "fibonacci heap",
+    "fuzzy map",
+    "hashed array tree",
+    "heap",
+    "interval tree",
+    "inverted index",
+    "kd tree",
+    "linked list",
+    "lru",
+    "lru cache",
+    "multimap",
+    "multiset",
+    "passjoin",
+    "queue",
+    "sparse map",
+    "sparse set",
+    "stack",
+    "structures",
+    "suffix tree",
+    "symspell",
+    "trie",
+    "union find",
+    "vantage point tree",
+    "vector",
+    "vp tree"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/yomguithereal/mnemonist/issues"
+  },
+  "homepage": "https://github.com/yomguithereal/mnemonist#readme",
+  "dependencies": {
+    "obliterator": "^2.0.1"
+  },
+  "devDependencies": {
+    "@yomguithereal/eslint-config": "^4.4.0",
+    "asciitree": "^1.0.2",
+    "damerau-levenshtein": "^1.0.7",
+    "eslint": "^8.2.0",
+    "leven": "^3.1.0",
+    "lodash": "^4.17.21",
+    "matcha": "^0.7.0",
+    "mocha": "^9.1.3",
+    "pandemonium": "^2.0.0",
+    "seedrandom": "^3.0.5",
+    "static-kdtree": "^1.0.2",
+    "typescript": "^4.5.2"
+  },
+  "eslintConfig": {
+    "extends": "@yomguithereal/eslint-config",
+    "parserOptions": {
+      "ecmaVersion": 6,
+      "ecmaFeatures": {
+        "forOf": true
+      }
+    },
+    "rules": {
+      "no-new": 0
+    }
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d9174676af330926b246c2777e3b519899a28a6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.d.ts
@@ -0,0 +1,54 @@
+/**
+ * Mnemonist PassjoinIndex Typings
+ * ================================
+ */
+type LevenshteinDistanceFunction<T> = (a: T, b: T) => number;
+
+export default class PassjoinIndex<T> implements Iterable<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(levenshtein: LevenshteinDistanceFunction<T>, k: number);
+
+  // Methods
+  add(value: T): this;
+  search(query: T): Set<T>;
+  clear(): void;
+  forEach(callback: (value: T, index: number, self: this) => void, scope?: any): void;
+  values(): IterableIterator<T>;
+  [Symbol.iterator](): IterableIterator<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    levenshtein: LevenshteinDistanceFunction<I>,
+    k: number
+  ): PassjoinIndex<I>;
+}
+
+export function countKeys(k: number, s: number): number;
+export function comparator<T>(a: T, b: T): number;
+export function partition(k: number, l: number): Array<[number, number]>;
+export function segments<T>(k: number, string: T): Array<T>;
+export function segmentPos<T>(k: number, i: number, string: T): number;
+
+export function multiMatchAwareInterval(
+  k: number,
+  delta: number,
+  i: number,
+  s: number,
+  pi: number,
+  li: number
+): [number, number];
+
+export function multiMatchAwareSubstrings<T>(
+  k: number,
+  string: T,
+  l: number,
+  i: number,
+  pi: number,
+  li: number
+): Array<T>;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.js b/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.js
new file mode 100644
index 0000000000000000000000000000000000000000..652d61433978f4e681ff1a0fde9088d893cb1493
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/passjoin-index.js
@@ -0,0 +1,518 @@
+/**
+ * Mnemonist PassjoinIndex
+ * ========================
+ *
+ * The PassjoinIndex is an index leveraging the "passjoin" algorithm as a mean
+ * to index strings for Levenshtein distance queries. It features a complexity
+ * related to the Levenshtein query threshold k rather than the number of
+ * strings to test (roughly O(k^3)).
+ *
+ * [References]:
+ * Jiang, Yu, Dong Deng, Jiannan Wang, Guoliang Li, et Jianhua Feng.
+ * « Efficient Parallel Partition-Based Algorithms for Similarity Search and Join
+ * with Edit Distance Constraints ». In Proceedings of the Joint EDBT/ICDT 2013
+ * Workshops on - EDBT ’13, 341. Genoa, Italy: ACM Press, 2013.
+ * https://doi.org/10.1145/2457317.2457382.
+ *
+ * Li, Guoliang, Dong Deng, et Jianhua Feng. « A Partition-Based Method for
+ * String Similarity Joins with Edit-Distance Constraints ». ACM Transactions on
+ * Database Systems 38, no 2 (1 juin 2013): 1‑33.
+ * https://doi.org/10.1145/2487259.2487261.
+ *
+ * [Urls]:
+ * http://people.csail.mit.edu/dongdeng/projects/passjoin/index.html
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach');
+
+// TODO: leveraging BagDistance as an upper bound of Levenshtein
+// TODO: leverage n-grams recursive indexing
+// TODO: try the MultiArray as a memory backend
+// TODO: what about damerau levenshtein
+
+/**
+ * Helpers.
+ */
+
+/**
+ * Function returning the number of substrings that will be selected by the
+ * multi-match-aware selection scheme for theshold `k`, for a string of length
+ * `s` to match strings of length `l`.
+ *
+ * @param   {number} k - Levenshtein distance threshold.
+ * @param   {number} s - Length of target strings.
+ * @param   {number} l - Length of strings to match.
+ * @returns {number}   - The number of selected substrings.
+ */
+function countSubstringsL(k, s, l) {
+  return (((Math.pow(k, 2) - Math.pow(Math.abs(s - l), 2)) / 2) | 0) + k + 1;
+}
+
+/**
+ * Function returning the minimum number of substrings that will be selected by
+ * the multi-match-aware selection scheme for theshold `k`, for a string of
+ * length `s` to match any string of relevant length.
+ *
+ * @param   {number} k - Levenshtein distance threshold.
+ * @param   {number} s - Length of target strings.
+ * @returns {number}   - The number of selected substrings.
+ */
+function countKeys(k, s) {
+  var c = 0;
+
+  for (var l = 0, m = s + 1; l < m; l++)
+    c += countSubstringsL(k, s, l);
+
+  return c;
+}
+
+/**
+ * Function used to compare two keys in order to sort them first by decreasing
+ * length and then alphabetically as per the "4.2 Effective Indexing Strategy"
+ * point of the paper.
+ *
+ * @param   {number} k - Levenshtein distance threshold.
+ * @param   {number} s - Length of target strings.
+ * @returns {number}   - The number of selected substrings.
+ */
+function comparator(a, b) {
+  if (a.length > b.length)
+    return -1;
+  if (a.length < b.length)
+    return 1;
+
+  if (a < b)
+    return -1;
+  if (a > b)
+    return 1;
+
+  return 0;
+}
+
+/**
+ * Function partitioning a string into k + 1 uneven segments, the shorter
+ * ones, then the longer ones.
+ *
+ * @param   {number} k - Levenshtein distance threshold.
+ * @param   {number} l - Length of the string.
+ * @returns {Array}    - The partition tuples (start, length).
+ */
+function partition(k, l) {
+  var m = k + 1,
+      a = (l / m) | 0,
+      b = a + 1,
+      i,
+      j;
+
+  var largeSegments = l - a * m,
+      smallSegments = m - largeSegments;
+
+  var tuples = new Array(k + 1);
+
+  for (i = 0; i < smallSegments; i++)
+    tuples[i] = [i * a, a];
+
+  var offset = (i - 1) * a + a;
+
+  for (j = 0; j < largeSegments; j++)
+    tuples[i + j] = [offset + j * b, b];
+
+  return tuples;
+}
+
+/**
+ * Function yielding a string's k + 1 passjoin segments to index.
+ *
+ * @param   {number} k      - Levenshtein distance threshold.
+ * @param   {string} string - Target string.
+ * @returns {Array}         - The string's segments.
+ */
+function segments(k, string) {
+  var l = string.length,
+      m = k + 1,
+      a = (l / m) | 0,
+      b = a + 1,
+      o,
+      i,
+      j;
+
+  var largeSegments = l - a * m,
+      smallSegments = m - largeSegments;
+
+  var S = new Array(k + 1);
+
+  for (i = 0; i < smallSegments; i++) {
+    o = i * a;
+    S[i] = string.slice(o, o + a);
+  }
+
+  var offset = (i - 1) * a + a;
+
+  for (j = 0; j < largeSegments; j++) {
+    o = offset + j * b;
+    S[i + j] = string.slice(o, o + b);
+  }
+
+  return S;
+}
+
+// TODO: jsdocs
+function segmentPos(k, i, string) {
+  if (i === 0)
+    return 0;
+
+  var l = string.length;
+
+  var m = k + 1,
+      a = (l / m) | 0,
+      b = a + 1;
+
+  var largeSegments = l - a * m,
+      smallSegments = m - largeSegments;
+
+  if (i <= smallSegments - 1)
+    return i * a;
+
+  var offset = i - smallSegments;
+
+  return smallSegments * a + offset * b;
+}
+
+/**
+ * Function returning the interval of relevant substrings to lookup using the
+ * multi-match-aware substring selection scheme described in the paper.
+ *
+ * @param   {number} k      - Levenshtein distance threshold.
+ * @param   {number} delta  - Signed length difference between both considered strings.
+ * @param   {number} i      - k + 1 segment index.
+ * @param   {number} s      - String's length.
+ * @param   {number} pi     - k + 1 segment position in target string.
+ * @param   {number} li     - k + 1 segment length.
+ * @returns {Array}         - The interval (start, stop).
+ */
+function multiMatchAwareInterval(k, delta, i, s, pi, li) {
+  var start1 = pi - i,
+      end1 = pi + i;
+
+  var o = k - i;
+
+  var start2 = pi + delta - o,
+      end2 = pi + delta + o;
+
+  var end3 = s - li;
+
+  return [Math.max(0, start1, start2), Math.min(end1, end2, end3)];
+}
+
+/**
+ * Function yielding relevant substrings to lookup using the multi-match-aware
+ * substring selection scheme described in the paper.
+ *
+ * @param   {number} k      - Levenshtein distance threshold.
+ * @param   {string} string  - Target string.
+ * @param   {number} l      - Length of strings to match.
+ * @param   {number} i      - k + 1 segment index.
+ * @param   {number} pi     - k + 1 segment position in target string.
+ * @param   {number} li     - k + 1 segment length.
+ * @returns {Array}         - The contiguous substrings.
+ */
+function multiMatchAwareSubstrings(k, string, l, i, pi, li) {
+  var s = string.length;
+
+  // Note that we need to keep the non-absolute delta for this function
+  // to work in both directions, up & down
+  var delta = s - l;
+
+  var interval = multiMatchAwareInterval(k, delta, i, s, pi, li);
+
+  var start = interval[0],
+      stop = interval[1];
+
+  var currentSubstring = '';
+
+  var substrings = [];
+
+  var substring, j, m;
+
+  for (j = start, m = stop + 1; j < m; j++) {
+    substring = string.slice(j, j + li);
+
+    // We skip identical consecutive substrings (to avoid repetition in case
+    // of contiguous letter duplication)
+    if (substring === currentSubstring)
+      continue;
+
+    substrings.push(substring);
+
+    currentSubstring = substring;
+  }
+
+  return substrings;
+}
+
+/**
+ * PassjoinIndex.
+ *
+ * @note I tried to apply the paper's optimizations regarding Levenshtein
+ * distance computations but it did not provide a performance boost, quite
+ * the contrary. This is because since we are mostly using the index for small k
+ * here, most of the strings we work on are quite small and the bookkeeping
+ * induced by Ukkonen's method and the paper's one are slowing us down more than
+ * they actually help us go faster.
+ *
+ * @note This implementation does not try to ensure that you add the same string
+ * more than once.
+ *
+ * @constructor
+ * @param {function} levenshtein - Levenshtein distance function.
+ * @param {number}   k           - Levenshtein distance threshold.
+ */
+function PassjoinIndex(levenshtein, k) {
+  if (typeof levenshtein !== 'function')
+    throw new Error('mnemonist/passjoin-index: `levenshtein` should be a function returning edit distance between two strings.');
+
+  if (typeof k !== 'number' || k < 1)
+    throw new Error('mnemonist/passjoin-index: `k` should be a number > 0');
+
+  this.levenshtein = levenshtein;
+  this.k = k;
+  this.clear();
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+PassjoinIndex.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.strings = [];
+  this.invertedIndices = {};
+};
+
+/**
+ * Method used to add a new value to the index.
+ *
+ * @param  {string|Array} value - Value to add.
+ * @return {PassjoinIndex}
+ */
+PassjoinIndex.prototype.add = function(value) {
+  var l = value.length;
+
+  var stringIndex = this.size;
+
+  this.strings.push(value);
+  this.size++;
+
+  var S = segments(this.k, value);
+
+  var Ll = this.invertedIndices[l];
+
+  if (typeof Ll === 'undefined') {
+    Ll = {};
+    this.invertedIndices[l] = Ll;
+  }
+
+  var segment,
+      matches,
+      key,
+      i,
+      m;
+
+  for (i = 0, m = S.length; i < m; i++) {
+    segment = S[i];
+    key = segment + i;
+    matches = Ll[key];
+
+    if (typeof matches === 'undefined') {
+      matches = [stringIndex];
+      Ll[key] = matches;
+    }
+    else {
+      matches.push(stringIndex);
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Method used to search for string matching the given query.
+ *
+ * @param  {string|Array} query - Query string.
+ * @return {Array}
+ */
+PassjoinIndex.prototype.search = function(query) {
+  var s = query.length,
+      k = this.k;
+
+  var M = new Set();
+
+  var candidates,
+      candidate,
+      queryPos,
+      querySegmentLength,
+      key,
+      S,
+      P,
+      l,
+      m,
+      i,
+      n1,
+      j,
+      n2,
+      y,
+      n3;
+
+  for (l = Math.max(0, s - k), m = s + k + 1; l < m; l++) {
+    var Ll = this.invertedIndices[l];
+
+    if (typeof Ll === 'undefined')
+      continue;
+
+    P = partition(k, l);
+
+    for (i = 0, n1 = P.length; i < n1; i++) {
+      queryPos = P[i][0];
+      querySegmentLength = P[i][1];
+
+      S = multiMatchAwareSubstrings(
+        k,
+        query,
+        l,
+        i,
+        queryPos,
+        querySegmentLength
+      );
+
+      // Empty string edge case
+      if (!S.length)
+        S = [''];
+
+      for (j = 0, n2 = S.length; j < n2; j++) {
+        key = S[j] + i;
+        candidates = Ll[key];
+
+        if (typeof candidates === 'undefined')
+          continue;
+
+        for (y = 0, n3 = candidates.length; y < n3; y++) {
+          candidate = this.strings[candidates[y]];
+
+          // NOTE: first condition is here not to compute Levenshtein
+          // distance for tiny strings
+
+          // NOTE: maintaining a Set of rejected candidate is not really useful
+          // because it consumes more memory and because non-matches are
+          // less likely to be candidates agains
+          if (
+            s <= k && l <= k ||
+            (
+              !M.has(candidate) &&
+              this.levenshtein(query, candidate) <= k
+            )
+          )
+            M.add(candidate);
+        }
+      }
+    }
+  }
+
+  return M;
+};
+
+/**
+ * Method used to iterate over the index.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+PassjoinIndex.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = 0, l = this.strings.length; i < l; i++)
+    callback.call(scope, this.strings[i], i, this);
+};
+
+/**
+ * Method used to create an iterator over a index's values.
+ *
+ * @return {Iterator}
+ */
+PassjoinIndex.prototype.values = function() {
+  var strings = this.strings,
+      l = strings.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = strings[i];
+    i++;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  PassjoinIndex.prototype[Symbol.iterator] = PassjoinIndex.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+PassjoinIndex.prototype.inspect = function() {
+  var array = this.strings.slice();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: PassjoinIndex,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  PassjoinIndex.prototype[Symbol.for('nodejs.util.inspect.custom')] = PassjoinIndex.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {PassjoinIndex}
+ */
+PassjoinIndex.from = function(iterable, levenshtein, k) {
+  var index = new PassjoinIndex(levenshtein, k);
+
+  forEach(iterable, function(string) {
+    index.add(string);
+  });
+
+  return index;
+};
+
+/**
+ * Exporting.
+ */
+PassjoinIndex.countKeys = countKeys;
+PassjoinIndex.comparator = comparator;
+PassjoinIndex.partition = partition;
+PassjoinIndex.segments = segments;
+PassjoinIndex.segmentPos = segmentPos;
+PassjoinIndex.multiMatchAwareInterval = multiMatchAwareInterval;
+PassjoinIndex.multiMatchAwareSubstrings = multiMatchAwareSubstrings;
+
+module.exports = PassjoinIndex;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/queue.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/queue.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d3e43453ead10af8c068914cb5cb32f8beabec2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/queue.d.ts
@@ -0,0 +1,27 @@
+/**
+ * Mnemonist Queue Typings
+ * ========================
+ */
+export default class Queue<T> implements Iterable<T> {
+
+  // Members
+  size: number;
+
+  // Methods
+  clear(): void;
+  enqueue(item: T): number;
+  dequeue(): T | undefined;
+  peek(): T | undefined;
+  forEach(callback: (item: T, index: number, queue: this) => void, scope?: any): void;
+  toArray(): Array<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  toString(): string;
+  toJSON(): Array<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): Queue<I>;
+  static of<I>(...items: Array<I>): Queue<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/queue.js b/libs/shared/graph-layout/node_modules/mnemonist/queue.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa554b6148e4e0ba8a972d98e3e9e9681762eaa2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/queue.js
@@ -0,0 +1,215 @@
+/**
+ * Mnemonist Queue
+ * ================
+ *
+ * Queue implementation based on the ideas of Queue.js that seems to beat
+ * a LinkedList one in performance.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach');
+
+/**
+ * Queue
+ *
+ * @constructor
+ */
+function Queue() {
+  this.clear();
+}
+
+/**
+ * Method used to clear the queue.
+ *
+ * @return {undefined}
+ */
+Queue.prototype.clear = function() {
+
+  // Properties
+  this.items = [];
+  this.offset = 0;
+  this.size = 0;
+};
+
+/**
+ * Method used to add an item to the queue.
+ *
+ * @param  {any}    item - Item to enqueue.
+ * @return {number}
+ */
+Queue.prototype.enqueue = function(item) {
+
+  this.items.push(item);
+  return ++this.size;
+};
+
+/**
+ * Method used to retrieve & remove the first item of the queue.
+ *
+ * @return {any}
+ */
+Queue.prototype.dequeue = function() {
+  if (!this.size)
+    return;
+
+  var item = this.items[this.offset];
+
+  if (++this.offset * 2 >= this.items.length) {
+    this.items = this.items.slice(this.offset);
+    this.offset = 0;
+  }
+
+  this.size--;
+
+  return item;
+};
+
+/**
+ * Method used to retrieve the first item of the queue.
+ *
+ * @return {any}
+ */
+Queue.prototype.peek = function() {
+  if (!this.size)
+    return;
+
+  return this.items[this.offset];
+};
+
+/**
+ * Method used to iterate over the queue.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+Queue.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = this.offset, j = 0, l = this.items.length; i < l; i++, j++)
+    callback.call(scope, this.items[i], j, this);
+};
+
+/*
+ * Method used to convert the queue to a JavaScript array.
+ *
+ * @return {array}
+ */
+Queue.prototype.toArray = function() {
+  return this.items.slice(this.offset);
+};
+
+/**
+ * Method used to create an iterator over a queue's values.
+ *
+ * @return {Iterator}
+ */
+Queue.prototype.values = function() {
+  var items = this.items,
+      i = this.offset;
+
+  return new Iterator(function() {
+    if (i >= items.length)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+    i++;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a queue's entries.
+ *
+ * @return {Iterator}
+ */
+Queue.prototype.entries = function() {
+  var items = this.items,
+      i = this.offset,
+      j = 0;
+
+  return new Iterator(function() {
+    if (i >= items.length)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+    i++;
+
+    return {
+      value: [j++, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  Queue.prototype[Symbol.iterator] = Queue.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+Queue.prototype.toString = function() {
+  return this.toArray().join(',');
+};
+
+Queue.prototype.toJSON = function() {
+  return this.toArray();
+};
+
+Queue.prototype.inspect = function() {
+  var array = this.toArray();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: Queue,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  Queue.prototype[Symbol.for('nodejs.util.inspect.custom')] = Queue.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a queue.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @return {Queue}
+ */
+Queue.from = function(iterable) {
+  var queue = new Queue();
+
+  forEach(iterable, function(value) {
+    queue.enqueue(value);
+  });
+
+  return queue;
+};
+
+/**
+ * Static @.of function taking an arbitrary number of arguments & converting it
+ * into a queue.
+ *
+ * @param  {...any} args
+ * @return {Queue}
+ */
+Queue.of = function() {
+  return Queue.from(arguments);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = Queue;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/semi-dynamic-trie.js b/libs/shared/graph-layout/node_modules/mnemonist/semi-dynamic-trie.js
new file mode 100644
index 0000000000000000000000000000000000000000..6627d348ef7c0b7f2560ac7b9349ec7def45bc1e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/semi-dynamic-trie.js
@@ -0,0 +1,251 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist SemiDynamicTrie
+ * ==========================
+ *
+ * Lowlevel Trie working at character level, storing information in typed
+ * array and organizing its children in linked lists.
+ *
+ * This implementation also uses a "fat node" strategy to boost access to some
+ * bloated node's children when the number of children rises above a certain
+ * threshold.
+ */
+var Vector = require('./vector.js');
+
+// TODO: rename => ternary search tree
+
+/**
+ * Constants.
+ */
+const MAX_LINKED = 7;
+
+/**
+ * SemiDynamicTrie.
+ *
+ * @constructor
+ */
+function SemiDynamicTrie() {
+
+  // Properties
+
+  // TODO: make it 16 bits
+  this.characters = new Vector.Uint8Vector(256);
+  this.nextPointers = new Vector.Int32Vector(256);
+  this.childPointers = new Vector.Uint32Vector(256);
+  this.maps = new Vector.Uint32Vector(256);
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+SemiDynamicTrie.prototype.clear = function() {
+
+  // Properties
+};
+
+SemiDynamicTrie.prototype.ensureSibling = function(block, character) {
+  var nextCharacter,
+      nextBlock,
+      newBlock;
+
+  // Do we have a root?
+  if (this.characters.length === 0) {
+
+    this.nextPointers.push(0);
+    this.childPointers.push(0);
+    this.characters.push(character);
+
+    return block;
+  }
+
+  // Are we traversing a fat node?
+  var fatNode = this.nextPointers.array[block];
+
+  if (fatNode < 0) {
+    var mapIndex = -fatNode + character;
+
+    nextBlock = this.maps.array[mapIndex];
+
+    if (nextBlock !== 0)
+      return nextBlock;
+
+    newBlock = this.characters.length;
+
+    this.nextPointers.push(0);
+    this.childPointers.push(0);
+    this.characters.push(character);
+
+    this.maps.set(mapIndex, newBlock);
+
+    return newBlock;
+  }
+
+  var listLength = 1,
+      startingBlock = block;
+
+  while (true) {
+    nextCharacter = this.characters.array[block];
+
+    if (nextCharacter === character)
+      return block;
+
+    nextBlock = this.nextPointers.array[block];
+
+    if (nextBlock === 0)
+      break;
+
+    listLength++;
+    block = nextBlock;
+  }
+
+  // If the list is too long, we create a fat node
+  if (listLength > MAX_LINKED) {
+    block = startingBlock;
+
+    var offset = this.maps.length;
+
+    this.maps.resize(offset + 255);
+    this.maps.set(offset + 255, 0);
+
+    while (true) {
+      nextBlock = this.nextPointers.array[block];
+
+      if (nextBlock === 0)
+        break;
+
+      nextCharacter = this.characters.array[nextBlock];
+      this.maps.set(offset + nextCharacter, nextBlock);
+
+      block = nextBlock;
+    }
+
+    this.nextPointers.set(startingBlock, -offset);
+
+    newBlock = this.characters.length;
+
+    this.nextPointers.push(0);
+    this.childPointers.push(0);
+    this.characters.push(character);
+
+    this.maps.set(offset + character, newBlock);
+
+    return newBlock;
+  }
+
+  // Else, we append the character to the list
+  newBlock = this.characters.length;
+
+  this.nextPointers.push(0);
+  this.childPointers.push(0);
+  this.nextPointers.set(block, newBlock);
+  this.characters.push(character);
+
+  return newBlock;
+};
+
+SemiDynamicTrie.prototype.findSibling = function(block, character) {
+  var nextCharacter;
+
+  // Do we have a fat node?
+  var fatNode = this.nextPointers.array[block];
+
+  if (fatNode < 0) {
+    var mapIndex = -fatNode + character;
+
+    var nextBlock = this.maps.array[mapIndex];
+
+    if (nextBlock === 0)
+      return -1;
+
+    return nextBlock;
+  }
+
+  while (true) {
+    nextCharacter = this.characters.array[block];
+
+    if (nextCharacter === character)
+      return block;
+
+    block = this.nextPointers.array[block];
+
+    if (block === 0)
+      return -1;
+  }
+};
+
+SemiDynamicTrie.prototype.add = function(key) {
+  var keyCharacter,
+      childBlock,
+      block = 0;
+
+  var i = 0, l = key.length;
+
+  // Going as far as possible
+  while (i < l) {
+    keyCharacter = key.charCodeAt(i);
+
+    // Ensuring a correct sibling exists
+    block = this.ensureSibling(block, keyCharacter);
+
+    i++;
+
+    if (i < l) {
+
+      // Descending
+      childBlock = this.childPointers.array[block];
+
+      if (childBlock === 0)
+        break;
+
+      block = childBlock;
+    }
+  }
+
+  // Adding as many blocks as necessary
+  while (i < l) {
+
+    childBlock = this.characters.length;
+    this.characters.push(key.charCodeAt(i));
+
+    this.childPointers.push(0);
+    this.nextPointers.push(0);
+    this.childPointers.set(block, childBlock);
+
+    block = childBlock;
+
+    i++;
+  }
+};
+
+SemiDynamicTrie.prototype.has = function(key) {
+  var i, l;
+
+  var block = 0,
+      siblingBlock;
+
+  for (i = 0, l = key.length; i < l; i++) {
+    siblingBlock = this.findSibling(block, key.charCodeAt(i));
+
+    if (siblingBlock === -1)
+      return false;
+
+    // TODO: be sure
+    if (i === l - 1)
+      return true;
+
+    block = this.childPointers.array[siblingBlock];
+
+    if (block === 0)
+      return false;
+  }
+
+  // TODO: fix, should have a leaf pointer somehow
+  return true;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = SemiDynamicTrie;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc8dae8148a5430decd9ead1011787294d864cb2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/set.d.ts
@@ -0,0 +1,18 @@
+/**
+ * Mnemonist Set Typings
+ * ======================
+ */
+export function intersection<T>(...set: Array<Set<T>>): Set<T>;
+export function union<T>(...set: Array<Set<T>>): Set<T>;
+export function difference<T>(a: Set<T>, b: Set<T>): Set<T>;
+export function symmetricDifference<T>(a: Set<T>, b: Set<T>): Set<T>;
+export function isSubset<T>(a: Set<T>, b: Set<T>): boolean;
+export function isSuperset<T>(a: Set<T>, b: Set<T>): boolean;
+export function add<T>(a: Set<T>, b: Set<T>): void;
+export function subtract<T>(a: Set<T>, b: Set<T>): void;
+export function intersect<T>(a: Set<T>, b: Set<T>): void;
+export function disjunct<T>(a: Set<T>, b: Set<T>): void;
+export function intersectionSize<T>(a: Set<T>, b:Set<T>): number;
+export function unionSize<T>(a: Set<T>, b:Set<T>): number;
+export function jaccard<T>(a: Set<T>, b:Set<T>): number;
+export function overlap<T>(a: Set<T>, b: Set<T>): number;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/set.js b/libs/shared/graph-layout/node_modules/mnemonist/set.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0d020bbaebd8c7e6c0df92f5498e4c6bf1c5613
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/set.js
@@ -0,0 +1,356 @@
+/**
+ * Mnemonist Set
+ * ==============
+ *
+ * Useful function related to sets such as union, intersection and so on...
+ */
+
+// TODO: optimize versions for less variadicities
+
+/**
+ * Variadic function computing the intersection of multiple sets.
+ *
+ * @param  {...Set} sets - Sets to intersect.
+ * @return {Set}         - The intesection.
+ */
+exports.intersection = function() {
+  if (arguments.length < 2)
+    throw new Error('mnemonist/Set.intersection: needs at least two arguments.');
+
+  var I = new Set();
+
+  // First we need to find the smallest set
+  var smallestSize = Infinity,
+      smallestSet = null;
+
+  var s, i, l = arguments.length;
+
+  for (i = 0; i < l; i++) {
+    s = arguments[i];
+
+    // If one of the set has no items, we can stop right there
+    if (s.size === 0)
+      return I;
+
+    if (s.size < smallestSize) {
+      smallestSize = s.size;
+      smallestSet = s;
+    }
+  }
+
+  // Now we need to intersect this set with the others
+  var iterator = smallestSet.values(),
+      step,
+      item,
+      add,
+      set;
+
+  // TODO: we can optimize by iterating each next time over the current intersection
+  // but this probably means more RAM to consume since we'll create n-1 sets rather than
+  // only the one.
+  while ((step = iterator.next(), !step.done)) {
+    item = step.value;
+    add = true;
+
+    for (i = 0; i < l; i++) {
+      set = arguments[i];
+
+      if (set === smallestSet)
+        continue;
+
+      if (!set.has(item)) {
+        add = false;
+        break;
+      }
+    }
+
+    if (add)
+      I.add(item);
+  }
+
+  return I;
+};
+
+/**
+ * Variadic function computing the union of multiple sets.
+ *
+ * @param  {...Set} sets - Sets to unite.
+ * @return {Set}         - The union.
+ */
+exports.union = function() {
+  if (arguments.length < 2)
+    throw new Error('mnemonist/Set.union: needs at least two arguments.');
+
+  var U = new Set();
+
+  var i, l = arguments.length;
+
+  var iterator,
+      step;
+
+  for (i = 0; i < l; i++) {
+    iterator = arguments[i].values();
+
+    while ((step = iterator.next(), !step.done))
+      U.add(step.value);
+  }
+
+  return U;
+};
+
+/**
+ * Function computing the difference between two sets.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {Set}   - The difference.
+ */
+exports.difference = function(A, B) {
+
+  // If first set is empty
+  if (!A.size)
+    return new Set();
+
+  if (!B.size)
+    return new Set(A);
+
+  var D = new Set();
+
+  var iterator = A.values(),
+      step;
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!B.has(step.value))
+      D.add(step.value);
+  }
+
+  return D;
+};
+
+/**
+ * Function computing the symmetric difference between two sets.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {Set}   - The symmetric difference.
+ */
+exports.symmetricDifference = function(A, B) {
+  var S = new Set();
+
+  var iterator = A.values(),
+      step;
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!B.has(step.value))
+      S.add(step.value);
+  }
+
+  iterator = B.values();
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!A.has(step.value))
+      S.add(step.value);
+  }
+
+  return S;
+};
+
+/**
+ * Function returning whether A is a subset of B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {boolean}
+ */
+exports.isSubset = function(A, B) {
+  var iterator = A.values(),
+      step;
+
+  // Shortcuts
+  if (A === B)
+    return true;
+
+  if (A.size > B.size)
+    return false;
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!B.has(step.value))
+      return false;
+  }
+
+  return true;
+};
+
+/**
+ * Function returning whether A is a superset of B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {boolean}
+ */
+exports.isSuperset = function(A, B) {
+  return exports.isSubset(B, A);
+};
+
+/**
+ * Function adding the items of set B to the set A.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ */
+exports.add = function(A, B) {
+  var iterator = B.values(),
+      step;
+
+  while ((step = iterator.next(), !step.done))
+    A.add(step.value);
+
+  return;
+};
+
+/**
+ * Function subtracting the items of set B from the set A.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ */
+exports.subtract = function(A, B) {
+  var iterator = B.values(),
+      step;
+
+  while ((step = iterator.next(), !step.done))
+    A.delete(step.value);
+
+  return;
+};
+
+/**
+ * Function intersecting the items of A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ */
+exports.intersect = function(A, B) {
+  var iterator = A.values(),
+      step;
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!B.has(step.value))
+      A.delete(step.value);
+  }
+
+  return;
+};
+
+/**
+ * Function disjuncting the items of A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ */
+exports.disjunct = function(A, B) {
+  var iterator = A.values(),
+      step;
+
+  var toRemove = [];
+
+  while ((step = iterator.next(), !step.done)) {
+    if (B.has(step.value))
+      toRemove.push(step.value);
+  }
+
+  iterator = B.values();
+
+  while ((step = iterator.next(), !step.done)) {
+    if (!A.has(step.value))
+      A.add(step.value);
+  }
+
+  for (var i = 0, l = toRemove.length; i < l; i++)
+    A.delete(toRemove[i]);
+
+  return;
+};
+
+/**
+ * Function returning the size of the intersection of A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {number}
+ */
+exports.intersectionSize = function(A, B) {
+  var tmp;
+
+  // We need to know the smallest set
+  if (A.size > B.size) {
+    tmp = A;
+    A = B;
+    B = tmp;
+  }
+
+  if (A.size === 0)
+    return 0;
+
+  if (A === B)
+    return A.size;
+
+  var iterator = A.values(),
+      step;
+
+  var I = 0;
+
+  while ((step = iterator.next(), !step.done)) {
+    if (B.has(step.value))
+      I++;
+  }
+
+  return I;
+};
+
+/**
+ * Function returning the size of the union of A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {number}
+ */
+exports.unionSize = function(A, B) {
+  var I = exports.intersectionSize(A, B);
+
+  return A.size + B.size - I;
+};
+
+/**
+ * Function returning the Jaccard similarity between A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {number}
+ */
+exports.jaccard = function(A, B) {
+  var I = exports.intersectionSize(A, B);
+
+  if (I === 0)
+    return 0;
+
+  var U = A.size + B.size - I;
+
+  return I / U;
+};
+
+/**
+ * Function returning the overlap coefficient between A & B.
+ *
+ * @param  {Set} A - First set.
+ * @param  {Set} B - Second set.
+ * @return {number}
+ */
+exports.overlap = function(A, B) {
+  var I = exports.intersectionSize(A, B);
+
+  if (I === 0)
+    return 0;
+
+  return I / Math.min(A.size, B.size);
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db22f9b719ef541169c53339d2a7cf28f8a5eb1f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.d.ts
@@ -0,0 +1,4 @@
+import {IArrayLike} from '../utils/types';
+
+export function inplaceInsertionSort(array: IArrayLike, lo: number, hi: number): IArrayLike;
+export function inplaceInsertionSortIndices(array: IArrayLike, indices: IArrayLike, lo: number, hi: number): IArrayLike;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.js b/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.js
new file mode 100644
index 0000000000000000000000000000000000000000..aebd1ad0b0b3e49f1a8e43b2be8c7f869ee1d39f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sort/insertion.js
@@ -0,0 +1,50 @@
+/**
+ * Mnemonist Insertion Sort
+ * =========================
+ *
+ * Insertion sort related functions.
+ */
+function inplaceInsertionSort(array, lo, hi) {
+  i = lo + 1;
+
+  var j, k;
+
+  for (; i < hi; i++) {
+    k = array[i];
+    j = i - 1;
+
+    while (j >= lo && array[j] > k) {
+      array[j + 1] = array[j];
+      j--;
+    }
+
+    array[j + 1] = k;
+  }
+
+  return array;
+}
+
+exports.inplaceInsertionSort = inplaceInsertionSort;
+
+function inplaceInsertionSortIndices(array, indices, lo, hi) {
+  i = lo + 1;
+
+  var j, k, t;
+
+  for (; i < hi; i++) {
+    t = indices[i];
+    k = array[t];
+    j = i - 1;
+
+    while (j >= lo && array[indices[j]] > k) {
+      indices[j + 1] = indices[j];
+      j--;
+    }
+
+    indices[j + 1] = t;
+  }
+
+  return indices;
+}
+
+exports.inplaceInsertionSortIndices = inplaceInsertionSortIndices;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5e6c90d11a2ac15909c76f7027da7e06ebd18e7b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.d.ts
@@ -0,0 +1,4 @@
+import {IArrayLike} from '../utils/types';
+
+export function inplaceQuickSort(array: IArrayLike, lo: number, hi: number): IArrayLike;
+export function inplaceQuickSortIndices(array: IArrayLike, indices: IArrayLike, lo: number, hi: number): IArrayLike;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.js b/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.js
new file mode 100644
index 0000000000000000000000000000000000000000..008d0fd8767fd8db383116417c927266366a3f89
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sort/quick.js
@@ -0,0 +1,116 @@
+/**
+ * Mnemonist Quick Sort
+ * =====================
+ *
+ * Quick sort related functions.
+ * Adapted from: https://alienryderflex.com/quicksort/
+ */
+var LOS = new Float64Array(64),
+    HIS = new Float64Array(64);
+
+function inplaceQuickSort(array, lo, hi) {
+  var p, i, l, r, swap;
+
+  LOS[0] = lo;
+  HIS[0] = hi;
+  i = 0;
+
+  while (i >= 0) {
+    l = LOS[i];
+    r = HIS[i] - 1;
+
+    if (l < r) {
+      p = array[l];
+
+      while (l < r) {
+        while (array[r] >= p && l < r)
+          r--;
+
+        if (l < r)
+          array[l++] = array[r];
+
+        while (array[l] <= p && l < r)
+          l++;
+
+        if (l < r)
+          array[r--] = array[l];
+      }
+
+      array[l] = p;
+      LOS[i + 1] = l + 1;
+      HIS[i + 1] = HIS[i];
+      HIS[i++] = l;
+
+      if (HIS[i] - LOS[i] > HIS[i - 1] - LOS[i - 1]) {
+        swap = LOS[i];
+        LOS[i] = LOS[i - 1];
+        LOS[i - 1] = swap;
+
+        swap = HIS[i];
+        HIS[i] = HIS[i - 1];
+        HIS[i - 1] = swap;
+      }
+    }
+    else {
+      i--;
+    }
+  }
+
+  return array;
+}
+
+exports.inplaceQuickSort = inplaceQuickSort;
+
+function inplaceQuickSortIndices(array, indices, lo, hi) {
+  var p, i, l, r, t, swap;
+
+  LOS[0] = lo;
+  HIS[0] = hi;
+  i = 0;
+
+  while (i >= 0) {
+    l = LOS[i];
+    r = HIS[i] - 1;
+
+    if (l < r) {
+      t = indices[l];
+      p = array[t];
+
+      while (l < r) {
+        while (array[indices[r]] >= p && l < r)
+          r--;
+
+        if (l < r)
+          indices[l++] = indices[r];
+
+        while (array[indices[l]] <= p && l < r)
+          l++;
+
+        if (l < r)
+          indices[r--] = indices[l];
+      }
+
+      indices[l] = t;
+      LOS[i + 1] = l + 1;
+      HIS[i + 1] = HIS[i];
+      HIS[i++] = l;
+
+      if (HIS[i] - LOS[i] > HIS[i - 1] - LOS[i - 1]) {
+        swap = LOS[i];
+        LOS[i] = LOS[i - 1];
+        LOS[i - 1] = swap;
+
+        swap = HIS[i];
+        HIS[i] = HIS[i - 1];
+        HIS[i - 1] = swap;
+      }
+    }
+    else {
+      i--;
+    }
+  }
+
+  return indices;
+}
+
+exports.inplaceQuickSortIndices = inplaceQuickSortIndices;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b22f9050f6f04d761739dbfea4dfcb7464dccbe
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.d.ts
@@ -0,0 +1,26 @@
+/**
+ * Mnemonist SparseMap Typings
+ * ============================
+ */
+export default class SparseMap<V> implements Iterable<[number, V]> {
+
+  // Members
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number);
+
+  // Methods
+  clear(): void;
+  has(key: number): boolean;
+  get(key: number): V | undefined;
+  set(key: number, value: V): this;
+  delete(key: number): boolean;
+  forEach(callback: (value: V, key: number, set: this) => void, scope?: any): void;
+  keys(): IterableIterator<number>;
+  values(): IterableIterator<V>;
+  entries(): IterableIterator<[number, V]>;
+  [Symbol.iterator](): IterableIterator<[number, V]>;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.js b/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5cf20d610270854e658966c189f91757961b2fa
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-map.js
@@ -0,0 +1,243 @@
+/**
+ * Mnemonist SparseMap
+ * ====================
+ *
+ * JavaScript sparse map implemented on top of byte arrays.
+ *
+ * [Reference]: https://research.swtch.com/sparse
+ */
+var Iterator = require('obliterator/iterator'),
+    getPointerArray = require('./utils/typed-arrays.js').getPointerArray;
+
+/**
+ * SparseMap.
+ *
+ * @constructor
+ */
+function SparseMap(Values, length) {
+  if (arguments.length < 2) {
+    length = Values;
+    Values = Array;
+  }
+
+  var ByteArray = getPointerArray(length);
+
+  // Properties
+  this.size = 0;
+  this.length = length;
+  this.dense = new ByteArray(length);
+  this.sparse = new ByteArray(length);
+  this.vals = new Values(length);
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+SparseMap.prototype.clear = function() {
+  this.size = 0;
+};
+
+/**
+ * Method used to check the existence of a member in the set.
+ *
+ * @param  {number} member - Member to test.
+ * @return {SparseMap}
+ */
+SparseMap.prototype.has = function(member) {
+  var index = this.sparse[member];
+
+  return (
+    index < this.size &&
+    this.dense[index] === member
+  );
+};
+
+/**
+ * Method used to get the value associated to a member in the set.
+ *
+ * @param  {number} member - Member to test.
+ * @return {any}
+ */
+SparseMap.prototype.get = function(member) {
+  var index = this.sparse[member];
+
+  if (index < this.size && this.dense[index] === member)
+    return this.vals[index];
+
+  return;
+};
+
+/**
+ * Method used to set a value into the map.
+ *
+ * @param  {number} member - Member to set.
+ * @param  {any}    value  - Associated value.
+ * @return {SparseMap}
+ */
+SparseMap.prototype.set = function(member, value) {
+  var index = this.sparse[member];
+
+  if (index < this.size && this.dense[index] === member) {
+    this.vals[index] = value;
+    return this;
+  }
+
+  this.dense[this.size] = member;
+  this.sparse[member] = this.size;
+  this.vals[this.size] = value;
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to remove a member from the set.
+ *
+ * @param  {number} member - Member to delete.
+ * @return {boolean}
+ */
+SparseMap.prototype.delete = function(member) {
+  var index = this.sparse[member];
+
+  if (index >= this.size || this.dense[index] !== member)
+    return false;
+
+  index = this.dense[this.size - 1];
+  this.dense[this.sparse[member]] = index;
+  this.sparse[index] = this.sparse[member];
+  this.size--;
+
+  return true;
+};
+
+/**
+ * Method used to iterate over the set's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+SparseMap.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = 0; i < this.size; i++)
+    callback.call(scope, this.vals[i], this.dense[i]);
+};
+
+/**
+ * Method used to create an iterator over a set's members.
+ *
+ * @return {Iterator}
+ */
+SparseMap.prototype.keys = function() {
+  var size = this.size,
+      dense = this.dense,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i < size) {
+      var item = dense[i];
+      i++;
+
+      return {
+        value: item
+      };
+    }
+
+    return {
+      done: true
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a set's values.
+ *
+ * @return {Iterator}
+ */
+SparseMap.prototype.values = function() {
+  var size = this.size,
+      values = this.vals,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i < size) {
+      var item = values[i];
+      i++;
+
+      return {
+        value: item
+      };
+    }
+
+    return {
+      done: true
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a set's entries.
+ *
+ * @return {Iterator}
+ */
+SparseMap.prototype.entries = function() {
+  var size = this.size,
+      dense = this.dense,
+      values = this.vals,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i < size) {
+      var item = [dense[i], values[i]];
+      i++;
+
+      return {
+        value: item
+      };
+    }
+
+    return {
+      done: true
+    };
+  });
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  SparseMap.prototype[Symbol.iterator] = SparseMap.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+SparseMap.prototype.inspect = function() {
+  var proxy = new Map();
+
+  for (var i = 0; i < this.size; i++)
+    proxy.set(this.dense[i], this.vals[i]);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: SparseMap,
+    enumerable: false
+  });
+
+  proxy.length = this.length;
+
+  if (this.vals.constructor !== Array)
+    proxy.type = this.vals.constructor.name;
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  SparseMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = SparseMap.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = SparseMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7463bf0259b1f2ac59db68554fc27f7f5bf9be2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.d.ts
@@ -0,0 +1,24 @@
+/**
+ * Mnemonist SparseQueueSet Typings
+ * =================================
+ */
+export default class SparseQueueSet implements Iterable<number> {
+
+  // Members
+  capacity: number;
+  start: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number);
+
+  // Methods
+  clear(): void;
+  has(value: number): boolean;
+  enqueue(value: number): this;
+  dequeue(): number | undefined;
+  forEach(callback: (value: number, key: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.js b/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5f42b3af09e1aa122b40f0aaf9717e52688a9f3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-queue-set.js
@@ -0,0 +1,218 @@
+/**
+ * Mnemonist SparseQueueSet
+ * =========================
+ *
+ * JavaScript sparse queue set implemented on top of byte arrays.
+ *
+ * [Reference]: https://research.swtch.com/sparse
+ */
+var Iterator = require('obliterator/iterator'),
+    getPointerArray = require('./utils/typed-arrays.js').getPointerArray;
+
+/**
+ * SparseQueueSet.
+ *
+ * @constructor
+ */
+function SparseQueueSet(capacity) {
+
+  var ByteArray = getPointerArray(capacity);
+
+  // Properties
+  this.start = 0;
+  this.size = 0;
+  this.capacity = capacity;
+  this.dense = new ByteArray(capacity);
+  this.sparse = new ByteArray(capacity);
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+SparseQueueSet.prototype.clear = function() {
+  this.start = 0;
+  this.size = 0;
+};
+
+/**
+ * Method used to check the existence of a member in the queue.
+ *
+ * @param  {number} member - Member to test.
+ * @return {SparseQueueSet}
+ */
+SparseQueueSet.prototype.has = function(member) {
+  if (this.size === 0)
+    return false;
+
+  var index = this.sparse[member];
+
+  var inBounds = (
+    index < this.capacity &&
+    (
+      index >= this.start &&
+      index < this.start + this.size
+    ) ||
+    (
+      index < ((this.start + this.size) % this.capacity)
+    )
+  );
+
+  return (
+    inBounds &&
+    this.dense[index] === member
+  );
+};
+
+/**
+ * Method used to add a member to the queue.
+ *
+ * @param  {number} member - Member to add.
+ * @return {SparseQueueSet}
+ */
+SparseQueueSet.prototype.enqueue = function(member) {
+  var index = this.sparse[member];
+
+  if (this.size !== 0) {
+    var inBounds = (
+      index < this.capacity &&
+      (
+        index >= this.start &&
+        index < this.start + this.size
+      ) ||
+      (
+        index < ((this.start + this.size) % this.capacity)
+      )
+    );
+
+    if (inBounds && this.dense[index] === member)
+      return this;
+  }
+
+  index = (this.start + this.size) % this.capacity;
+
+  this.dense[index] = member;
+  this.sparse[member] = index;
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to remove the next member from the queue.
+ *
+ * @param  {number} member - Member to delete.
+ * @return {boolean}
+ */
+SparseQueueSet.prototype.dequeue = function() {
+  if (this.size === 0)
+    return;
+
+  var index = this.start;
+
+  this.size--;
+  this.start++;
+
+  if (this.start === this.capacity)
+    this.start = 0;
+
+  var member = this.dense[index];
+
+  this.sparse[member] = this.capacity;
+
+  return member;
+};
+
+/**
+ * Method used to iterate over the queue's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+SparseQueueSet.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  while (j < l) {
+    callback.call(scope, this.dense[i], j, this);
+    i++;
+    j++;
+
+    if (i === c)
+      i = 0;
+  }
+};
+
+/**
+ * Method used to create an iterator over a set's values.
+ *
+ * @return {Iterator}
+ */
+SparseQueueSet.prototype.values = function() {
+  var dense = this.dense,
+      c = this.capacity,
+      l = this.size,
+      i = this.start,
+      j = 0;
+
+  return new Iterator(function() {
+    if (j >= l)
+      return {
+        done: true
+      };
+
+    var value = dense[i];
+
+    i++;
+    j++;
+
+    if (i === c)
+      i = 0;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  SparseQueueSet.prototype[Symbol.iterator] = SparseQueueSet.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+SparseQueueSet.prototype.inspect = function() {
+  var proxy = [];
+
+  this.forEach(function(member) {
+    proxy.push(member);
+  });
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: SparseQueueSet,
+    enumerable: false
+  });
+
+  proxy.capacity = this.capacity;
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  SparseQueueSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = SparseQueueSet.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = SparseQueueSet;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99fe655e830f28fea76003caabe61eea0b7c29bf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.d.ts
@@ -0,0 +1,23 @@
+/**
+ * Mnemonist SparseSet Typings
+ * ============================
+ */
+export default class SparseSet implements Iterable<number> {
+
+  // Members
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number);
+
+  // Methods
+  clear(): void;
+  has(value: number): boolean;
+  add(value: number): this;
+  delete(value: number): boolean;
+  forEach(callback: (value: number, key: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.js b/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..7498f33f644b965548f36eab3c90453813c8bc9d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/sparse-set.js
@@ -0,0 +1,168 @@
+/**
+ * Mnemonist SparseSet
+ * ====================
+ *
+ * JavaScript sparse set implemented on top of byte arrays.
+ *
+ * [Reference]: https://research.swtch.com/sparse
+ */
+var Iterator = require('obliterator/iterator'),
+    getPointerArray = require('./utils/typed-arrays.js').getPointerArray;
+
+/**
+ * SparseSet.
+ *
+ * @constructor
+ */
+function SparseSet(length) {
+
+  var ByteArray = getPointerArray(length);
+
+  // Properties
+  this.size = 0;
+  this.length = length;
+  this.dense = new ByteArray(length);
+  this.sparse = new ByteArray(length);
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+SparseSet.prototype.clear = function() {
+  this.size = 0;
+};
+
+/**
+ * Method used to check the existence of a member in the set.
+ *
+ * @param  {number} member - Member to test.
+ * @return {SparseSet}
+ */
+SparseSet.prototype.has = function(member) {
+  var index = this.sparse[member];
+
+  return (
+    index < this.size &&
+    this.dense[index] === member
+  );
+};
+
+/**
+ * Method used to add a member to the set.
+ *
+ * @param  {number} member - Member to add.
+ * @return {SparseSet}
+ */
+SparseSet.prototype.add = function(member) {
+  var index = this.sparse[member];
+
+  if (index < this.size && this.dense[index] === member)
+    return this;
+
+  this.dense[this.size] = member;
+  this.sparse[member] = this.size;
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to remove a member from the set.
+ *
+ * @param  {number} member - Member to delete.
+ * @return {boolean}
+ */
+SparseSet.prototype.delete = function(member) {
+  var index = this.sparse[member];
+
+  if (index >= this.size || this.dense[index] !== member)
+    return false;
+
+  index = this.dense[this.size - 1];
+  this.dense[this.sparse[member]] = index;
+  this.sparse[index] = this.sparse[member];
+  this.size--;
+
+  return true;
+};
+
+/**
+ * Method used to iterate over the set's values.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+SparseSet.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  var item;
+
+  for (var i = 0; i < this.size; i++) {
+    item = this.dense[i];
+
+    callback.call(scope, item, item);
+  }
+};
+
+/**
+ * Method used to create an iterator over a set's values.
+ *
+ * @return {Iterator}
+ */
+SparseSet.prototype.values = function() {
+  var size = this.size,
+      dense = this.dense,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i < size) {
+      var item = dense[i];
+      i++;
+
+      return {
+        value: item
+      };
+    }
+
+    return {
+      done: true
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  SparseSet.prototype[Symbol.iterator] = SparseSet.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+SparseSet.prototype.inspect = function() {
+  var proxy = new Set();
+
+  for (var i = 0; i < this.size; i++)
+    proxy.add(this.dense[i]);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: SparseSet,
+    enumerable: false
+  });
+
+  proxy.length = this.length;
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  SparseSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = SparseSet.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+module.exports = SparseSet;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/stack.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/stack.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa6998b4a06682dee1b3cbd53950d6a73169df49
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/stack.d.ts
@@ -0,0 +1,27 @@
+/**
+ * Mnemonist Stack Typings
+ * ========================
+ */
+export default class Stack<T> implements Iterable<T> {
+
+  // Members
+  size: number;
+
+  // Methods
+  clear(): void;
+  push(item: T): number;
+  pop(): T | undefined;
+  peek(): T | undefined;
+  forEach(callback: (item: T, index: number, stack: this) => void, scope?: any): void;
+  toArray(): Array<T>;
+  values(): IterableIterator<T>;
+  entries(): IterableIterator<[number, T]>;
+  [Symbol.iterator](): IterableIterator<T>;
+  toString(): string;
+  toJSON(): Array<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): Stack<I>;
+  static of<I>(...items: Array<I>): Stack<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/stack.js b/libs/shared/graph-layout/node_modules/mnemonist/stack.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e83519649449e841825840b159211b89af59a32
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/stack.js
@@ -0,0 +1,210 @@
+/**
+ * Mnemonist Stack
+ * ================
+ *
+ * Stack implementation relying on JavaScript arrays, which are fast enough &
+ * correctly optimized for this kind of work.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach');
+
+/**
+ * Stack
+ *
+ * @constructor
+ */
+function Stack() {
+  this.clear();
+}
+
+/**
+ * Method used to clear the stack.
+ *
+ * @return {undefined}
+ */
+Stack.prototype.clear = function() {
+
+  // Properties
+  this.items = [];
+  this.size = 0;
+};
+
+/**
+ * Method used to add an item to the stack.
+ *
+ * @param  {any}    item - Item to add.
+ * @return {number}
+ */
+Stack.prototype.push = function(item) {
+  this.items.push(item);
+  return ++this.size;
+};
+
+/**
+ * Method used to retrieve & remove the last item of the stack.
+ *
+ * @return {any}
+ */
+Stack.prototype.pop = function() {
+  if (this.size === 0)
+    return;
+
+  this.size--;
+  return this.items.pop();
+};
+
+/**
+ * Method used to get the last item of the stack.
+ *
+ * @return {any}
+ */
+Stack.prototype.peek = function() {
+  return this.items[this.size - 1];
+};
+
+/**
+ * Method used to iterate over the stack.
+ *
+ * @param  {function}  callback - Function to call for each item.
+ * @param  {object}    scope    - Optional scope.
+ * @return {undefined}
+ */
+Stack.prototype.forEach = function(callback, scope) {
+  scope = arguments.length > 1 ? scope : this;
+
+  for (var i = 0, l = this.items.length; i < l; i++)
+    callback.call(scope, this.items[l - i - 1], i, this);
+};
+
+/**
+ * Method used to convert the stack to a JavaScript array.
+ *
+ * @return {array}
+ */
+Stack.prototype.toArray = function() {
+  var array = new Array(this.size),
+      l = this.size - 1,
+      i = this.size;
+
+  while (i--)
+    array[i] = this.items[l - i];
+
+  return array;
+};
+
+/**
+ * Method used to create an iterator over a stack's values.
+ *
+ * @return {Iterator}
+ */
+Stack.prototype.values = function() {
+  var items = this.items,
+      l = items.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[l - i - 1];
+    i++;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a stack's entries.
+ *
+ * @return {Iterator}
+ */
+Stack.prototype.entries = function() {
+  var items = this.items,
+      l = items.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[l - i - 1];
+
+    return {
+      value: [i++, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  Stack.prototype[Symbol.iterator] = Stack.prototype.values;
+
+
+/**
+ * Convenience known methods.
+ */
+Stack.prototype.toString = function() {
+  return this.toArray().join(',');
+};
+
+Stack.prototype.toJSON = function() {
+  return this.toArray();
+};
+
+Stack.prototype.inspect = function() {
+  var array = this.toArray();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: Stack,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  Stack.prototype[Symbol.for('nodejs.util.inspect.custom')] = Stack.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a stack.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @return {Stack}
+ */
+Stack.from = function(iterable) {
+  var stack = new Stack();
+
+  forEach(iterable, function(value) {
+    stack.push(value);
+  });
+
+  return stack;
+};
+
+/**
+ * Static @.of function taking an arbitrary number of arguments & converting it
+ * into a stack.
+ *
+ * @param  {...any} args
+ * @return {Stack}
+ */
+Stack.of = function() {
+  return Stack.from(arguments);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = Stack;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3e808da810d1cd4f7a130916c6944a3e45dc4710
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.d.ts
@@ -0,0 +1,23 @@
+/**
+ * Mnemonist StaticDisjointSet Typings
+ * ====================================
+ */
+import {ArrayLike} from './utils/types';
+
+export default class StaticDisjointSet {
+  
+  // Members
+  dimension: number;
+  size: number;
+
+  // Constructor
+  constructor(size: number);
+
+  // Methods
+  find(x: number): number;
+  union(x: number, y: number): this;
+  connected(x: number, y: number): boolean;
+  mapping(): ArrayLike;
+  compile(): Array<Array<number>>;
+  inspect(): any;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.js b/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a84b93278ccab37bfdaade1b2e7f99b8ca38ad4
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/static-disjoint-set.js
@@ -0,0 +1,195 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist StaticDisjointSet
+ * ============================
+ *
+ * JavaScript implementation of a static disjoint set (union-find).
+ *
+ * Note that to remain performant, this implementation needs to know a size
+ * beforehand.
+ */
+var helpers = require('./utils/typed-arrays.js');
+
+/**
+ * StaticDisjointSet.
+ *
+ * @constructor
+ */
+function StaticDisjointSet(size) {
+
+  // Optimizing the typed array types
+  var ParentsTypedArray = helpers.getPointerArray(size),
+      RanksTypedArray = helpers.getPointerArray(Math.log2(size));
+
+  // Properties
+  this.size = size;
+  this.dimension = size;
+  this.parents = new ParentsTypedArray(size);
+  this.ranks = new RanksTypedArray(size);
+
+  // Initializing parents
+  for (var i = 0; i < size; i++)
+    this.parents[i] = i;
+}
+
+/**
+ * Method used to find the root of the given item.
+ *
+ * @param  {number} x - Target item.
+ * @return {number}
+ */
+StaticDisjointSet.prototype.find = function(x) {
+  var y = x;
+
+  var c, p;
+
+  while (true) {
+    c = this.parents[y];
+
+    if (y === c)
+      break;
+
+    y = c;
+  }
+
+  // Path compression
+  while (true) {
+    p = this.parents[x];
+
+    if (p === y)
+      break;
+
+    this.parents[x] = y;
+    x = p;
+  }
+
+  return y;
+};
+
+/**
+ * Method used to perform the union of two items.
+ *
+ * @param  {number} x - First item.
+ * @param  {number} y - Second item.
+ * @return {StaticDisjointSet}
+ */
+StaticDisjointSet.prototype.union = function(x, y) {
+  var xRoot = this.find(x),
+      yRoot = this.find(y);
+
+  // x and y are already in the same set
+  if (xRoot === yRoot)
+    return this;
+
+  this.dimension--;
+
+  // x and y are not in the same set, we merge them
+  var xRank = this.ranks[x],
+      yRank = this.ranks[y];
+
+  if (xRank < yRank) {
+    this.parents[xRoot] = yRoot;
+  }
+  else if (xRank > yRank) {
+    this.parents[yRoot] = xRoot;
+  }
+  else {
+    this.parents[yRoot] = xRoot;
+    this.ranks[xRoot]++;
+  }
+
+  return this;
+};
+
+/**
+ * Method returning whether two items are connected.
+ *
+ * @param  {number} x - First item.
+ * @param  {number} y - Second item.
+ * @return {boolean}
+ */
+StaticDisjointSet.prototype.connected = function(x, y) {
+  var xRoot = this.find(x);
+
+  return xRoot === this.find(y);
+};
+
+/**
+ * Method returning the set mapping.
+ *
+ * @return {TypedArray}
+ */
+StaticDisjointSet.prototype.mapping = function() {
+  var MappingClass = helpers.getPointerArray(this.dimension);
+
+  var ids = {},
+      mapping = new MappingClass(this.size),
+      c = 0;
+
+  var r;
+
+  for (var i = 0, l = this.parents.length; i < l; i++) {
+    r = this.find(i);
+
+    if (typeof ids[r] === 'undefined') {
+      mapping[i] = c;
+      ids[r] = c++;
+    }
+    else {
+      mapping[i] = ids[r];
+    }
+  }
+
+  return mapping;
+};
+
+/**
+ * Method used to compile the disjoint set into an array of arrays.
+ *
+ * @return {array}
+ */
+StaticDisjointSet.prototype.compile = function() {
+  var ids = {},
+      result = new Array(this.dimension),
+      c = 0;
+
+  var r;
+
+  for (var i = 0, l = this.parents.length; i < l; i++) {
+    r = this.find(i);
+
+    if (typeof ids[r] === 'undefined') {
+      result[c] = [i];
+      ids[r] = c++;
+    }
+    else {
+      result[ids[r]].push(i);
+    }
+  }
+
+  return result;
+};
+
+/**
+ * Convenience known methods.
+ */
+StaticDisjointSet.prototype.inspect = function() {
+  var array = this.compile();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: StaticDisjointSet,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  StaticDisjointSet.prototype[Symbol.for('nodejs.util.inspect.custom')] = StaticDisjointSet.prototype.inspect;
+
+
+/**
+ * Exporting.
+ */
+module.exports = StaticDisjointSet;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5302f1edc2bc8713f8edc46f77aa8e471c65f3f9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.d.ts
@@ -0,0 +1,24 @@
+/**
+ * Mnemonist StaticIntervalTree Typings
+ * =====================================
+ */
+type StaticIntervalTreeGetter<T> = (item: T) => number;
+type StaticIntervalTreeGettersTuple<T> = [StaticIntervalTreeGetter<T>, StaticIntervalTreeGetter<T>];
+
+export default class StaticIntervalTree<T> {
+
+  // Members
+  height: number;
+  size: number;
+
+  // Constructor
+  constructor(intervals: Array<T>, getters?: StaticIntervalTreeGettersTuple<T>);
+
+  // Methods
+  intervalsContainingPoint(point: number): Array<T>;
+  intervalsOverlappingInterval(interval: T): Array<T>;
+  inspect(): any;
+  
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}): StaticIntervalTree<I>;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.js b/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..41452f815d4579f114cb0cdd74ffeea688998974
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/static-interval-tree.js
@@ -0,0 +1,387 @@
+/*
+ * Mnemonist StaticIntervalTree
+ * =============================
+ *
+ * JavaScript implementation of a static interval tree. This tree is static in
+ * that you are required to know all its items beforehand and to built it
+ * from an iterable.
+ *
+ * This implementation represents the interval tree as an augmented balanced
+ * binary search tree. It works by sorting the intervals by startpoint first
+ * then proceeds building the augmented balanced BST bottom-up from the
+ * sorted list.
+ *
+ * Note that this implementation considers every given intervals as closed for
+ * simplicity's sake.
+ *
+ * For more information: https://en.wikipedia.org/wiki/Interval_tree
+ */
+var iterables = require('./utils/iterables.js'),
+    typed = require('./utils/typed-arrays.js');
+
+var FixedStack = require('./fixed-stack.js');
+
+
+// TODO: pass index to getters
+// TODO: custom comparison
+// TODO: possibility to pass offset buffer
+
+// TODO: intervals() => Symbol.iterator
+// TODO: dfs()
+
+/**
+ * Helpers.
+ */
+
+/**
+ * Recursive function building the BST from the sorted list of interval
+ * indices.
+ *
+ * @param  {array}    intervals     - Array of intervals to index.
+ * @param  {function} endGetter     - Getter function for end of intervals.
+ * @param  {array}    sortedIndices - Sorted indices of the intervals.
+ * @param  {array}    tree          - BST memory.
+ * @param  {array}    augmentations - Array of node augmentations.
+ * @param  {number}   i             - BST index of current node.
+ * @param  {number}   low           - Dichotomy low index.
+ * @param  {number}   high          - Dichotomy high index.
+ * @return {number}                 - Created node augmentation value.
+ */
+function buildBST(
+  intervals,
+  endGetter,
+  sortedIndices,
+  tree,
+  augmentations,
+  i,
+  low,
+  high
+) {
+  var mid = (low + (high - low) / 2) | 0,
+      midMinusOne = ~-mid,
+      midPlusOne = -~mid;
+
+  var current = sortedIndices[mid];
+  tree[i] = current + 1;
+
+  var end = endGetter ? endGetter(intervals[current]) : intervals[current][1];
+
+  var left = i * 2 + 1,
+      right = i * 2 + 2;
+
+  var leftEnd = -Infinity,
+      rightEnd = -Infinity;
+
+  if (low <= midMinusOne) {
+    leftEnd = buildBST(
+      intervals,
+      endGetter,
+      sortedIndices,
+      tree,
+      augmentations,
+      left,
+      low,
+      midMinusOne
+    );
+  }
+
+  if (midPlusOne <= high) {
+    rightEnd = buildBST(
+      intervals,
+      endGetter,
+      sortedIndices,
+      tree,
+      augmentations,
+      right,
+      midPlusOne,
+      high
+    );
+  }
+
+  var augmentation = Math.max(end, leftEnd, rightEnd);
+
+  var augmentationPointer = current;
+
+  if (augmentation === leftEnd)
+    augmentationPointer = augmentations[tree[left] - 1];
+  else if (augmentation === rightEnd)
+    augmentationPointer = augmentations[tree[right] - 1];
+
+  augmentations[current] = augmentationPointer;
+
+  return augmentation;
+}
+
+/**
+ * StaticIntervalTree.
+ *
+ * @constructor
+ * @param {array}           intervals - Array of intervals to index.
+ * @param {array<function>} getters   - Optional getters.
+ */
+function StaticIntervalTree(intervals, getters) {
+
+  // Properties
+  this.size = intervals.length;
+  this.intervals = intervals;
+
+  var startGetter = null,
+      endGetter = null;
+
+  if (Array.isArray(getters)) {
+    startGetter = getters[0];
+    endGetter = getters[1];
+  }
+
+  // Building the indices array
+  var length = intervals.length;
+
+  var IndicesArray = typed.getPointerArray(length + 1);
+
+  var indices = new IndicesArray(length);
+
+  var i;
+
+  for (i = 1; i < length; i++)
+    indices[i] = i;
+
+  // Sorting indices array
+  // TODO: check if some version of radix sort can outperform this part
+  indices.sort(function(a, b) {
+    a = intervals[a];
+    b = intervals[b];
+
+    if (startGetter) {
+      a = startGetter(a);
+      b = startGetter(b);
+    }
+    else {
+      a = a[0];
+      b = b[0];
+    }
+
+    if (a < b)
+      return -1;
+
+    if (a > b)
+      return 1;
+
+    // TODO: use getters
+    // TODO: this ordering has the following invariant: if query interval
+    // contains [nodeStart, max], then whole right subtree can be collected
+    // a = a[1];
+    // b = b[1];
+
+    // if (a < b)
+    //   return 1;
+
+    // if (a > b)
+    //   return -1;
+
+    return 0;
+  });
+
+  // Building the binary tree
+  var height = Math.ceil(Math.log2(length + 1)),
+      treeSize = Math.pow(2, height) - 1;
+
+  var tree = new IndicesArray(treeSize);
+
+  var augmentations = new IndicesArray(length);
+
+  buildBST(
+    intervals,
+    endGetter,
+    indices,
+    tree,
+    augmentations,
+    0,
+    0,
+    length - 1
+  );
+
+  // Dropping indices
+  indices = null;
+
+  // Storing necessary information
+  this.height = height;
+  this.tree = tree;
+  this.augmentations = augmentations;
+  this.startGetter = startGetter;
+  this.endGetter = endGetter;
+
+  // Initializing DFS stack
+  this.stack = new FixedStack(IndicesArray, this.height);
+}
+
+/**
+ * Method returning a list of intervals containing the given point.
+ *
+ * @param  {any}   point - Target point.
+ * @return {array}
+ */
+StaticIntervalTree.prototype.intervalsContainingPoint = function(point) {
+  var matches = [];
+
+  var stack = this.stack;
+
+  stack.clear();
+  stack.push(0);
+
+  var l = this.tree.length;
+
+  var bstIndex,
+      intervalIndex,
+      interval,
+      maxInterval,
+      start,
+      end,
+      max,
+      left,
+      right;
+
+  while (stack.size) {
+    bstIndex = stack.pop();
+    intervalIndex = this.tree[bstIndex] - 1;
+    interval = this.intervals[intervalIndex];
+    maxInterval = this.intervals[this.augmentations[intervalIndex]];
+
+    max = this.endGetter ? this.endGetter(maxInterval) : maxInterval[1];
+
+    // No possible match, point is farther right than the max end value
+    if (point > max)
+      continue;
+
+    // Searching left
+    left = bstIndex * 2 + 1;
+
+    if (left < l && this.tree[left] !== 0)
+      stack.push(left);
+
+    start = this.startGetter ? this.startGetter(interval) : interval[0];
+    end = this.endGetter ? this.endGetter(interval) : interval[1];
+
+    // Checking current node
+    if (point >= start && point <= end)
+      matches.push(interval);
+
+    // If the point is to the left of the start of the current interval,
+    // then it cannot be in the right child
+    if (point < start)
+      continue;
+
+    // Searching right
+    right = bstIndex * 2 + 2;
+
+    if (right < l && this.tree[right] !== 0)
+      stack.push(right);
+  }
+
+  return matches;
+};
+
+/**
+ * Method returning a list of intervals overlapping the given interval.
+ *
+ * @param  {any}   interval - Target interval.
+ * @return {array}
+ */
+StaticIntervalTree.prototype.intervalsOverlappingInterval = function(interval) {
+  var intervalStart = this.startGetter ? this.startGetter(interval) : interval[0],
+      intervalEnd = this.endGetter ? this.endGetter(interval) : interval[1];
+
+  var matches = [];
+
+  var stack = this.stack;
+
+  stack.clear();
+  stack.push(0);
+
+  var l = this.tree.length;
+
+  var bstIndex,
+      intervalIndex,
+      currentInterval,
+      maxInterval,
+      start,
+      end,
+      max,
+      left,
+      right;
+
+  while (stack.size) {
+    bstIndex = stack.pop();
+    intervalIndex = this.tree[bstIndex] - 1;
+    currentInterval = this.intervals[intervalIndex];
+    maxInterval = this.intervals[this.augmentations[intervalIndex]];
+
+    max = this.endGetter ? this.endGetter(maxInterval) : maxInterval[1];
+
+    // No possible match, start is farther right than the max end value
+    if (intervalStart > max)
+      continue;
+
+    // Searching left
+    left = bstIndex * 2 + 1;
+
+    if (left < l && this.tree[left] !== 0)
+      stack.push(left);
+
+    start = this.startGetter ? this.startGetter(currentInterval) : currentInterval[0];
+    end = this.endGetter ? this.endGetter(currentInterval) : currentInterval[1];
+
+    // Checking current node
+    if (intervalEnd >= start && intervalStart <= end)
+      matches.push(currentInterval);
+
+    // If the end is to the left of the start of the current interval,
+    // then it cannot be in the right child
+    if (intervalEnd < start)
+      continue;
+
+    // Searching right
+    right = bstIndex * 2 + 2;
+
+    if (right < l && this.tree[right] !== 0)
+      stack.push(right);
+  }
+
+  return matches;
+};
+
+/**
+ * Convenience known methods.
+ */
+StaticIntervalTree.prototype.inspect = function() {
+  var proxy = this.intervals.slice();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: StaticIntervalTree,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  StaticIntervalTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = StaticIntervalTree.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {StaticIntervalTree}
+ */
+StaticIntervalTree.from = function(iterable, getters) {
+  if (iterables.isArrayLike(iterable))
+    return new StaticIntervalTree(iterable, getters);
+
+  return new StaticIntervalTree(Array.from(iterable), getters);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = StaticIntervalTree;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b959403e47e580e224af3041a417c1949acc172e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.d.ts
@@ -0,0 +1,37 @@
+/**
+ * Mnemonist SuffixArray Typings
+ * ==============================
+ */
+export default class SuffixArray {
+  
+  // Members
+  array: Array<number>;
+  length: number;
+  string: string | Array<string>;
+
+  // Constructor
+  constructor(string: string | Array<string>);
+
+  // Methods
+  toString(): string;
+  toJSON(): Array<number>;
+  inspect(): any;
+}
+
+export class GeneralizedSuffixArray {
+
+  // Members
+  array: Array<number>;
+  length: number;
+  size: number;
+  text: string | Array<string>;
+
+  // Constructor
+  constructor(strings: Array<string> | Array<Array<string>>);
+
+  // Methods
+  longestCommonSubsequence(): string | Array<string>;
+  toString(): string;
+  toJSON(): Array<number>;
+  inspect(): any;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.js b/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.js
new file mode 100644
index 0000000000000000000000000000000000000000..14990f4de215c5489888e646ffe665fd66426082
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/suffix-array.js
@@ -0,0 +1,352 @@
+/**
+ * Mnemonist Suffix Array
+ * =======================
+ *
+ * Linear time implementation of a suffix array using the recursive
+ * method by Karkkainen and Sanders.
+ *
+ * [References]:
+ * https://www.cs.helsinki.fi/u/tpkarkka/publications/jacm05-revised.pdf
+ * http://people.mpi-inf.mpg.de/~sanders/programs/suffix/
+ * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.184.442&rep=rep1&type=pdf
+ *
+ * [Article]:
+ * "Simple Linear Work Suffix Array Construction", Karkkainen and Sanders.
+ *
+ * [Note]:
+ * A paper by Simon J. Puglisi, William F. Smyth & Andrew Turpin named
+ * "The Performance of Linear Time Suffix Sorting Algorithms" seems to
+ * prove that supralinear algorithm are in fact better faring for
+ * "real" world use cases. It would be nice to check this out in JavaScript
+ * because the high level of the language could change a lot to the fact.
+ *
+ * The current code is largely inspired by the following:
+ * https://github.com/tixxit/suffixarray/blob/master/suffixarray.js
+ */
+
+/**
+ * Constants.
+ */
+var SEPARATOR = '\u0001';
+
+/**
+ * Function used to sort the triples.
+ *
+ * @param {string|array} string - Padded sequence.
+ * @param {array}        array  - Array to sort (will be mutated).
+ * @param {number}       offset - Index offset.
+ */
+function sort(string, array, offset) {
+  var l = array.length,
+      buckets = [],
+      i = l,
+      j = -1,
+      b,
+      d = 0,
+      bits;
+
+  while (i--)
+    j = Math.max(string[array[i] + offset], j);
+
+  bits = j >> 24 && 32 || j >> 16 && 24 || j >> 8 && 16 || 8;
+
+  for (; d < bits; d += 4) {
+    for (i = 16; i--;)
+      buckets[i] = [];
+    for (i = l; i--;)
+      buckets[((string[array[i] + offset]) >> d) & 15].push(array[i]);
+    for (b = 0; b < 16; b++) {
+      for (j = buckets[b].length; j--;)
+        array[++i] = buckets[b][j];
+    }
+  }
+}
+
+/**
+ * Comparison helper.
+ */
+function compare(string, lookup, m, n) {
+  return (
+    (string[m] - string[n]) ||
+    (m % 3 === 2 ?
+      (string[m + 1] - string[n + 1]) || (lookup[m + 2] - lookup[n + 2]) :
+      (lookup[m + 1] - lookup[n + 1]))
+  );
+}
+
+/**
+ * Recursive function used to build the suffix tree in linear time.
+ *
+ * @param  {string|array} string - Padded sequence.
+ * @param  {number}       l      - True length of sequence (unpadded).
+ * @return {array}
+ */
+function build(string, l) {
+  var a = [],
+      b = [],
+      al = (2 * l / 3) | 0,
+      bl = l - al,
+      r = (al + 1) >> 1,
+      i = al,
+      j = 0,
+      k,
+      lookup = [],
+      result = [];
+
+  if (l === 1)
+    return [0];
+
+  while (i--)
+    a[i] = ((i * 3) >> 1) + 1;
+
+  for (i = 3; i--;)
+    sort(string, a, i);
+
+  j = b[((a[0] / 3) | 0) + (a[0] % 3 === 1 ? 0 : r)] = 1;
+
+  for (i = 1; i < al; i++) {
+    if (string[a[i]] !== string[a[i - 1]] ||
+        string[a[i] + 1] !== string[a[i - 1] + 1] ||
+        string[a[i] + 2] !== string[a[i - 1] + 2])
+      j++;
+
+    b[((a[i] / 3) | 0) + (a[i] % 3 === 1 ? 0 : r)] = j;
+  }
+
+  if (j < al) {
+    b = build(b, al);
+
+    for (i = al; i--;)
+      a[i] = b[i] < r ? b[i] * 3 + 1 : ((b[i] - r) * 3 + 2);
+  }
+
+  for (i = al; i--;)
+    lookup[a[i]] = i;
+  lookup[l] = -1;
+  lookup[l + 1] = -2;
+
+  b = l % 3 === 1 ? [l - 1] : [];
+
+  for (i = 0; i < al; i++) {
+    if (a[i] % 3 === 1)
+      b.push(a[i] - 1);
+  }
+
+  sort(string, b, 0);
+
+  for (i = 0, j = 0, k = 0; i < al && j < bl;)
+    result[k++] = (
+      compare(string, lookup, a[i], b[j]) < 0 ?
+        a[i++] :
+        b[j++]
+    );
+
+  while (i < al)
+    result[k++] = a[i++];
+
+  while (j < bl)
+    result[k++] = b[j++];
+
+  return result;
+}
+
+/**
+ * Function used to create the array we are going to work on.
+ *
+ * @param  {string|array} target - Target sequence.
+ * @return {array}
+ */
+function convert(target) {
+
+  // Creating the alphabet array
+  var length = target.length,
+      paddingOffset = length % 3,
+      array = new Array(length + paddingOffset),
+      l,
+      i;
+
+  // If we have an arbitrary sequence, we need to transform it
+  if (typeof target !== 'string') {
+    var uniqueTokens = Object.create(null);
+
+    for (i = 0; i < length; i++) {
+      if (!uniqueTokens[target[i]])
+        uniqueTokens[target[i]] = true;
+    }
+
+    var alphabet = Object.create(null),
+        sortedUniqueTokens = Object.keys(uniqueTokens).sort();
+
+    for (i = 0, l = sortedUniqueTokens.length; i < l; i++)
+      alphabet[sortedUniqueTokens[i]] = i + 1;
+
+    for (i = 0; i < length; i++) {
+      array[i] = alphabet[target[i]];
+    }
+  }
+  else {
+    for (i = 0; i < length; i++)
+      array[i] = target.charCodeAt(i);
+  }
+
+  // Padding the array
+  for (; i < paddingOffset; i++)
+    array[i] = 0;
+
+  return array;
+}
+
+/**
+ * Suffix Array.
+ *
+ * @constructor
+ * @param {string|array} string - Sequence for which to build the suffix array.
+ */
+function SuffixArray(string) {
+
+  // Properties
+  this.hasArbitrarySequence = typeof string !== 'string';
+  this.string = string;
+  this.length = string.length;
+
+  // Building the array
+  this.array = build(convert(string), this.length);
+}
+
+/**
+ * Convenience known methods.
+ */
+SuffixArray.prototype.toString = function() {
+  return this.array.join(',');
+};
+
+SuffixArray.prototype.toJSON = function() {
+  return this.array;
+};
+
+SuffixArray.prototype.inspect = function() {
+  var array = new Array(this.length);
+
+  for (var i = 0; i < this.length; i++)
+    array[i] = this.string.slice(this.array[i]);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: SuffixArray,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  SuffixArray.prototype[Symbol.for('nodejs.util.inspect.custom')] = SuffixArray.prototype.inspect;
+
+/**
+ * Generalized Suffix Array.
+ *
+ * @constructor
+ */
+function GeneralizedSuffixArray(strings) {
+
+  // Properties
+  this.hasArbitrarySequence = typeof strings[0] !== 'string';
+  this.size = strings.length;
+
+  if (this.hasArbitrarySequence) {
+    this.text = [];
+
+    for (var i = 0, l = this.size; i < l; i++) {
+      this.text.push.apply(this.text, strings[i]);
+
+      if (i < l - 1)
+        this.text.push(SEPARATOR);
+    }
+  }
+  else {
+    this.text = strings.join(SEPARATOR);
+  }
+
+  this.firstLength = strings[0].length;
+  this.length = this.text.length;
+
+  // Building the array
+  this.array = build(convert(this.text), this.length);
+}
+
+/**
+ * Method used to retrieve the longest common subsequence of the generalized
+ * suffix array.
+ *
+ * @return {string|array}
+ */
+GeneralizedSuffixArray.prototype.longestCommonSubsequence = function() {
+  var lcs = this.hasArbitrarySequence ? [] : '',
+      lcp,
+      i,
+      j,
+      s,
+      t;
+
+  for (i = 1; i < this.length; i++) {
+    s = this.array[i];
+    t = this.array[i - 1];
+
+    if (s < this.firstLength &&
+        t < this.firstLength)
+      continue;
+
+    if (s > this.firstLength &&
+        t > this.firstLength)
+      continue;
+
+    lcp = Math.min(this.length - s, this.length - t);
+
+    for (j = 0; j < lcp; j++) {
+      if (this.text[s + j] !== this.text[t + j]) {
+        lcp = j;
+        break;
+      }
+    }
+
+    if (lcp > lcs.length)
+      lcs = this.text.slice(s, s + lcp);
+  }
+
+  return lcs;
+};
+
+/**
+ * Convenience known methods.
+ */
+GeneralizedSuffixArray.prototype.toString = function() {
+  return this.array.join(',');
+};
+
+GeneralizedSuffixArray.prototype.toJSON = function() {
+  return this.array;
+};
+
+GeneralizedSuffixArray.prototype.inspect = function() {
+  var array = new Array(this.length);
+
+  for (var i = 0; i < this.length; i++)
+    array[i] = this.text.slice(this.array[i]);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: GeneralizedSuffixArray,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  GeneralizedSuffixArray.prototype[Symbol.for('nodejs.util.inspect.custom')] = GeneralizedSuffixArray.prototype.inspect;
+
+/**
+ * Exporting.
+ */
+SuffixArray.GeneralizedSuffixArray = GeneralizedSuffixArray;
+module.exports = SuffixArray;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/symspell.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/symspell.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0e926d0dd2d1a48f4a47fdd24a7004dd8faf17ba
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/symspell.d.ts
@@ -0,0 +1,33 @@
+/**
+ * Mnemonist SymSpell Typings
+ * ===========================
+ */
+type SymSpellVerbosity = 0 | 1 | 2;
+
+type SymSpellOptions = {
+  maxDistance?: number;
+  verbosity?: SymSpellVerbosity
+};
+
+type SymSpellMatch = {
+  term: string;
+  distance: number;
+  count: number;
+}
+
+export default class SymSpell {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(options?: SymSpellOptions);
+
+  // Methods
+  clear(): void;
+  add(string: string): this;
+  search(query: string): Array<SymSpellMatch>;
+
+  // Statics
+  static from(strings: Iterable<string> | {[key: string]: string}, options?: SymSpellOptions): SymSpell;
+} 
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/symspell.js b/libs/shared/graph-layout/node_modules/mnemonist/symspell.js
new file mode 100644
index 0000000000000000000000000000000000000000..365ee430b8a70a80cba6c8da4ba1b71437253116
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/symspell.js
@@ -0,0 +1,547 @@
+/* eslint no-loop-func: 0 */
+/**
+ * Mnemonist SymSpell
+ * ===================
+ *
+ * JavaScript implementation of the Symmetric Delete Spelling dictionary to
+ * efficiently index & query expression based on edit distance.
+ * Note that the current implementation target the v3.0 of the algorithm.
+ *
+ * [Reference]:
+ * http://blog.faroo.com/2012/06/07/improved-edit-distance-based-spelling-correction/
+ * https://github.com/wolfgarbe/symspell
+ *
+ * [Author]:
+ * Wolf Garbe
+ */
+var forEach = require('obliterator/foreach');
+
+/**
+ * Constants.
+ */
+var DEFAULT_MAX_DISTANCE = 2,
+    DEFAULT_VERBOSITY = 2;
+
+var VERBOSITY = new Set([
+  // Returns only the top suggestion
+  0,
+  // Returns suggestions with the smallest edit distance
+  1,
+  // Returns every suggestion (no early termination)
+  2
+]);
+
+var VERBOSITY_EXPLANATIONS = {
+  0: 'Returns only the top suggestion',
+  1: 'Returns suggestions with the smallest edit distance',
+  2: 'Returns every suggestion (no early termination)'
+};
+
+/**
+ * Functions.
+ */
+
+/**
+ * Function creating a dictionary item.
+ *
+ * @param  {number} [value] - An optional suggestion.
+ * @return {object}         - The created item.
+ */
+function createDictionaryItem(value) {
+  var suggestions = new Set();
+
+  if (typeof value === 'number')
+    suggestions.add(value);
+
+  return {
+    suggestions,
+    count: 0
+  };
+}
+
+/**
+ * Function creating a suggestion item.
+ *
+ * @return {object} - The created item.
+ */
+function createSuggestionItem(term, distance, count) {
+  return {
+    term: term || '',
+    distance: distance || 0,
+    count: count || 0
+  };
+}
+
+/**
+ * Simplified edit function.
+ *
+ * @param {string} word      - Target word.
+ * @param {number} distance  - Distance.
+ * @param {number} max       - Max distance.
+ * @param {Set}    [deletes] - Set mutated to store deletes.
+ */
+function edits(word, distance, max, deletes) {
+  deletes = deletes || new Set();
+  distance++;
+
+  var deletedItem,
+      l = word.length,
+      i;
+
+  if (l > 1) {
+    for (i = 0; i < l; i++) {
+      deletedItem = word.substring(0, i) + word.substring(i + 1);
+
+      if (!deletes.has(deletedItem)) {
+        deletes.add(deletedItem);
+
+        if (distance < max)
+          edits(deletedItem, distance, max, deletes);
+      }
+    }
+  }
+
+  return deletes;
+}
+
+/**
+ * Function used to conditionally add suggestions.
+ *
+ * @param {array}  words       - Words list.
+ * @param {number} verbosity   - Verbosity level.
+ * @param {object} item        - The target item.
+ * @param {string} suggestion  - The target suggestion.
+ * @param {number} int         - Integer key of the word.
+ * @param {object} deletedItem - Considered deleted item.
+ * @param {SymSpell}
+ */
+function addLowestDistance(words, verbosity, item, suggestion, int, deletedItem) {
+  var first = item.suggestions.values().next().value;
+
+  if (verbosity < 2 &&
+      item.suggestions.size > 0 &&
+      words[first].length - deletedItem.length > suggestion.length - deletedItem.length) {
+    item.suggestions = new Set();
+    item.count = 0;
+  }
+
+  if (verbosity === 2 ||
+      !item.suggestions.size ||
+      words[first].length - deletedItem.length >= suggestion.length - deletedItem.length) {
+    item.suggestions.add(int);
+  }
+}
+
+/**
+ * Custom Damerau-Levenshtein used by the algorithm.
+ *
+ * @param  {string} source - First string.
+ * @param  {string} target - Second string.
+ * @return {number}        - The distance.
+ */
+function damerauLevenshtein(source, target) {
+  var m = source.length,
+      n = target.length,
+      H = [[]],
+      INF = m + n,
+      sd = new Map(),
+      i,
+      l,
+      j;
+
+  H[0][0] = INF;
+
+  for (i = 0; i <= m; i++) {
+    if (!H[i + 1])
+      H[i + 1] = [];
+    H[i + 1][1] = i;
+    H[i + 1][0] = INF;
+  }
+
+  for (j = 0; j <= n; j++) {
+    H[1][j + 1] = j;
+    H[0][j + 1] = INF;
+  }
+
+  var st = source + target,
+      letter;
+
+  for (i = 0, l = st.length; i < l; i++) {
+    letter = st[i];
+
+    if (!sd.has(letter))
+      sd.set(letter, 0);
+  }
+
+  // Iterating
+  for (i = 1; i <= m; i++) {
+    var DB = 0;
+
+    for (j = 1; j <= n; j++) {
+      var i1 = sd.get(target[j - 1]),
+          j1 = DB;
+
+      if (source[i - 1] === target[j - 1]) {
+        H[i + 1][j + 1] = H[i][j];
+        DB = j;
+      }
+      else {
+        H[i + 1][j + 1] = Math.min(
+          H[i][j],
+          H[i + 1][j],
+          H[i][j + 1]
+        ) + 1;
+      }
+
+      H[i + 1][j + 1] = Math.min(
+        H[i + 1][j + 1],
+        H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1)
+      );
+    }
+
+    sd.set(source[i - 1], i);
+  }
+
+  return H[m + 1][n + 1];
+}
+
+/**
+ * Lookup function.
+ *
+ * @param  {object} dictionary  - A SymSpell dictionary.
+ * @param  {array}  words       - Unique words list.
+ * @param  {number} verbosity   - Verbosity level.
+ * @param  {number} maxDistance - Maximum distance.
+ * @param  {number} maxLength   - Maximum word length in the dictionary.
+ * @param  {string} input       - Input string.
+ * @return {array}              - The list of suggestions.
+ */
+function lookup(dictionary, words, verbosity, maxDistance, maxLength, input) {
+  var length = input.length;
+
+  if (length - maxDistance > maxLength)
+    return [];
+
+  var candidates = [input],
+      candidateSet = new Set(),
+      suggestionSet = new Set();
+
+  var suggestions = [],
+      candidate,
+      item;
+
+  // Exhausting every candidates
+  while (candidates.length > 0) {
+    candidate = candidates.shift();
+
+    // Early termination
+    if (
+      verbosity < 2 &&
+      suggestions.length > 0 &&
+      length - candidate.length > suggestions[0].distance
+    )
+      break;
+
+    item = dictionary[candidate];
+
+    if (item !== undefined) {
+      if (typeof item === 'number')
+        item = createDictionaryItem(item);
+
+      if (item.count > 0 && !suggestionSet.has(candidate)) {
+        suggestionSet.add(candidate);
+
+        var suggestItem = createSuggestionItem(
+          candidate,
+          length - candidate.length,
+          item.count
+        );
+
+        suggestions.push(suggestItem);
+
+        // Another early termination
+        if (verbosity < 2 && length - candidate.length === 0)
+          break;
+      }
+
+      // Iterating over the item's suggestions
+      item.suggestions.forEach(index => {
+        var suggestion = words[index];
+
+        // Do we already have this suggestion?
+        if (suggestionSet.has(suggestion))
+          return;
+
+        suggestionSet.add(suggestion);
+
+        // Computing distance between candidate & suggestion
+        var distance = 0;
+
+        if (input !== suggestion) {
+          if (suggestion.length === candidate.length) {
+            distance = length - candidate.length;
+          }
+          else if (length === candidate.length) {
+            distance = suggestion.length - candidate.length;
+          }
+          else {
+            var ii = 0,
+                jj = 0;
+
+            var l = suggestion.length;
+
+            while (
+              ii < l &&
+              ii < length &&
+              suggestion[ii] === input[ii]
+            ) {
+              ii++;
+            }
+
+            while (
+              jj < l - ii &&
+              jj < length &&
+              suggestion[l - jj - 1] === input[length - jj - 1]
+            ) {
+              jj++;
+            }
+
+            if (ii > 0 || jj > 0) {
+              distance = damerauLevenshtein(
+                suggestion.substr(ii, l - ii - jj),
+                input.substr(ii, length - ii - jj)
+              );
+            }
+            else {
+              distance = damerauLevenshtein(suggestion, input);
+            }
+          }
+        }
+
+        // Removing suggestions of higher distance
+        if (verbosity < 2 &&
+            suggestions.length > 0 &&
+            suggestions[0].distance > distance) {
+          suggestions = [];
+        }
+
+        if (verbosity < 2 &&
+            suggestions.length > 0 &&
+            distance > suggestions[0].distance) {
+          return;
+        }
+
+        if (distance <= maxDistance) {
+          var target = dictionary[suggestion];
+
+          if (target !== undefined) {
+            suggestions.push(createSuggestionItem(
+              suggestion,
+              distance,
+              target.count
+            ));
+          }
+        }
+      });
+    }
+
+    // Adding edits
+    if (length - candidate.length < maxDistance) {
+
+      if (verbosity < 2 &&
+          suggestions.length > 0 &&
+          length - candidate.length >= suggestions[0].distance)
+        continue;
+
+      for (var i = 0, l = candidate.length; i < l; i++) {
+        var deletedItem = (
+          candidate.substring(0, i) +
+          candidate.substring(i + 1)
+        );
+
+        if (!candidateSet.has(deletedItem)) {
+          candidateSet.add(deletedItem);
+          candidates.push(deletedItem);
+        }
+      }
+    }
+  }
+
+  if (verbosity === 0)
+    return suggestions.slice(0, 1);
+
+  return suggestions;
+}
+
+/**
+ * SymSpell.
+ *
+ * @constructor
+ */
+function SymSpell(options) {
+  options = options || {};
+
+  this.clear();
+
+  // Properties
+  this.maxDistance = typeof options.maxDistance === 'number' ?
+    options.maxDistance :
+    DEFAULT_MAX_DISTANCE;
+  this.verbosity = typeof options.verbosity === 'number' ?
+    options.verbosity :
+    DEFAULT_VERBOSITY;
+
+  // Sanity checks
+  if (typeof this.maxDistance !== 'number' || this.maxDistance <= 0)
+    throw Error('mnemonist/SymSpell.constructor: invalid `maxDistance` option. Should be a integer greater than 0.');
+
+  if (!VERBOSITY.has(this.verbosity))
+    throw Error('mnemonist/SymSpell.constructor: invalid `verbosity` option. Should be either 0, 1 or 2.');
+}
+
+/**
+ * Method used to clear the structure.
+ *
+ * @return {undefined}
+ */
+SymSpell.prototype.clear = function() {
+
+  // Properties
+  this.size = 0;
+  this.dictionary = Object.create(null);
+  this.maxLength = 0;
+  this.words = [];
+};
+
+/**
+ * Method used to add a word to the index.
+ *
+ * @param {string} word - Word to add.
+ * @param {SymSpell}
+ */
+SymSpell.prototype.add = function(word) {
+  var item = this.dictionary[word];
+
+  if (item !== undefined) {
+    if (typeof item === 'number') {
+      item = createDictionaryItem(item);
+      this.dictionary[word] = item;
+    }
+
+    item.count++;
+  }
+
+  else {
+    item = createDictionaryItem();
+    item.count++;
+
+    this.dictionary[word] = item;
+
+    if (word.length > this.maxLength)
+      this.maxLength = word.length;
+  }
+
+  if (item.count === 1) {
+    var number = this.words.length;
+    this.words.push(word);
+
+    var deletes = edits(word, 0, this.maxDistance);
+
+    deletes.forEach(deletedItem => {
+      var target = this.dictionary[deletedItem];
+
+      if (target !== undefined) {
+        if (typeof target === 'number') {
+          target = createDictionaryItem(target);
+
+          this.dictionary[deletedItem] = target;
+        }
+
+        if (!target.suggestions.has(number)) {
+          addLowestDistance(
+            this.words,
+            this.verbosity,
+            target,
+            word,
+            number,
+            deletedItem
+          );
+        }
+      }
+      else {
+        this.dictionary[deletedItem] = number;
+      }
+    });
+  }
+
+  this.size++;
+
+  return this;
+};
+
+/**
+ * Method used to search the index.
+ *
+ * @param  {string} input - Input query.
+ * @return {array}        - The found suggestions.
+ */
+SymSpell.prototype.search = function(input) {
+  return lookup(
+    this.dictionary,
+    this.words,
+    this.verbosity,
+    this.maxDistance,
+    this.maxLength,
+    input
+  );
+};
+
+/**
+ * Convenience known methods.
+ */
+SymSpell.prototype.inspect = function() {
+  var array = [];
+
+  array.size = this.size;
+  array.maxDistance = this.maxDistance;
+  array.verbosity = this.verbosity;
+  array.behavior = VERBOSITY_EXPLANATIONS[this.verbosity];
+
+  for (var k in this.dictionary) {
+    if (typeof this.dictionary[k] === 'object' && this.dictionary[k].count)
+      array.push([k, this.dictionary[k].count]);
+  }
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: SymSpell,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  SymSpell.prototype[Symbol.for('nodejs.util.inspect.custom')] = SymSpell.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a structure.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @return {SymSpell}
+ */
+SymSpell.from = function(iterable, options) {
+  var index = new SymSpell(options);
+
+  forEach(iterable, function(value) {
+    index.add(value);
+  });
+
+  return index;
+};
+
+/**
+ * Exporting.
+ */
+module.exports = SymSpell;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/trie-map.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/trie-map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0833042893b10fa43913dd12d42928ca6d26667
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/trie-map.d.ts
@@ -0,0 +1,30 @@
+/**
+ * Mnemonist TrieMap Typings
+ * ==========================
+ */
+export default class TrieMap<K, V> implements Iterable<[K, V]> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(Token?: new () => K);
+
+  // Methods
+  clear(): void;
+  set(prefix: K, value: V): this;
+  update(prefix: K, updateFunction: (oldValue: V | undefined) => V): this
+  get(prefix: K): V;
+  delete(prefix: K): boolean;
+  has(prefix: K): boolean;
+  find(prefix: K): Array<[K, V]>;
+  values(): IterableIterator<V>;
+  prefixes(): IterableIterator<K>;
+  keys(): IterableIterator<K>;
+  entries(): IterableIterator<[K, V]>;
+  [Symbol.iterator](): IterableIterator<[K, V]>;
+  inspect(): any;
+
+  // Statics
+  static from<I, J>(iterable: Iterable<[I, J]> | {[key: string]: J}): TrieMap<I, J>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/trie-map.js b/libs/shared/graph-layout/node_modules/mnemonist/trie-map.js
new file mode 100644
index 0000000000000000000000000000000000000000..d601448e2d14899732218ef8ad47ea76819d700e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/trie-map.js
@@ -0,0 +1,477 @@
+/**
+ * Mnemonist TrieMap
+ * ==================
+ *
+ * JavaScript TrieMap implementation based upon plain objects. As such this
+ * structure is more a convenience building upon the trie's advantages than
+ * a real performant alternative to already existing structures.
+ *
+ * Note that the Trie is based upon the TrieMap since the underlying machine
+ * is the very same. The Trie just does not let you set values and only
+ * considers the existence of the given prefixes.
+ */
+var forEach = require('obliterator/foreach'),
+    Iterator = require('obliterator/iterator');
+
+/**
+ * Constants.
+ */
+var SENTINEL = String.fromCharCode(0);
+
+/**
+ * TrieMap.
+ *
+ * @constructor
+ */
+function TrieMap(Token) {
+  this.mode = Token === Array ? 'array' : 'string';
+  this.clear();
+}
+
+/**
+ * Method used to clear the trie.
+ *
+ * @return {undefined}
+ */
+TrieMap.prototype.clear = function() {
+
+  // Properties
+  this.root = {};
+  this.size = 0;
+};
+
+/**
+ * Method used to set the value of the given prefix in the trie.
+ *
+ * @param  {string|array} prefix - Prefix to follow.
+ * @param  {any}          value  - Value for the prefix.
+ * @return {TrieMap}
+ */
+TrieMap.prototype.set = function(prefix, value) {
+  var node = this.root,
+      token;
+
+  for (var i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+
+    node = node[token] || (node[token] = {});
+  }
+
+  // Do we need to increase size?
+  if (!(SENTINEL in node))
+    this.size++;
+
+  node[SENTINEL] = value;
+
+  return this;
+};
+
+/**
+ * Method used to update the value of the given prefix in the trie.
+ *
+ * @param  {string|array} prefix - Prefix to follow.
+ * @param  {(oldValue: any | undefined) => any} updateFunction - Update value visitor callback.
+ * @return {TrieMap}
+ */
+TrieMap.prototype.update = function(prefix, updateFunction) {
+  var node = this.root,
+      token;
+
+  for (var i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+
+    node = node[token] || (node[token] = {});
+  }
+
+  // Do we need to increase size?
+  if (!(SENTINEL in node))
+    this.size++;
+
+  node[SENTINEL] = updateFunction(node[SENTINEL]);
+
+  return this;
+};
+
+/**
+ * Method used to return the value sitting at the end of the given prefix or
+ * undefined if none exist.
+ *
+ * @param  {string|array} prefix - Prefix to follow.
+ * @return {any|undefined}
+ */
+TrieMap.prototype.get = function(prefix) {
+  var node = this.root,
+      token,
+      i,
+      l;
+
+  for (i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+    node = node[token];
+
+    // Prefix does not exist
+    if (typeof node === 'undefined')
+      return;
+  }
+
+  if (!(SENTINEL in node))
+    return;
+
+  return node[SENTINEL];
+};
+
+/**
+ * Method used to delete a prefix from the trie.
+ *
+ * @param  {string|array} prefix - Prefix to delete.
+ * @return {boolean}
+ */
+TrieMap.prototype.delete = function(prefix) {
+  var node = this.root,
+      toPrune = null,
+      tokenToPrune = null,
+      parent,
+      token,
+      i,
+      l;
+
+  for (i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+    parent = node;
+    node = node[token];
+
+    // Prefix does not exist
+    if (typeof node === 'undefined')
+      return false;
+
+    // Keeping track of a potential branch to prune
+    if (toPrune !== null) {
+      if (Object.keys(node).length > 1) {
+        toPrune = null;
+        tokenToPrune = null;
+      }
+    }
+    else {
+      if (Object.keys(node).length < 2) {
+        toPrune = parent;
+        tokenToPrune = token;
+      }
+    }
+  }
+
+  if (!(SENTINEL in node))
+    return false;
+
+  this.size--;
+
+  if (toPrune)
+    delete toPrune[tokenToPrune];
+  else
+    delete node[SENTINEL];
+
+  return true;
+};
+
+// TODO: add #.prune?
+
+/**
+ * Method used to assert whether the given prefix exists in the TrieMap.
+ *
+ * @param  {string|array} prefix - Prefix to check.
+ * @return {boolean}
+ */
+TrieMap.prototype.has = function(prefix) {
+  var node = this.root,
+      token;
+
+  for (var i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+    node = node[token];
+
+    if (typeof node === 'undefined')
+      return false;
+  }
+
+  return SENTINEL in node;
+};
+
+/**
+ * Method used to retrieve every item in the trie with the given prefix.
+ *
+ * @param  {string|array} prefix - Prefix to query.
+ * @return {array}
+ */
+TrieMap.prototype.find = function(prefix) {
+  var isString = typeof prefix === 'string';
+
+  var node = this.root,
+      matches = [],
+      token,
+      i,
+      l;
+
+  for (i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+    node = node[token];
+
+    if (typeof node === 'undefined')
+      return matches;
+  }
+
+  // Performing DFS from prefix
+  var nodeStack = [node],
+      prefixStack = [prefix],
+      k;
+
+  while (nodeStack.length) {
+    prefix = prefixStack.pop();
+    node = nodeStack.pop();
+
+    for (k in node) {
+      if (k === SENTINEL) {
+        matches.push([prefix, node[SENTINEL]]);
+        continue;
+      }
+
+      nodeStack.push(node[k]);
+      prefixStack.push(isString ? prefix + k : prefix.concat(k));
+    }
+  }
+
+  return matches;
+};
+
+/**
+ * Method returning an iterator over the trie's values.
+ *
+ * @param  {string|array} [prefix] - Optional starting prefix.
+ * @return {Iterator}
+ */
+TrieMap.prototype.values = function(prefix) {
+  var node = this.root,
+      nodeStack = [],
+      token,
+      i,
+      l;
+
+  // Resolving initial prefix
+  if (prefix) {
+    for (i = 0, l = prefix.length; i < l; i++) {
+      token = prefix[i];
+      node = node[token];
+
+      // If the prefix does not exist, we return an empty iterator
+      if (typeof node === 'undefined')
+        return Iterator.empty();
+    }
+  }
+
+  nodeStack.push(node);
+
+  return new Iterator(function() {
+    var currentNode,
+        hasValue = false,
+        k;
+
+    while (nodeStack.length) {
+      currentNode = nodeStack.pop();
+
+      for (k in currentNode) {
+        if (k === SENTINEL) {
+          hasValue = true;
+          continue;
+        }
+
+        nodeStack.push(currentNode[k]);
+      }
+
+      if (hasValue)
+        return {done: false, value: currentNode[SENTINEL]};
+    }
+
+    return {done: true};
+  });
+};
+
+/**
+ * Method returning an iterator over the trie's prefixes.
+ *
+ * @param  {string|array} [prefix] - Optional starting prefix.
+ * @return {Iterator}
+ */
+TrieMap.prototype.prefixes = function(prefix) {
+  var node = this.root,
+      nodeStack = [],
+      prefixStack = [],
+      token,
+      i,
+      l;
+
+  var isString = this.mode === 'string';
+
+  // Resolving initial prefix
+  if (prefix) {
+    for (i = 0, l = prefix.length; i < l; i++) {
+      token = prefix[i];
+      node = node[token];
+
+      // If the prefix does not exist, we return an empty iterator
+      if (typeof node === 'undefined')
+        return Iterator.empty();
+    }
+  }
+  else {
+    prefix = isString ? '' : [];
+  }
+
+  nodeStack.push(node);
+  prefixStack.push(prefix);
+
+  return new Iterator(function() {
+    var currentNode,
+        currentPrefix,
+        hasValue = false,
+        k;
+
+    while (nodeStack.length) {
+      currentNode = nodeStack.pop();
+      currentPrefix = prefixStack.pop();
+
+      for (k in currentNode) {
+        if (k === SENTINEL) {
+          hasValue = true;
+          continue;
+        }
+
+        nodeStack.push(currentNode[k]);
+        prefixStack.push(isString ? currentPrefix + k : currentPrefix.concat(k));
+      }
+
+      if (hasValue)
+        return {done: false, value: currentPrefix};
+    }
+
+    return {done: true};
+  });
+};
+TrieMap.prototype.keys = TrieMap.prototype.prefixes;
+
+/**
+ * Method returning an iterator over the trie's entries.
+ *
+ * @param  {string|array} [prefix] - Optional starting prefix.
+ * @return {Iterator}
+ */
+TrieMap.prototype.entries = function(prefix) {
+  var node = this.root,
+      nodeStack = [],
+      prefixStack = [],
+      token,
+      i,
+      l;
+
+  var isString = this.mode === 'string';
+
+  // Resolving initial prefix
+  if (prefix) {
+    for (i = 0, l = prefix.length; i < l; i++) {
+      token = prefix[i];
+      node = node[token];
+
+      // If the prefix does not exist, we return an empty iterator
+      if (typeof node === 'undefined')
+        return Iterator.empty();
+    }
+  }
+  else {
+    prefix = isString ? '' : [];
+  }
+
+  nodeStack.push(node);
+  prefixStack.push(prefix);
+
+  return new Iterator(function() {
+    var currentNode,
+        currentPrefix,
+        hasValue = false,
+        k;
+
+    while (nodeStack.length) {
+      currentNode = nodeStack.pop();
+      currentPrefix = prefixStack.pop();
+
+      for (k in currentNode) {
+        if (k === SENTINEL) {
+          hasValue = true;
+          continue;
+        }
+
+        nodeStack.push(currentNode[k]);
+        prefixStack.push(isString ? currentPrefix + k : currentPrefix.concat(k));
+      }
+
+      if (hasValue)
+        return {done: false, value: [currentPrefix, currentNode[SENTINEL]]};
+    }
+
+    return {done: true};
+  });
+};
+
+/**
+ * Attaching the #.entries method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  TrieMap.prototype[Symbol.iterator] = TrieMap.prototype.entries;
+
+/**
+ * Convenience known methods.
+ */
+TrieMap.prototype.inspect = function() {
+  var proxy = new Array(this.size);
+
+  var iterator = this.entries(),
+      step,
+      i = 0;
+
+  while ((step = iterator.next(), !step.done))
+    proxy[i++] = step.value;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: TrieMap,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  TrieMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = TrieMap.prototype.inspect;
+
+TrieMap.prototype.toJSON = function() {
+  return this.root;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a trie.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @return {TrieMap}
+ */
+TrieMap.from = function(iterable) {
+  var trie = new TrieMap();
+
+  forEach(iterable, function(value, key) {
+    trie.set(key, value);
+  });
+
+  return trie;
+};
+
+/**
+ * Exporting.
+ */
+TrieMap.SENTINEL = SENTINEL;
+module.exports = TrieMap;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/trie.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/trie.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4b2a202e5efd705ae8d95d96ee6217b115c065f6
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/trie.d.ts
@@ -0,0 +1,26 @@
+/**
+ * Mnemonist Trie Typings
+ * =======================
+ */
+export default class Trie<T> implements Iterable<T> {
+
+  // Members
+  size: number;
+
+  // Constructor
+  constructor(Token?: new () => T);
+
+  // Methods
+  clear(): void;
+  add(prefix: T): this;
+  delete(prefix: T): boolean;
+  has(prefix: T): boolean;
+  find(prefix: T): Array<T>;
+  prefixes(): IterableIterator<T>;
+  keys(): IterableIterator<T>;
+  [Symbol.iterator](): IterableIterator<T>;
+  inspect(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string]: I}): Trie<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/trie.js b/libs/shared/graph-layout/node_modules/mnemonist/trie.js
new file mode 100644
index 0000000000000000000000000000000000000000..9562aef7a25f3b308faeb7682ba71e844b03cffa
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/trie.js
@@ -0,0 +1,167 @@
+/**
+ * Mnemonist Trie
+ * ===============
+ *
+ * JavaScript Trie implementation based upon plain objects. As such this
+ * structure is more a convenience building upon the trie's advantages than
+ * a real performant alternative to already existing structures.
+ *
+ * Note that the Trie is based upon the TrieMap since the underlying machine
+ * is the very same. The Trie just does not let you set values and only
+ * considers the existence of the given prefixes.
+ */
+var forEach = require('obliterator/foreach'),
+    TrieMap = require('./trie-map.js');
+
+/**
+ * Constants.
+ */
+var SENTINEL = String.fromCharCode(0);
+
+/**
+ * Trie.
+ *
+ * @constructor
+ */
+function Trie(Token) {
+  this.mode = Token === Array ? 'array' : 'string';
+  this.clear();
+}
+
+// Re-using TrieMap's prototype
+for (var methodName in TrieMap.prototype)
+  Trie.prototype[methodName] = TrieMap.prototype[methodName];
+
+// Dropping irrelevant methods
+delete Trie.prototype.set;
+delete Trie.prototype.get;
+delete Trie.prototype.values;
+delete Trie.prototype.entries;
+
+/**
+ * Method used to add the given prefix to the trie.
+ *
+ * @param  {string|array} prefix - Prefix to follow.
+ * @return {TrieMap}
+ */
+Trie.prototype.add = function(prefix) {
+  var node = this.root,
+      token;
+
+  for (var i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+
+    node = node[token] || (node[token] = {});
+  }
+
+  // Do we need to increase size?
+  if (!(SENTINEL in node))
+    this.size++;
+
+  node[SENTINEL] = true;
+
+  return this;
+};
+
+/**
+ * Method used to retrieve every item in the trie with the given prefix.
+ *
+ * @param  {string|array} prefix - Prefix to query.
+ * @return {array}
+ */
+Trie.prototype.find = function(prefix) {
+  var isString = typeof prefix === 'string';
+
+  var node = this.root,
+      matches = [],
+      token,
+      i,
+      l;
+
+  for (i = 0, l = prefix.length; i < l; i++) {
+    token = prefix[i];
+    node = node[token];
+
+    if (typeof node === 'undefined')
+      return matches;
+  }
+
+  // Performing DFS from prefix
+  var nodeStack = [node],
+      prefixStack = [prefix],
+      k;
+
+  while (nodeStack.length) {
+    prefix = prefixStack.pop();
+    node = nodeStack.pop();
+
+    for (k in node) {
+      if (k === SENTINEL) {
+        matches.push(prefix);
+        continue;
+      }
+
+      nodeStack.push(node[k]);
+      prefixStack.push(isString ? prefix + k : prefix.concat(k));
+    }
+  }
+
+  return matches;
+};
+
+/**
+ * Attaching the #.keys method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  Trie.prototype[Symbol.iterator] = Trie.prototype.keys;
+
+/**
+ * Convenience known methods.
+ */
+Trie.prototype.inspect = function() {
+  var proxy = new Set();
+
+  var iterator = this.keys(),
+      step;
+
+  while ((step = iterator.next(), !step.done))
+    proxy.add(step.value);
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: Trie,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  Trie.prototype[Symbol.for('nodejs.util.inspect.custom')] = Trie.prototype.inspect;
+
+Trie.prototype.toJSON = function() {
+  return this.root;
+};
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a trie.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @return {Trie}
+ */
+Trie.from = function(iterable) {
+  var trie = new Trie();
+
+  forEach(iterable, function(value) {
+    trie.add(value);
+  });
+
+  return trie;
+};
+
+/**
+ * Exporting.
+ */
+Trie.SENTINEL = SENTINEL;
+module.exports = Trie;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/binary-search.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/binary-search.js
new file mode 100644
index 0000000000000000000000000000000000000000..0666c82f19bbcc2ef0ef3de13081aa8fb4a64238
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/binary-search.js
@@ -0,0 +1,216 @@
+/**
+ * Mnemonist Binary Search Helpers
+ * ================================
+ *
+ * Typical binary search functions.
+ */
+
+/**
+ * Function returning the index of the search value in the array or `-1` if
+ * not found.
+ *
+ * @param  {array} array - Haystack.
+ * @param  {any}   value - Needle.
+ * @return {number}
+ */
+exports.search = function(array, value, lo, hi) {
+  var mid = 0;
+
+  lo = typeof lo !== 'undefined' ? lo : 0;
+  hi = typeof hi !== 'undefined' ? hi : array.length;
+
+  hi--;
+
+  var current;
+
+  while (lo <= hi) {
+    mid = (lo + hi) >>> 1;
+
+    current = array[mid];
+
+    if (current > value) {
+      hi = ~-mid;
+    }
+    else if (current < value) {
+      lo = -~mid;
+    }
+    else {
+      return mid;
+    }
+  }
+
+  return -1;
+};
+
+/**
+ * Same as above, but can use a custom comparator function.
+ *
+ * @param  {function} comparator - Custom comparator function.
+ * @param  {array}    array      - Haystack.
+ * @param  {any}      value      - Needle.
+ * @return {number}
+ */
+exports.searchWithComparator = function(comparator, array, value) {
+  var mid = 0,
+      lo = 0,
+      hi = ~-array.length,
+      comparison;
+
+  while (lo <= hi) {
+    mid = (lo + hi) >>> 1;
+
+    comparison = comparator(array[mid], value);
+
+    if (comparison > 0) {
+      hi = ~-mid;
+    }
+    else if (comparison < 0) {
+      lo = -~mid;
+    }
+    else {
+      return mid;
+    }
+  }
+
+  return -1;
+};
+
+/**
+ * Function returning the lower bound of the given value in the array.
+ *
+ * @param  {array}  array - Haystack.
+ * @param  {any}    value - Needle.
+ * @param  {number} [lo] - Start index.
+ * @param  {numner} [hi] - End index.
+ * @return {number}
+ */
+exports.lowerBound = function(array, value, lo, hi) {
+  var mid = 0;
+
+  lo = typeof lo !== 'undefined' ? lo : 0;
+  hi = typeof hi !== 'undefined' ? hi : array.length;
+
+  while (lo < hi) {
+    mid = (lo + hi) >>> 1;
+
+    if (value <= array[mid]) {
+      hi = mid;
+    }
+    else {
+      lo = -~mid;
+    }
+  }
+
+  return lo;
+};
+
+/**
+ * Same as above, but can use a custom comparator function.
+ *
+ * @param  {function} comparator - Custom comparator function.
+ * @param  {array}    array      - Haystack.
+ * @param  {any}      value      - Needle.
+ * @return {number}
+ */
+exports.lowerBoundWithComparator = function(comparator, array, value) {
+  var mid = 0,
+      lo = 0,
+      hi = array.length;
+
+  while (lo < hi) {
+    mid = (lo + hi) >>> 1;
+
+    if (comparator(value, array[mid]) <= 0) {
+      hi = mid;
+    }
+    else {
+      lo = -~mid;
+    }
+  }
+
+  return lo;
+};
+
+/**
+ * Same as above, but can work on sorted indices.
+ *
+ * @param  {array}    array - Haystack.
+ * @param  {array}    array - Indices.
+ * @param  {any}      value - Needle.
+ * @return {number}
+ */
+exports.lowerBoundIndices = function(array, indices, value, lo, hi) {
+  var mid = 0;
+
+  lo = typeof lo !== 'undefined' ? lo : 0;
+  hi = typeof hi !== 'undefined' ? hi : array.length;
+
+  while (lo < hi) {
+    mid = (lo + hi) >>> 1;
+
+    if (value <= array[indices[mid]]) {
+      hi = mid;
+    }
+    else {
+      lo = -~mid;
+    }
+  }
+
+  return lo;
+};
+
+/**
+ * Function returning the upper bound of the given value in the array.
+ *
+ * @param  {array}  array - Haystack.
+ * @param  {any}    value - Needle.
+ * @param  {number} [lo] - Start index.
+ * @param  {numner} [hi] - End index.
+ * @return {number}
+ */
+exports.upperBound = function(array, value, lo, hi) {
+  var mid = 0;
+
+  lo = typeof lo !== 'undefined' ? lo : 0;
+  hi = typeof hi !== 'undefined' ? hi : array.length;
+
+  while (lo < hi) {
+    mid = (lo + hi) >>> 1;
+
+    if (value >= array[mid]) {
+      lo = -~mid;
+    }
+    else {
+      hi = mid;
+    }
+  }
+
+  return lo;
+};
+
+/**
+ * Same as above, but can use a custom comparator function.
+ *
+ * @param  {function} comparator - Custom comparator function.
+ * @param  {array}    array      - Haystack.
+ * @param  {any}      value      - Needle.
+ * @return {number}
+ */
+exports.upperBoundWithComparator = function(comparator, array, value) {
+  var mid = 0,
+      lo = 0,
+      hi = array.length;
+
+  while (lo < hi) {
+    mid = (lo + hi) >>> 1;
+
+    if (comparator(value, array[mid]) >= 0) {
+      lo = -~mid;
+    }
+    else {
+      hi = mid;
+    }
+  }
+
+  return lo;
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/bitwise.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/bitwise.js
new file mode 100644
index 0000000000000000000000000000000000000000..191dfc2289af1f582e5ee90f66ddd804f2d9a0bc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/bitwise.js
@@ -0,0 +1,109 @@
+/**
+ * Mnemonist Bitwise Helpers
+ * ==========================
+ *
+ * Miscellaneous helpers helping with bitwise operations.
+ */
+
+/**
+ * Takes a 32 bits integer and returns its MSB using SWAR strategy.
+ *
+ * @param  {number} x - Target number.
+ * @return {number}
+ */
+function msb32(x) {
+  x |= (x >> 1);
+  x |= (x >> 2);
+  x |= (x >> 4);
+  x |= (x >> 8);
+  x |= (x >> 16);
+
+  return (x & ~(x >> 1));
+}
+exports.msb32 = msb32;
+
+/**
+ * Takes a byte and returns its MSB using SWAR strategy.
+ *
+ * @param  {number} x - Target number.
+ * @return {number}
+ */
+function msb8(x) {
+  x |= (x >> 1);
+  x |= (x >> 2);
+  x |= (x >> 4);
+
+  return (x & ~(x >> 1));
+}
+exports.msb8 = msb8;
+
+/**
+ * Takes a number and return bit at position.
+ *
+ * @param  {number} x   - Target number.
+ * @param  {number} pos - Position.
+ * @return {number}
+ */
+exports.test = function(x, pos) {
+  return (x >> pos) & 1;
+};
+
+/**
+ * Compare two bytes and return their critical bit.
+ *
+ * @param  {number} a - First byte.
+ * @param  {number} b - Second byte.
+ * @return {number}
+ */
+exports.criticalBit8 = function(a, b) {
+  return msb8(a ^ b);
+};
+
+exports.criticalBit8Mask = function(a, b) {
+  return (~msb8(a ^ b) >>> 0) & 0xff;
+};
+
+exports.testCriticalBit8 = function(x, mask) {
+  return (1 + (x | mask)) >> 8;
+};
+
+exports.criticalBit32Mask = function(a, b) {
+  return (~msb32(a ^ b) >>> 0) & 0xffffffff;
+};
+
+/**
+ * Takes a 32 bits integer and returns its population count (number of 1 of
+ * the binary representation).
+ *
+ * @param  {number} x - Target number.
+ * @return {number}
+ */
+exports.popcount = function(x) {
+  x -= x >> 1 & 0x55555555;
+  x = (x & 0x33333333) + (x >> 2 & 0x33333333);
+  x = x + (x >> 4) & 0x0f0f0f0f;
+  x += x >> 8;
+  x += x >> 16;
+  return x & 0x7f;
+};
+
+/**
+ * Slightly faster popcount function based on a precomputed table of 8bits
+ * words.
+ *
+ * @param  {number} x - Target number.
+ * @return {number}
+ */
+var TABLE8 = new Uint8Array(Math.pow(2, 8));
+
+for (var i = 0, l = TABLE8.length; i < l; i++)
+  TABLE8[i] = exports.popcount(i);
+
+exports.table8Popcount = function(x) {
+  return (
+    TABLE8[x & 0xff] +
+    TABLE8[(x >> 8) & 0xff] +
+    TABLE8[(x >> 16) & 0xff] +
+    TABLE8[(x >> 24) & 0xff]
+  );
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/comparators.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/comparators.js
new file mode 100644
index 0000000000000000000000000000000000000000..498b4a6fc2886142c8c14b456fd41daae71630af
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/comparators.js
@@ -0,0 +1,79 @@
+/**
+ * Mnemonist Heap Comparators
+ * ===========================
+ *
+ * Default comparators & functions dealing with comparators reversing etc.
+ */
+var DEFAULT_COMPARATOR = function(a, b) {
+  if (a < b)
+    return -1;
+  if (a > b)
+    return 1;
+
+  return 0;
+};
+
+var DEFAULT_REVERSE_COMPARATOR = function(a, b) {
+  if (a < b)
+    return 1;
+  if (a > b)
+    return -1;
+
+  return 0;
+};
+
+/**
+ * Function used to reverse a comparator.
+ */
+function reverseComparator(comparator) {
+  return function(a, b) {
+    return comparator(b, a);
+  };
+}
+
+/**
+ * Function returning a tuple comparator.
+ */
+function createTupleComparator(size) {
+  if (size === 2) {
+    return function(a, b) {
+      if (a[0] < b[0])
+        return -1;
+
+      if (a[0] > b[0])
+        return 1;
+
+      if (a[1] < b[1])
+        return -1;
+
+      if (a[1] > b[1])
+        return 1;
+
+      return 0;
+    };
+  }
+
+  return function(a, b) {
+    var i = 0;
+
+    while (i < size) {
+      if (a[i] < b[i])
+        return -1;
+
+      if (a[i] > b[i])
+        return 1;
+
+      i++;
+    }
+
+    return 0;
+  };
+}
+
+/**
+ * Exporting.
+ */
+exports.DEFAULT_COMPARATOR = DEFAULT_COMPARATOR;
+exports.DEFAULT_REVERSE_COMPARATOR = DEFAULT_REVERSE_COMPARATOR;
+exports.reverseComparator = reverseComparator;
+exports.createTupleComparator = createTupleComparator;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/hash-tables.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/hash-tables.js
new file mode 100644
index 0000000000000000000000000000000000000000..dfed95ea23e42e7fc08a2c342a122cf1650226af
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/hash-tables.js
@@ -0,0 +1,107 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist Hashtable Helpers
+ * ============================
+ *
+ * Miscellaneous helpers helper function dealing with hashtables.
+ */
+function jenkinsInt32(a) {
+
+  a = (a + 0x7ed55d16) + (a << 12);
+  a = (a ^ 0xc761c23c) ^ (a >> 19);
+  a = (a + 0x165667b1) + (a << 5);
+  a = (a + 0xd3a2646c) ^ (a << 9);
+  a = (a + 0xfd7046c5) + (a << 3);
+  a = (a ^ 0xb55a4f09) ^ (a >> 16);
+
+  return a;
+}
+
+function linearProbingGet(hash, keys, values, key) {
+  var n = keys.length,
+      j = hash(key) & (n - 1),
+      i = j;
+
+  var c;
+
+  while (true) {
+    c = keys[i];
+
+    if (c === key)
+      return values[i];
+
+    else if (c === 0)
+      return;
+
+    // Handling wrapping around
+    i += 1;
+    i %= n;
+
+    // Full turn
+    if (i === j)
+      return;
+  }
+}
+
+function linearProbingHas(hash, keys, key) {
+  var n = keys.length,
+      j = hash(key) & (n - 1),
+      i = j;
+
+  var c;
+
+  while (true) {
+    c = keys[i];
+
+    if (c === key)
+      return true;
+
+    else if (c === 0)
+      return false;
+
+    // Handling wrapping around
+    i += 1;
+    i %= n;
+
+    // Full turn
+    if (i === j)
+      return false;
+  }
+}
+
+function linearProbingSet(hash, keys, values, key, value) {
+  var n = keys.length,
+      j = hash(key) & (n - 1),
+      i = j;
+
+  var c;
+
+  while (true) {
+    c = keys[i];
+
+    if (c === 0 || c === key)
+      break;
+
+    // Handling wrapping around
+    i += 1;
+    i %= n;
+
+    // Full turn
+    if (i === j)
+      throw new Error('mnemonist/utils/hash-tables.linearProbingSet: table is full.');
+  }
+
+  keys[i] = key;
+  values[i] = value;
+}
+
+module.exports = {
+  hashes: {
+    jenkinsInt32: jenkinsInt32
+  },
+  linearProbing: {
+    get: linearProbingGet,
+    has: linearProbingHas,
+    set: linearProbingSet
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/iterables.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/iterables.js
new file mode 100644
index 0000000000000000000000000000000000000000..d95f7017d43055f9667368457924325e13a1da55
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/iterables.js
@@ -0,0 +1,93 @@
+/**
+ * Mnemonist Iterable Function
+ * ============================
+ *
+ * Harmonized iteration helpers over mixed iterable targets.
+ */
+var forEach = require('obliterator/foreach');
+
+var typed = require('./typed-arrays.js');
+
+/**
+ * Function used to determine whether the given object supports array-like
+ * random access.
+ *
+ * @param  {any} target - Target object.
+ * @return {boolean}
+ */
+function isArrayLike(target) {
+  return Array.isArray(target) || typed.isTypedArray(target);
+}
+
+/**
+ * Function used to guess the length of the structure over which we are going
+ * to iterate.
+ *
+ * @param  {any} target - Target object.
+ * @return {number|undefined}
+ */
+function guessLength(target) {
+  if (typeof target.length === 'number')
+    return target.length;
+
+  if (typeof target.size === 'number')
+    return target.size;
+
+  return;
+}
+
+/**
+ * Function used to convert an iterable to an array.
+ *
+ * @param  {any}   target - Iteration target.
+ * @return {array}
+ */
+function toArray(target) {
+  var l = guessLength(target);
+
+  var array = typeof l === 'number' ? new Array(l) : [];
+
+  var i = 0;
+
+  // TODO: we could optimize when given target is array like
+  forEach(target, function(value) {
+    array[i++] = value;
+  });
+
+  return array;
+}
+
+/**
+ * Same as above but returns a supplementary indices array.
+ *
+ * @param  {any}   target - Iteration target.
+ * @return {array}
+ */
+function toArrayWithIndices(target) {
+  var l = guessLength(target);
+
+  var IndexArray = typeof l === 'number' ?
+    typed.getPointerArray(l) :
+    Array;
+
+  var array = typeof l === 'number' ? new Array(l) : [];
+  var indices = typeof l === 'number' ? new IndexArray(l) : [];
+
+  var i = 0;
+
+  // TODO: we could optimize when given target is array like
+  forEach(target, function(value) {
+    array[i] = value;
+    indices[i] = i++;
+  });
+
+  return [array, indices];
+}
+
+/**
+ * Exporting.
+ */
+exports.isArrayLike = isArrayLike;
+exports.guessLength = guessLength;
+exports.toArray = toArray;
+exports.toArrayWithIndices = toArrayWithIndices;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/merge.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/merge.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf40d453627f0d23259c5150fb083450ad7dcb8f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/merge.js
@@ -0,0 +1,563 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Mnemonist Merge Helpers
+ * ========================
+ *
+ * Various merge algorithms used to handle sorted lists. Note that the given
+ * functions are optimized and won't accept mixed arguments.
+ *
+ * Note: maybe this piece of code belong to sortilege, along with binary-search.
+ */
+var typed = require('./typed-arrays.js'),
+    isArrayLike = require('./iterables.js').isArrayLike,
+    binarySearch = require('./binary-search.js'),
+    FibonacciHeap = require('../fibonacci-heap.js');
+
+// TODO: update to use exponential search
+// TODO: when not knowing final length => should use plain arrays rather than
+// same type as input
+
+/**
+ * Merge two sorted array-like structures into one.
+ *
+ * @param  {array} a - First array.
+ * @param  {array} b - Second array.
+ * @return {array}
+ */
+function mergeArrays(a, b) {
+
+  // One of the arrays is empty
+  if (a.length === 0)
+    return b.slice();
+  if (b.length === 0)
+    return a.slice();
+
+  // Finding min array
+  var tmp;
+
+  if (a[0] > b[0]) {
+    tmp = a;
+    a = b;
+    b = tmp;
+  }
+
+  // If array have non overlapping ranges, we can just concatenate them
+  var aEnd = a[a.length - 1],
+      bStart = b[0];
+
+  if (aEnd <= bStart) {
+    if (typed.isTypedArray(a))
+      return typed.concat(a, b);
+    return a.concat(b);
+  }
+
+  // Initializing target
+  var array = new a.constructor(a.length + b.length);
+
+  // Iterating until we overlap
+  var i, l, v;
+
+  for (i = 0, l = a.length; i < l; i++) {
+    v = a[i];
+
+    if (v <= bStart)
+      array[i] = v;
+    else
+      break;
+  }
+
+  // Handling overlap
+  var aPointer = i,
+      aLength = a.length,
+      bPointer = 0,
+      bLength = b.length,
+      aHead,
+      bHead;
+
+  while (aPointer < aLength && bPointer < bLength) {
+    aHead = a[aPointer];
+    bHead = b[bPointer];
+
+    if (aHead <= bHead) {
+      array[i++] = aHead;
+      aPointer++;
+    }
+    else {
+      array[i++] = bHead;
+      bPointer++;
+    }
+  }
+
+  // Filling
+  while (aPointer < aLength)
+    array[i++] = a[aPointer++];
+  while (bPointer < bLength)
+    array[i++] = b[bPointer++];
+
+  return array;
+}
+
+/**
+ * Perform the union of two already unique sorted array-like structures into one.
+ *
+ * @param  {array} a - First array.
+ * @param  {array} b - Second array.
+ * @return {array}
+ */
+function unionUniqueArrays(a, b) {
+
+  // One of the arrays is empty
+  if (a.length === 0)
+    return b.slice();
+  if (b.length === 0)
+    return a.slice();
+
+  // Finding min array
+  var tmp;
+
+  if (a[0] > b[0]) {
+    tmp = a;
+    a = b;
+    b = tmp;
+  }
+
+  // If array have non overlapping ranges, we can just concatenate them
+  var aEnd = a[a.length - 1],
+      bStart = b[0];
+
+  if (aEnd < bStart) {
+    if (typed.isTypedArray(a))
+      return typed.concat(a, b);
+    return a.concat(b);
+  }
+
+  // Initializing target
+  var array = new a.constructor();
+
+  // Iterating until we overlap
+  var i, l, v;
+
+  for (i = 0, l = a.length; i < l; i++) {
+    v = a[i];
+
+    if (v < bStart)
+      array.push(v);
+    else
+      break;
+  }
+
+  // Handling overlap
+  var aPointer = i,
+      aLength = a.length,
+      bPointer = 0,
+      bLength = b.length,
+      aHead,
+      bHead;
+
+  while (aPointer < aLength && bPointer < bLength) {
+    aHead = a[aPointer];
+    bHead = b[bPointer];
+
+    if (aHead <= bHead) {
+
+      if (array.length === 0 || array[array.length - 1] !== aHead)
+        array.push(aHead);
+
+      aPointer++;
+    }
+    else {
+      if (array.length === 0 || array[array.length - 1] !== bHead)
+        array.push(bHead);
+
+      bPointer++;
+    }
+  }
+
+  // Filling
+  // TODO: it's possible to optimize a bit here, since the condition is only
+  // relevant the first time
+  while (aPointer < aLength) {
+    aHead = a[aPointer++];
+
+    if (array.length === 0 || array[array.length - 1] !== aHead)
+      array.push(aHead);
+  }
+  while (bPointer < bLength) {
+    bHead = b[bPointer++];
+
+    if (array.length === 0 || array[array.length - 1] !== bHead)
+      array.push(bHead);
+  }
+
+  return array;
+}
+
+/**
+ * Perform the intersection of two already unique sorted array-like structures into one.
+ *
+ * @param  {array} a - First array.
+ * @param  {array} b - Second array.
+ * @return {array}
+ */
+exports.intersectionUniqueArrays = function(a, b) {
+
+  // One of the arrays is empty
+  if (a.length === 0 || b.length === 0)
+    return new a.constructor(0);
+
+  // Finding min array
+  var tmp;
+
+  if (a[0] > b[0]) {
+    tmp = a;
+    a = b;
+    b = tmp;
+  }
+
+  // If array have non overlapping ranges, there is no intersection
+  var aEnd = a[a.length - 1],
+      bStart = b[0];
+
+  if (aEnd < bStart)
+    return new a.constructor(0);
+
+  // Initializing target
+  var array = new a.constructor();
+
+  // Handling overlap
+  var aPointer = binarySearch.lowerBound(a, bStart),
+      aLength = a.length,
+      bPointer = 0,
+      bLength = binarySearch.upperBound(b, aEnd),
+      aHead,
+      bHead;
+
+  while (aPointer < aLength && bPointer < bLength) {
+    aHead = a[aPointer];
+    bHead = b[bPointer];
+
+    if (aHead < bHead) {
+      aPointer = binarySearch.lowerBound(a, bHead, aPointer + 1);
+    }
+    else if (aHead > bHead) {
+      bPointer = binarySearch.lowerBound(b, aHead, bPointer + 1);
+    }
+    else {
+      array.push(aHead);
+      aPointer++;
+      bPointer++;
+    }
+  }
+
+  return array;
+};
+
+/**
+ * Merge k sorted array-like structures into one.
+ *
+ * @param  {array<array>} arrays - Arrays to merge.
+ * @return {array}
+ */
+function kWayMergeArrays(arrays) {
+  var length = 0,
+      max = -Infinity,
+      al,
+      i,
+      l;
+
+  var filtered = [];
+
+  for (i = 0, l = arrays.length; i < l; i++) {
+    al = arrays[i].length;
+
+    if (al === 0)
+      continue;
+
+    filtered.push(arrays[i]);
+
+    length += al;
+
+    if (al > max)
+      max = al;
+  }
+
+  if (filtered.length === 0)
+    return new arrays[0].constructor(0);
+
+  if (filtered.length === 1)
+    return filtered[0].slice();
+
+  if (filtered.length === 2)
+    return mergeArrays(filtered[0], filtered[1]);
+
+  arrays = filtered;
+
+  var array = new arrays[0].constructor(length);
+
+  var PointerArray = typed.getPointerArray(max);
+
+  var pointers = new PointerArray(arrays.length);
+
+  // TODO: benchmark vs. a binomial heap
+  var heap = new FibonacciHeap(function(a, b) {
+    a = arrays[a][pointers[a]];
+    b = arrays[b][pointers[b]];
+
+    if (a < b)
+      return -1;
+
+    if (a > b)
+      return 1;
+
+    return 0;
+  });
+
+  for (i = 0; i < l; i++)
+    heap.push(i);
+
+  i = 0;
+
+  var p,
+      v;
+
+  while (heap.size) {
+    p = heap.pop();
+    v = arrays[p][pointers[p]++];
+    array[i++] = v;
+
+    if (pointers[p] < arrays[p].length)
+      heap.push(p);
+  }
+
+  return array;
+}
+
+/**
+ * Perform the union of k sorted unique array-like structures into one.
+ *
+ * @param  {array<array>} arrays - Arrays to merge.
+ * @return {array}
+ */
+function kWayUnionUniqueArrays(arrays) {
+  var max = -Infinity,
+      al,
+      i,
+      l;
+
+  var filtered = [];
+
+  for (i = 0, l = arrays.length; i < l; i++) {
+    al = arrays[i].length;
+
+    if (al === 0)
+      continue;
+
+    filtered.push(arrays[i]);
+
+    if (al > max)
+      max = al;
+  }
+
+  if (filtered.length === 0)
+    return new arrays[0].constructor(0);
+
+  if (filtered.length === 1)
+    return filtered[0].slice();
+
+  if (filtered.length === 2)
+    return unionUniqueArrays(filtered[0], filtered[1]);
+
+  arrays = filtered;
+
+  var array = new arrays[0].constructor();
+
+  var PointerArray = typed.getPointerArray(max);
+
+  var pointers = new PointerArray(arrays.length);
+
+  // TODO: benchmark vs. a binomial heap
+  var heap = new FibonacciHeap(function(a, b) {
+    a = arrays[a][pointers[a]];
+    b = arrays[b][pointers[b]];
+
+    if (a < b)
+      return -1;
+
+    if (a > b)
+      return 1;
+
+    return 0;
+  });
+
+  for (i = 0; i < l; i++)
+    heap.push(i);
+
+  var p,
+      v;
+
+  while (heap.size) {
+    p = heap.pop();
+    v = arrays[p][pointers[p]++];
+
+    if (array.length === 0 || array[array.length - 1] !== v)
+      array.push(v);
+
+    if (pointers[p] < arrays[p].length)
+      heap.push(p);
+  }
+
+  return array;
+}
+
+/**
+ * Perform the intersection of k sorted array-like structures into one.
+ *
+ * @param  {array<array>} arrays - Arrays to merge.
+ * @return {array}
+ */
+exports.kWayIntersectionUniqueArrays = function(arrays) {
+  var max = -Infinity,
+      maxStart = -Infinity,
+      minEnd = Infinity,
+      first,
+      last,
+      al,
+      i,
+      l;
+
+  for (i = 0, l = arrays.length; i < l; i++) {
+    al = arrays[i].length;
+
+    // If one of the arrays is empty, so is the intersection
+    if (al === 0)
+      return [];
+
+    if (al > max)
+      max = al;
+
+    first = arrays[i][0];
+    last = arrays[i][al - 1];
+
+    if (first > maxStart)
+      maxStart = first;
+
+    if (last < minEnd)
+      minEnd = last;
+  }
+
+  // Full overlap is impossible
+  if (maxStart > minEnd)
+    return [];
+
+  // Only one value
+  if (maxStart === minEnd)
+    return [maxStart];
+
+  // NOTE: trying to outsmart I(D,I(C,I(A,B))) is pointless unfortunately...
+  // NOTE: I tried to be very clever about bounds but it does not seem
+  // to improve the performance of the algorithm.
+  var a, b,
+      array = arrays[0],
+      aPointer,
+      bPointer,
+      aLimit,
+      bLimit,
+      aHead,
+      bHead,
+      start = maxStart;
+
+  for (i = 1; i < l; i++) {
+    a = array;
+    b = arrays[i];
+
+    // Change that to `[]` and observe some perf drops on V8...
+    array = new Array();
+
+    aPointer = 0;
+    bPointer = binarySearch.lowerBound(b, start);
+
+    aLimit = a.length;
+    bLimit = b.length;
+
+    while (aPointer < aLimit && bPointer < bLimit) {
+      aHead = a[aPointer];
+      bHead = b[bPointer];
+
+      if (aHead < bHead) {
+        aPointer = binarySearch.lowerBound(a, bHead, aPointer + 1);
+      }
+      else if (aHead > bHead) {
+        bPointer = binarySearch.lowerBound(b, aHead, bPointer + 1);
+      }
+      else {
+        array.push(aHead);
+        aPointer++;
+        bPointer++;
+      }
+    }
+
+    if (array.length === 0)
+      return array;
+
+    start = array[0];
+  }
+
+  return array;
+};
+
+/**
+ * Variadic merging all of the given arrays.
+ *
+ * @param  {...array}
+ * @return {array}
+ */
+exports.merge = function() {
+  if (arguments.length === 2) {
+    if (isArrayLike(arguments[0]))
+      return mergeArrays(arguments[0], arguments[1]);
+  }
+  else {
+    if (isArrayLike(arguments[0]))
+      return kWayMergeArrays(arguments);
+  }
+
+  return null;
+};
+
+/**
+ * Variadic function performing the union of all the given unique arrays.
+ *
+ * @param  {...array}
+ * @return {array}
+ */
+exports.unionUnique = function() {
+  if (arguments.length === 2) {
+    if (isArrayLike(arguments[0]))
+      return unionUniqueArrays(arguments[0], arguments[1]);
+  }
+  else {
+    if (isArrayLike(arguments[0]))
+      return kWayUnionUniqueArrays(arguments);
+  }
+
+  return null;
+};
+
+/**
+ * Variadic function performing the intersection of all the given unique arrays.
+ *
+ * @param  {...array}
+ * @return {array}
+ */
+exports.intersectionUnique = function() {
+  if (arguments.length === 2) {
+    if (isArrayLike(arguments[0]))
+      return exports.intersectionUniqueArrays(arguments[0], arguments[1]);
+  }
+  else {
+    if (isArrayLike(arguments[0]))
+      return exports.kWayIntersectionUniqueArrays(arguments);
+  }
+
+  return null;
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/murmurhash3.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/murmurhash3.js
new file mode 100644
index 0000000000000000000000000000000000000000..c09ec8acd18aa2c50f89dcf6a14a684655e71430
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/murmurhash3.js
@@ -0,0 +1,87 @@
+/* eslint no-fallthrough: 0 */
+/**
+ * Mnemonist MurmurHash 3
+ * =======================
+ *
+ * Straightforward implementation of the third version of MurmurHash.
+ *
+ * Note: this piece of code belong to haschisch.
+ */
+
+/**
+ * Various helpers.
+ */
+function mul32(a, b) {
+  return (a & 0xffff) * b + (((a >>> 16) * b & 0xffff) << 16) & 0xffffffff;
+}
+
+function sum32(a, b) {
+  return (a & 0xffff) + (b >>> 16) + (((a >>> 16) + b & 0xffff) << 16) & 0xffffffff;
+}
+
+function rotl32(a, b) {
+  return (a << b) | (a >>> (32 - b));
+}
+
+/**
+ * MumurHash3 function.
+ *
+ * @param  {number}    seed - Seed.
+ * @param  {ByteArray} data - Data.
+ */
+module.exports = function murmurhash3(seed, data) {
+  var c1 = 0xcc9e2d51,
+      c2 = 0x1b873593,
+      r1 = 15,
+      r2 = 13,
+      m = 5,
+      n = 0x6b64e654;
+
+  var hash = seed,
+      k1,
+      i,
+      l;
+
+  for (i = 0, l = data.length - 4; i <= l; i += 4) {
+    k1 = (
+      data[i] |
+      (data[i + 1] << 8) |
+      (data[i + 2] << 16) |
+      (data[i + 3] << 24)
+    );
+
+    k1 = mul32(k1, c1);
+    k1 = rotl32(k1, r1);
+    k1 = mul32(k1, c2);
+
+    hash ^= k1;
+    hash = rotl32(hash, r2);
+    hash = mul32(hash, m);
+    hash = sum32(hash, n);
+  }
+
+  k1 = 0;
+
+  switch (data.length & 3) {
+    case 3:
+      k1 ^= data[i + 2] << 16;
+    case 2:
+      k1 ^= data[i + 1] << 8;
+    case 1:
+      k1 ^= data[i];
+      k1 = mul32(k1, c1);
+      k1 = rotl32(k1, r1);
+      k1 = mul32(k1, c2);
+      hash ^= k1;
+    default:
+  }
+
+  hash ^= data.length;
+  hash ^= hash >>> 16;
+  hash = mul32(hash, 0x85ebca6b);
+  hash ^= hash >>> 13;
+  hash = mul32(hash, 0xc2b2ae35);
+  hash ^= hash >>> 16;
+
+  return hash >>> 0;
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/typed-arrays.js b/libs/shared/graph-layout/node_modules/mnemonist/utils/typed-arrays.js
new file mode 100644
index 0000000000000000000000000000000000000000..f70bcb7c810a8456aadced8eecc3dbc159b5af57
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/typed-arrays.js
@@ -0,0 +1,187 @@
+/**
+ * Mnemonist Typed Array Helpers
+ * ==============================
+ *
+ * Miscellaneous helpers related to typed arrays.
+ */
+
+/**
+ * When using an unsigned integer array to store pointers, one might want to
+ * choose the optimal word size in regards to the actual numbers of pointers
+ * to store.
+ *
+ * This helpers does just that.
+ *
+ * @param  {number} size - Expected size of the array to map.
+ * @return {TypedArray}
+ */
+var MAX_8BIT_INTEGER = Math.pow(2, 8) - 1,
+    MAX_16BIT_INTEGER = Math.pow(2, 16) - 1,
+    MAX_32BIT_INTEGER = Math.pow(2, 32) - 1;
+
+var MAX_SIGNED_8BIT_INTEGER = Math.pow(2, 7) - 1,
+    MAX_SIGNED_16BIT_INTEGER = Math.pow(2, 15) - 1,
+    MAX_SIGNED_32BIT_INTEGER = Math.pow(2, 31) - 1;
+
+exports.getPointerArray = function(size) {
+  var maxIndex = size - 1;
+
+  if (maxIndex <= MAX_8BIT_INTEGER)
+    return Uint8Array;
+
+  if (maxIndex <= MAX_16BIT_INTEGER)
+    return Uint16Array;
+
+  if (maxIndex <= MAX_32BIT_INTEGER)
+    return Uint32Array;
+
+  throw new Error('mnemonist: Pointer Array of size > 4294967295 is not supported.');
+};
+
+exports.getSignedPointerArray = function(size) {
+  var maxIndex = size - 1;
+
+  if (maxIndex <= MAX_SIGNED_8BIT_INTEGER)
+    return Int8Array;
+
+  if (maxIndex <= MAX_SIGNED_16BIT_INTEGER)
+    return Int16Array;
+
+  if (maxIndex <= MAX_SIGNED_32BIT_INTEGER)
+    return Int32Array;
+
+  return Float64Array;
+};
+
+/**
+ * Function returning the minimal type able to represent the given number.
+ *
+ * @param  {number} value - Value to test.
+ * @return {TypedArrayClass}
+ */
+exports.getNumberType = function(value) {
+
+  // <= 32 bits itnteger?
+  if (value === (value | 0)) {
+
+    // Negative
+    if (Math.sign(value) === -1) {
+      if (value <= 127 && value >= -128)
+        return Int8Array;
+
+      if (value <= 32767 && value >= -32768)
+        return Int16Array;
+
+      return Int32Array;
+    }
+    else {
+
+      if (value <= 255)
+        return Uint8Array;
+
+      if (value <= 65535)
+        return Uint16Array;
+
+      return Uint32Array;
+    }
+  }
+
+  // 53 bits integer & floats
+  // NOTE: it's kinda hard to tell whether we could use 32bits or not...
+  return Float64Array;
+};
+
+/**
+ * Function returning the minimal type able to represent the given array
+ * of JavaScript numbers.
+ *
+ * @param  {array}    array  - Array to represent.
+ * @param  {function} getter - Optional getter.
+ * @return {TypedArrayClass}
+ */
+var TYPE_PRIORITY = {
+  Uint8Array: 1,
+  Int8Array: 2,
+  Uint16Array: 3,
+  Int16Array: 4,
+  Uint32Array: 5,
+  Int32Array: 6,
+  Float32Array: 7,
+  Float64Array: 8
+};
+
+// TODO: make this a one-shot for one value
+exports.getMinimalRepresentation = function(array, getter) {
+  var maxType = null,
+      maxPriority = 0,
+      p,
+      t,
+      v,
+      i,
+      l;
+
+  for (i = 0, l = array.length; i < l; i++) {
+    v = getter ? getter(array[i]) : array[i];
+    t = exports.getNumberType(v);
+    p = TYPE_PRIORITY[t.name];
+
+    if (p > maxPriority) {
+      maxPriority = p;
+      maxType = t;
+    }
+  }
+
+  return maxType;
+};
+
+/**
+ * Function returning whether the given value is a typed array.
+ *
+ * @param  {any} value - Value to test.
+ * @return {boolean}
+ */
+exports.isTypedArray = function(value) {
+  return typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value);
+};
+
+/**
+ * Function used to concat byte arrays.
+ *
+ * @param  {...ByteArray}
+ * @return {ByteArray}
+ */
+exports.concat = function() {
+  var length = 0,
+      i,
+      o,
+      l;
+
+  for (i = 0, l = arguments.length; i < l; i++)
+    length += arguments[i].length;
+
+  var array = new (arguments[0].constructor)(length);
+
+  for (i = 0, o = 0; i < l; i++) {
+    array.set(arguments[i], o);
+    o += arguments[i].length;
+  }
+
+  return array;
+};
+
+/**
+ * Function used to initialize a byte array of indices.
+ *
+ * @param  {number}    length - Length of target.
+ * @return {ByteArray}
+ */
+exports.indices = function(length) {
+  var PointerArray = exports.getPointerArray(length);
+
+  var array = new PointerArray(length);
+
+  for (var i = 0; i < length; i++)
+    array[i] = i;
+
+  return array;
+};
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/utils/types.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/utils/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1a199d66d8c8e3c382d57756a7e535f24dc69549
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/utils/types.d.ts
@@ -0,0 +1,16 @@
+/**
+ * Mnemonist Generic Types
+ * ========================
+ * 
+ * Collection of types used throughout the library.
+ */
+export interface IArrayLike {
+  length: number;
+  slice(from: number, to?: number): IArrayLike;
+}
+
+export type ArrayLike = IArrayLike | ArrayBuffer;
+
+export interface IArrayLikeConstructor {
+  new(...args: any[]): ArrayLike;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/vector.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/vector.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..414f96943078b2ed9f804d8bde789b413844e82f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/vector.d.ts
@@ -0,0 +1,81 @@
+/**
+ * Mnemonist Vector Typings
+ * =========================
+ */
+import {IArrayLikeConstructor} from './utils/types';
+
+type VectorOptions = {
+  initialLength?: number;
+  initialCapacity?: number;
+  policy?: (capacity: number) => number;
+}
+
+export default class Vector implements Iterable<number> {
+
+  // Members
+  capacity: number;
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(ArrayClass: IArrayLikeConstructor, length: number | VectorOptions);
+
+  // Methods
+  clear(): void;
+  set(index: number, value: number): this;
+  reallocate(capacity: number): this;
+  grow(capacity?: number): this;
+  resize(length: number): this;
+  push(value: number): number;
+  pop(): number | undefined;
+  get(index: number): number;
+  forEach(callback: (index: number, value: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  entries(): IterableIterator<[number, number]>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+  toJSON(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}, ArrayClass: IArrayLikeConstructor, capacity?: number): Vector;
+}
+
+declare class TypedVector implements Iterable<number> {
+
+  // Members
+  capacity: number;
+  length: number;
+  size: number;
+
+  // Constructor
+  constructor(length: number | VectorOptions);
+
+  // Methods
+  clear(): void;
+  set(index: number, value: number): this;
+  reallocate(capacity: number): this;
+  grow(capacity?: number): this;
+  resize(length: number): this;
+  push(value: number): number;
+  pop(): number | undefined;
+  get(index: number): number;
+  forEach(callback: (index: number, value: number, set: this) => void, scope?: any): void;
+  values(): IterableIterator<number>;
+  entries(): IterableIterator<[number, number]>;
+  [Symbol.iterator](): IterableIterator<number>;
+  inspect(): any;
+  toJSON(): any;
+
+  // Statics
+  static from<I>(iterable: Iterable<I> | {[key: string] : I}, capacity?: number): TypedVector;
+}
+
+export class Int8Vector extends TypedVector {}
+export class Uint8Vector extends TypedVector {}
+export class Uint8ClampedVector extends TypedVector {}
+export class Int16Vector extends TypedVector {}
+export class Uint16Vector extends TypedVector {}
+export class Int32Vector extends TypedVector {}
+export class Uint32Vector extends TypedVector {}
+export class Float32Vector extends TypedVector {}
+export class Float64Array extends TypedVector {}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/vector.js b/libs/shared/graph-layout/node_modules/mnemonist/vector.js
new file mode 100644
index 0000000000000000000000000000000000000000..467bf20f5c3b33d4989447112c1a7a9f65e00b78
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/vector.js
@@ -0,0 +1,373 @@
+/**
+ * Mnemonist Vector
+ * =================
+ *
+ * Abstract implementation of a growing array that can be used with JavaScript
+ * typed arrays and other array-like structures.
+ *
+ * Note: should try and use ArrayBuffer.transfer when it will be available.
+ */
+var Iterator = require('obliterator/iterator'),
+    forEach = require('obliterator/foreach'),
+    iterables = require('./utils/iterables.js'),
+    typed = require('./utils/typed-arrays.js');
+
+/**
+ * Defaults.
+ */
+var DEFAULT_GROWING_POLICY = function(currentCapacity) {
+  return Math.max(1, Math.ceil(currentCapacity * 1.5));
+};
+
+var pointerArrayFactory = function(capacity) {
+  var PointerArray = typed.getPointerArray(capacity);
+
+  return new PointerArray(capacity);
+};
+
+/**
+ * Vector.
+ *
+ * @constructor
+ * @param {function}      ArrayClass             - An array constructor.
+ * @param {number|object} initialCapacityOrOptions - Self-explanatory:
+ * @param {number}        initialCapacity          - Initial capacity.
+ * @param {number}        initialLength            - Initial length.
+ * @param {function}      policy                   - Allocation policy.
+ */
+function Vector(ArrayClass, initialCapacityOrOptions) {
+  if (arguments.length < 1)
+    throw new Error('mnemonist/vector: expecting at least a byte array constructor.');
+
+  var initialCapacity = initialCapacityOrOptions || 0,
+      policy = DEFAULT_GROWING_POLICY,
+      initialLength = 0,
+      factory = false;
+
+  if (typeof initialCapacityOrOptions === 'object') {
+    initialCapacity = initialCapacityOrOptions.initialCapacity || 0;
+    initialLength = initialCapacityOrOptions.initialLength || 0;
+    policy = initialCapacityOrOptions.policy || policy;
+    factory = initialCapacityOrOptions.factory === true;
+  }
+
+  this.factory = factory ? ArrayClass : null;
+  this.ArrayClass = ArrayClass;
+  this.length = initialLength;
+  this.capacity = Math.max(initialLength, initialCapacity);
+  this.policy = policy;
+  this.array = new ArrayClass(this.capacity);
+}
+
+/**
+ * Method used to set a value.
+ *
+ * @param  {number} index - Index to edit.
+ * @param  {any}    value - Value.
+ * @return {Vector}
+ */
+Vector.prototype.set = function(index, value) {
+
+  // Out of bounds?
+  if (this.length < index)
+    throw new Error('Vector(' + this.ArrayClass.name + ').set: index out of bounds.');
+
+  // Updating value
+  this.array[index] = value;
+
+  return this;
+};
+
+/**
+ * Method used to get a value.
+ *
+ * @param  {number} index - Index to retrieve.
+ * @return {any}
+ */
+Vector.prototype.get = function(index) {
+  if (this.length < index)
+    return undefined;
+
+  return this.array[index];
+};
+
+/**
+ * Method used to apply the growing policy.
+ *
+ * @param  {number} [override] - Override capacity.
+ * @return {number}
+ */
+Vector.prototype.applyPolicy = function(override) {
+  var newCapacity = this.policy(override || this.capacity);
+
+  if (typeof newCapacity !== 'number' || newCapacity < 0)
+    throw new Error('mnemonist/vector.applyPolicy: policy returned an invalid value (expecting a positive integer).');
+
+  if (newCapacity <= this.capacity)
+    throw new Error('mnemonist/vector.applyPolicy: policy returned a less or equal capacity to allocate.');
+
+  // TODO: we should probably check that the returned number is an integer
+  return newCapacity;
+};
+
+/**
+ * Method used to reallocate the underlying array.
+ *
+ * @param  {number}       capacity - Target capacity.
+ * @return {Vector}
+ */
+Vector.prototype.reallocate = function(capacity) {
+  if (capacity === this.capacity)
+    return this;
+
+  var oldArray = this.array;
+
+  if (capacity < this.length)
+    this.length = capacity;
+
+  if (capacity > this.capacity) {
+    if (this.factory === null)
+      this.array = new this.ArrayClass(capacity);
+    else
+      this.array = this.factory(capacity);
+
+    if (typed.isTypedArray(this.array)) {
+      this.array.set(oldArray, 0);
+    }
+    else {
+      for (var i = 0, l = this.length; i < l; i++)
+        this.array[i] = oldArray[i];
+    }
+  }
+  else {
+    this.array = oldArray.slice(0, capacity);
+  }
+
+  this.capacity = capacity;
+
+  return this;
+};
+
+/**
+ * Method used to grow the array.
+ *
+ * @param  {number}       [capacity] - Optional capacity to match.
+ * @return {Vector}
+ */
+Vector.prototype.grow = function(capacity) {
+  var newCapacity;
+
+  if (typeof capacity === 'number') {
+
+    if (this.capacity >= capacity)
+      return this;
+
+    // We need to match the given capacity
+    newCapacity = this.capacity;
+
+    while (newCapacity < capacity)
+      newCapacity = this.applyPolicy(newCapacity);
+
+    this.reallocate(newCapacity);
+
+    return this;
+  }
+
+  // We need to run the policy once
+  newCapacity = this.applyPolicy();
+  this.reallocate(newCapacity);
+
+  return this;
+};
+
+/**
+ * Method used to resize the array. Won't deallocate.
+ *
+ * @param  {number}       length - Target length.
+ * @return {Vector}
+ */
+Vector.prototype.resize = function(length) {
+  if (length === this.length)
+    return this;
+
+  if (length < this.length) {
+    this.length = length;
+    return this;
+  }
+
+  this.length = length;
+  this.reallocate(length);
+
+  return this;
+};
+
+/**
+ * Method used to push a value into the array.
+ *
+ * @param  {any}    value - Value to push.
+ * @return {number}       - Length of the array.
+ */
+Vector.prototype.push = function(value) {
+  if (this.capacity === this.length)
+    this.grow();
+
+  this.array[this.length++] = value;
+
+  return this.length;
+};
+
+/**
+ * Method used to pop the last value of the array.
+ *
+ * @return {number} - The popped value.
+ */
+Vector.prototype.pop = function() {
+  if (this.length === 0)
+    return;
+
+  return this.array[--this.length];
+};
+
+/**
+ * Method used to create an iterator over a vector's values.
+ *
+ * @return {Iterator}
+ */
+Vector.prototype.values = function() {
+  var items = this.array,
+      l = this.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+    i++;
+
+    return {
+      value: value,
+      done: false
+    };
+  });
+};
+
+/**
+ * Method used to create an iterator over a vector's entries.
+ *
+ * @return {Iterator}
+ */
+Vector.prototype.entries = function() {
+  var items = this.array,
+      l = this.length,
+      i = 0;
+
+  return new Iterator(function() {
+    if (i >= l)
+      return {
+        done: true
+      };
+
+    var value = items[i];
+
+    return {
+      value: [i++, value],
+      done: false
+    };
+  });
+};
+
+/**
+ * Attaching the #.values method to Symbol.iterator if possible.
+ */
+if (typeof Symbol !== 'undefined')
+  Vector.prototype[Symbol.iterator] = Vector.prototype.values;
+
+/**
+ * Convenience known methods.
+ */
+Vector.prototype.inspect = function() {
+  var proxy = this.array.slice(0, this.length);
+
+  proxy.type = this.array.constructor.name;
+  proxy.items = this.length;
+  proxy.capacity = this.capacity;
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(proxy, 'constructor', {
+    value: Vector,
+    enumerable: false
+  });
+
+  return proxy;
+};
+
+if (typeof Symbol !== 'undefined')
+  Vector.prototype[Symbol.for('nodejs.util.inspect.custom')] = Vector.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a vector.
+ *
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {function} ArrayClass - Byte array class.
+ * @param  {number}   capacity   - Desired capacity.
+ * @return {Vector}
+ */
+Vector.from = function(iterable, ArrayClass, capacity) {
+
+  if (arguments.length < 3) {
+
+    // Attempting to guess the needed capacity
+    capacity = iterables.guessLength(iterable);
+
+    if (typeof capacity !== 'number')
+      throw new Error('mnemonist/vector.from: could not guess iterable length. Please provide desired capacity as last argument.');
+  }
+
+  var vector = new Vector(ArrayClass, capacity);
+
+  forEach(iterable, function(value) {
+    vector.push(value);
+  });
+
+  return vector;
+};
+
+/**
+ * Exporting.
+ */
+function subClass(ArrayClass) {
+  var SubClass = function(initialCapacityOrOptions) {
+    Vector.call(this, ArrayClass, initialCapacityOrOptions);
+  };
+
+  for (var k in Vector.prototype) {
+    if (Vector.prototype.hasOwnProperty(k))
+      SubClass.prototype[k] = Vector.prototype[k];
+  }
+
+  SubClass.from = function(iterable, capacity) {
+    return Vector.from(iterable, ArrayClass, capacity);
+  };
+
+  if (typeof Symbol !== 'undefined')
+    SubClass.prototype[Symbol.iterator] = SubClass.prototype.values;
+
+  return SubClass;
+}
+
+Vector.Int8Vector = subClass(Int8Array);
+Vector.Uint8Vector = subClass(Uint8Array);
+Vector.Uint8ClampedVector = subClass(Uint8ClampedArray);
+Vector.Int16Vector = subClass(Int16Array);
+Vector.Uint16Vector = subClass(Uint16Array);
+Vector.Int32Vector = subClass(Int32Array);
+Vector.Uint32Vector = subClass(Uint32Array);
+Vector.Float32Vector = subClass(Float32Array);
+Vector.Float64Vector = subClass(Float64Array);
+Vector.PointerVector = subClass(pointerArrayFactory);
+
+module.exports = Vector;
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.d.ts b/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c0335418c44a897bb4538a6f91a716c7a1e4a61
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.d.ts
@@ -0,0 +1,27 @@
+/**
+ * Mnemonist VPTree Typings
+ * =========================
+ */
+type DistanceFunction<T> = (a: T, b: T) => number;
+type QueryMatch<T> = {distance: number, item: T};
+
+export default class VPTree<T> {
+
+  // Members
+  distance: DistanceFunction<T>;
+  size: number;
+  D: number;
+
+  // Constructor
+  constructor(distance: DistanceFunction<T>, items: Iterable<T>);
+
+  // Methods
+  nearestNeighbors(k: number, query: T): Array<QueryMatch<T>>;
+  neighbors(radius: number, query: T): Array<QueryMatch<T>>;
+
+  // Statics
+  static from<I>(
+    iterable: Iterable<I> | {[key: string] : I},
+    distance: DistanceFunction<I>
+  ): VPTree<I>;
+}
diff --git a/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.js b/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..2acd01e8481355eb3a39e1df3e98069cd1e40899
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/mnemonist/vp-tree.js
@@ -0,0 +1,367 @@
+/**
+ * Mnemonist Vantage Point Tree
+ * =============================
+ *
+ * JavaScript implementation of the Vantage Point Tree storing the binary
+ * tree as a flat byte array.
+ *
+ * Note that a VPTree has worst cases and is likely not to be perfectly
+ * balanced because of median ambiguity. It is therefore not suitable
+ * for hairballs and tiny datasets.
+ *
+ * [Reference]:
+ * https://en.wikipedia.org/wiki/Vantage-point_tree
+ */
+var iterables = require('./utils/iterables.js'),
+    typed = require('./utils/typed-arrays.js'),
+    inplaceQuickSortIndices = require('./sort/quick.js').inplaceQuickSortIndices,
+    lowerBoundIndices = require('./utils/binary-search.js').lowerBoundIndices,
+    Heap = require('./heap.js');
+
+var getPointerArray = typed.getPointerArray;
+
+// TODO: implement vantage point selection techniques (by swapping with last)
+// TODO: is this required to implement early termination for k <= size?
+
+/**
+ * Heap comparator used by the #.nearestNeighbors method.
+ */
+function comparator(a, b) {
+  if (a.distance < b.distance)
+    return 1;
+
+  if (a.distance > b.distance)
+    return -1;
+
+  return 0;
+}
+
+/**
+ * Function used to create the binary tree.
+ *
+ * @param  {function}     distance - Distance function to use.
+ * @param  {array}        items    - Items to index (will be mutated).
+ * @param  {array}        indices  - Indexes of the items.
+ * @return {Float64Array}          - The flat binary tree.
+ */
+function createBinaryTree(distance, items, indices) {
+  var N = indices.length;
+
+  var PointerArray = getPointerArray(N);
+
+  var C = 0,
+      nodes = new PointerArray(N),
+      lefts = new PointerArray(N),
+      rights = new PointerArray(N),
+      mus = new Float64Array(N),
+      stack = [0, 0, N],
+      distances = new Float64Array(N),
+      nodeIndex,
+      vantagePoint,
+      medianIndex,
+      lo,
+      hi,
+      mid,
+      mu,
+      i,
+      l;
+
+  while (stack.length) {
+    hi = stack.pop();
+    lo = stack.pop();
+    nodeIndex = stack.pop();
+
+    // Getting our vantage point
+    vantagePoint = indices[hi - 1];
+    hi--;
+
+    l = hi - lo;
+
+    // Storing vantage point
+    nodes[nodeIndex] = vantagePoint;
+
+    // We are in a leaf
+    if (l === 0)
+      continue;
+
+    // We only have two elements, the second one has to go right
+    if (l === 1) {
+
+      // We put remaining item to the right
+      mu = distance(items[vantagePoint], items[indices[lo]]);
+
+      mus[nodeIndex] = mu;
+
+      // Right
+      C++;
+      rights[nodeIndex] = C;
+      nodes[C] = indices[lo];
+
+      continue;
+    }
+
+    // Computing distance from vantage point to other points
+    for (i = lo; i < hi; i++)
+      distances[indices[i]] = distance(items[vantagePoint], items[indices[i]]);
+
+    inplaceQuickSortIndices(distances, indices, lo, hi);
+
+    // Finding median of distances
+    medianIndex = lo + (l / 2) - 1;
+
+    // Need to interpolate?
+    if (medianIndex === (medianIndex | 0)) {
+      mu = (
+        distances[indices[medianIndex]] +
+        distances[indices[medianIndex + 1]]
+      ) / 2;
+    }
+    else {
+      mu = distances[indices[Math.ceil(medianIndex)]];
+    }
+
+    // Storing mu
+    mus[nodeIndex] = mu;
+
+    mid = lowerBoundIndices(distances, indices, mu, lo, hi);
+
+    // console.log('Vantage point', items[vantagePoint], vantagePoint);
+    // console.log('mu =', mu);
+    // console.log('lo =', lo);
+    // console.log('hi =', hi);
+    // console.log('mid =', mid);
+
+    // console.log('need to split', Array.from(indices).slice(lo, hi).map(i => {
+    //   return [distances[i], distance(items[vantagePoint], items[i]), items[i]];
+    // }));
+
+    // Right
+    if (hi - mid > 0) {
+      C++;
+      rights[nodeIndex] = C;
+      stack.push(C, mid, hi);
+      // console.log('Went right with ', Array.from(indices).slice(mid, hi).map(i => {
+      //   return [distances[i], distance(items[vantagePoint], items[i]), items[i]];
+      // }));
+    }
+
+    // Left
+    if (mid - lo > 0) {
+      C++;
+      lefts[nodeIndex] = C;
+      stack.push(C, lo, mid);
+      // console.log('Went left with', Array.from(indices).slice(lo, mid).map(i => {
+      //   return [distances[i], distance(items[vantagePoint], items[i]), items[i]];
+      // }));
+    }
+
+    // console.log();
+  }
+
+  return {
+    nodes: nodes,
+    lefts: lefts,
+    rights: rights,
+    mus: mus
+  };
+}
+
+/**
+ * VPTree.
+ *
+ * @constructor
+ * @param {function} distance - Distance function to use.
+ * @param {Iterable} items    - Items to store.
+ */
+function VPTree(distance, items) {
+  if (typeof distance !== 'function')
+    throw new Error('mnemonist/VPTree.constructor: given `distance` must be a function.');
+
+  if (!items)
+    throw new Error('mnemonist/VPTree.constructor: you must provide items to the tree. A VPTree cannot be updated after its creation.');
+
+  // Properties
+  this.distance = distance;
+  this.heap = new Heap(comparator);
+  this.D = 0;
+
+  var arrays = iterables.toArrayWithIndices(items);
+  this.items = arrays[0];
+  var indices = arrays[1];
+
+  // Creating the binary tree
+  this.size = indices.length;
+
+  var result = createBinaryTree(distance, this.items, indices);
+
+  this.nodes = result.nodes;
+  this.lefts = result.lefts;
+  this.rights = result.rights;
+  this.mus = result.mus;
+}
+
+/**
+ * Function used to retrieve the k nearest neighbors of the query.
+ *
+ * @param  {number} k     - Number of neighbors to retrieve.
+ * @param  {any}    query - The query.
+ * @return {array}
+ */
+VPTree.prototype.nearestNeighbors = function(k, query) {
+  var neighbors = this.heap,
+      stack = [0],
+      tau = Infinity,
+      nodeIndex,
+      itemIndex,
+      vantagePoint,
+      leftIndex,
+      rightIndex,
+      mu,
+      d;
+
+  this.D = 0;
+
+  while (stack.length) {
+    nodeIndex = stack.pop();
+    itemIndex = this.nodes[nodeIndex];
+    vantagePoint = this.items[itemIndex];
+
+    // Distance between query & the current vantage point
+    d = this.distance(vantagePoint, query);
+    this.D++;
+
+    if (d < tau) {
+      neighbors.push({distance: d, item: vantagePoint});
+
+      // Trimming
+      if (neighbors.size > k)
+        neighbors.pop();
+
+      // Adjusting tau (only if we already have k items, else it stays Infinity)
+      if (neighbors.size >= k)
+       tau = neighbors.peek().distance;
+    }
+
+    leftIndex = this.lefts[nodeIndex];
+    rightIndex = this.rights[nodeIndex];
+
+    // We are a leaf
+    if (!leftIndex && !rightIndex)
+      continue;
+
+    mu = this.mus[nodeIndex];
+
+    if (d < mu) {
+      if (leftIndex && d < mu + tau)
+        stack.push(leftIndex);
+      if (rightIndex && d >= mu - tau) // Might not be necessary to test d
+        stack.push(rightIndex);
+    }
+    else {
+      if (rightIndex && d >= mu - tau)
+        stack.push(rightIndex);
+      if (leftIndex && d < mu + tau) // Might not be necessary to test d
+        stack.push(leftIndex);
+    }
+  }
+
+  var array = new Array(neighbors.size);
+
+  for (var i = neighbors.size - 1; i >= 0; i--)
+    array[i] = neighbors.pop();
+
+  return array;
+};
+
+/**
+ * Function used to retrieve every neighbors of query in the given radius.
+ *
+ * @param  {number} radius - Radius.
+ * @param  {any}    query  - The query.
+ * @return {array}
+ */
+VPTree.prototype.neighbors = function(radius, query) {
+  var neighbors = [],
+      stack = [0],
+      nodeIndex,
+      itemIndex,
+      vantagePoint,
+      leftIndex,
+      rightIndex,
+      mu,
+      d;
+
+  this.D = 0;
+
+  while (stack.length) {
+    nodeIndex = stack.pop();
+    itemIndex = this.nodes[nodeIndex];
+    vantagePoint = this.items[itemIndex];
+
+    // Distance between query & the current vantage point
+    d = this.distance(vantagePoint, query);
+    this.D++;
+
+    if (d <= radius)
+      neighbors.push({distance: d, item: vantagePoint});
+
+    leftIndex = this.lefts[nodeIndex];
+    rightIndex = this.rights[nodeIndex];
+
+    // We are a leaf
+    if (!leftIndex && !rightIndex)
+      continue;
+
+    mu = this.mus[nodeIndex];
+
+    if (d < mu) {
+      if (leftIndex && d < mu + radius)
+        stack.push(leftIndex);
+      if (rightIndex && d >= mu - radius) // Might not be necessary to test d
+        stack.push(rightIndex);
+    }
+    else {
+      if (rightIndex && d >= mu - radius)
+        stack.push(rightIndex);
+      if (leftIndex && d < mu + radius) // Might not be necessary to test d
+        stack.push(leftIndex);
+    }
+  }
+
+  return neighbors;
+};
+
+/**
+ * Convenience known methods.
+ */
+VPTree.prototype.inspect = function() {
+  var array = this.items.slice();
+
+  // Trick so that node displays the name of the constructor
+  Object.defineProperty(array, 'constructor', {
+    value: VPTree,
+    enumerable: false
+  });
+
+  return array;
+};
+
+if (typeof Symbol !== 'undefined')
+  VPTree.prototype[Symbol.for('nodejs.util.inspect.custom')] = VPTree.prototype.inspect;
+
+/**
+ * Static @.from function taking an arbitrary iterable & converting it into
+ * a tree.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {function} distance - Distance function to use.
+ * @return {VPTree}
+ */
+VPTree.from = function(iterable, distance) {
+  return new VPTree(distance, iterable);
+};
+
+/**
+ * Exporting.
+ */
+module.exports = VPTree;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/LICENSE.txt b/libs/shared/graph-layout/node_modules/obliterator/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df12ee58e0ff4fa313b5bc24e694918ccc1b363a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/obliterator/README.md b/libs/shared/graph-layout/node_modules/obliterator/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8f3fc6656314b07a90c3fa644395b72d59f9db64
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/README.md
@@ -0,0 +1,415 @@
+[![Build Status](https://github.com/Yomguithereal/obliterator/workflows/Tests/badge.svg)](https://github.com/Yomguithereal/obliterator/actions)
+
+# Obliterator
+
+Obliterator is a dead simple JavaScript/TypeScript library providing miscellaneous higher-order iterator/iterable functions such as combining two or more iterators into a single one.
+
+Note that when possible, `obliterator` also consider sequences such as arrays, strings etc. as valid iterables (although they are not proper ES6 iterables values), for convenience.
+
+# Installation
+
+```
+npm install --save obliterator
+```
+
+Note that `obliterator` comes along with its TypeScript declarations.
+
+# Usage
+
+## Summary
+
+_Classes_
+
+- [Iterator](#iterator)
+
+_Functions_
+
+- [chain](#chain)
+- [combinations](#combinations)
+- [consume](#consume)
+- [every](#every)
+- [filter](#filter)
+- [find](#find)
+- [forEach](#foreach)
+- [forEachWithNullKeys](#foreachwithnullkeys)
+- [includes](#includes)
+- [iter](#iter)
+- [map](#map)
+- [match](#match)
+- [permutations](#permutations)
+- [powerSet](#powerSet)
+- [some](#some)
+- [split](#split)
+- [take](#take)
+
+## Iterator
+
+A handy Iterator class easily usable with ES2015's `for ... of` loop constructs & spread operator.
+
+```js
+import Iterator from 'obliterator/iterator';
+// Or
+import {Iterator} from 'obliterator';
+
+const iterator = new Iterator(function () {
+  // Define what the `next` function does
+  return {done: false, value: 34};
+});
+
+// Checking that the given value is an iterator (native or else)
+Iterator.is(value);
+
+// Creating an empty iterator
+const emptyIterator = Iterator.empty();
+
+// Creating a simple iterator from a single value
+const simpleIterator = Iterator.of(34);
+
+// Creating a simple iterator from multiple values
+const multipleIterator = Iterator.of(1, 2, 3);
+```
+
+## chain
+
+Variadic function chaining all the given iterable-like values.
+
+```js
+import chain from 'obliterator/chain';
+// Or
+import {chain} from 'obliterator';
+
+const set1 = new Set('a');
+const set2 = new Set('bc');
+
+const chained = chain(set1.values(), set2);
+
+chained.next();
+>>> {done: false, value: 'a'}
+chained.next();
+>>> {done: false, value: 'b'}
+```
+
+## combinations
+
+Returns an iterator of combinations of the given array and of the given size.
+
+Note that for performance reasons, the yielded combination is always the same object.
+
+```js
+import combinations from 'obliterator/combinations';
+// Or
+import {combinations} from 'obliterator';
+
+const iterator = combinations(['A', 'B', 'C', 'D'], 2);
+
+iterator.next().value;
+>>> ['A', 'B']
+iterator.next().value;
+>>> ['A', 'C']
+```
+
+## consume
+
+Function consuming the given iterator fully or for n steps.
+
+```js
+import consume from 'obliterator/consume';
+// Or
+import {consume} from 'obliterator';
+
+const set = new Set([1, 2, 3]);
+
+// Consuming the whole iterator
+let iterator = set.values();
+consume(iterator);
+iterator.next().done >>> true;
+
+// Consuming n steps
+let iterator = set.values();
+consume(iterator, 2);
+iterator.next().value >>> 3;
+```
+
+## every
+
+Function returning whether all items of an iterable-like match the given predicate function.
+
+```js
+import every from 'obliterator/every';
+// Or
+import {every} from 'obliterator';
+
+every([2, 4, 6], n => n % 2 === 0);
+>>> true
+
+every([1, 2, 3], n => n % 2 === 0);
+>>> false
+```
+
+## filter
+
+Function returning an iterator filtering another one's values using the given predicate function.
+
+```js
+import filter from 'obliterator/filter';
+// Or
+import {filter} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const even = x => x % 2 === 0;
+
+const iterator = filter(set.values(), even);
+
+iterator.next().value >>> 2;
+iterator.next().value >>> 4;
+```
+
+## find
+
+Function returning the next item matching given predicate function in an iterable-like.
+
+```js
+import find from 'obliterator/find';
+// Or
+import {find} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const even = x => x % 2 === 0;
+
+const values = set.values();
+
+find(values, even);
+>>> 2
+
+find(values, even);
+>>> 4
+
+find(values, even);
+>>> undefined
+```
+
+## forEach
+
+Function able to iterate over almost any JavaScript iterable value using a callback.
+
+Supported values range from arrays, typed arrays, sets, maps, objects, strings, arguments, iterators, arbitrary iterables etc.
+
+```js
+import forEach from 'obliterator/foreach';
+// Or
+import {forEach} from 'obliterator';
+
+const set = new Set(['apple', 'banana']);
+
+forEach(set.values(), (value, i) => {
+  console.log(i, value);
+});
+
+// Iterating over a string
+forEach('abc', (char, i) => ...);
+
+// Iterating over a map
+forEach(map, (value, key) => ...);
+```
+
+## forEachWithNullKeys
+
+Variant of [forEach](#foreach) one can use to iterate over mixed values but with the twist that iterables without proper keys (lists, sets etc.), will yield `null` instead of an index key.
+
+Supported values range from arrays, typed arrays, sets, maps, objects, strings, arguments, iterators, arbitrary iterables etc.
+
+```js
+import {forEachWithNullKeys} from 'obliterator/foreach';
+
+const set = new Set(['apple', 'banana']);
+
+forEach(set, (value, key) => {
+  console.log(key, value);
+});
+>>> null, 'apple'
+>>> null, 'banana'
+```
+
+## includes
+
+Function returning whether the given value can be found in given iterable-like.
+
+```js
+import {includes} from 'obliterator';
+// Or
+import includes from 'obliterator/includes';
+
+includes([1, 2, 3], 3);
+>>> true;
+
+includes('test', 'a');
+>>> false;
+```
+
+## iter
+
+Function casting any iterable-like value to a proper iterator. Will throw an error if the given value cannot be cast as an iterator.
+
+```js
+import {iter} from 'obliterator';
+// Or
+import iter from 'obliterator/iter';
+
+iter('test');
+iter(new Set([1, 2, 3]));
+
+// This will throw:
+iter(null);
+```
+
+## map
+
+Function returning an iterator mapping another one's values using the given function.
+
+```js
+import map from 'obliterator/map';
+// Or
+import {map} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const triple = x => x * 3;
+
+const iterator = map(set.values(), triple);
+
+iterator.next().value >>> 3;
+iterator.next().value >>> 6;
+```
+
+## match
+
+Function returning an iterator over the matches of a given regex applied to the target string.
+
+```js
+import match from 'obliterator/match';
+// Or
+import {match} from 'obliterator';
+
+const iterator = match(/t/, 'test');
+
+iterator.next().value.index >>> 0;
+iterator.next().value.index >>> 3;
+```
+
+## permutations
+
+Returns an iterator of permutations of the given array and of the given size.
+
+Note that for performance reasons, the yielded permutation is always the same object.
+
+```js
+import permutations from 'obliterator/permutations';
+// Or
+import {permutations} from 'obliterator';
+
+let iterator = permutations([1, 2, 3]);
+
+iterator.next().value
+>>> [1, 2, 3]
+iterator.next().value
+>>> [1, 3, 2]
+
+iterator = permutations(['A', 'B', 'C', 'D'], 2);
+
+iterator.next().value;
+>>> ['A', 'B']
+iterator.next().value;
+>>> ['A', 'C']
+```
+
+## powerSet
+
+Returns an iterator of sets composing the power set of the given array.
+
+```js
+import powerSet from 'obliterator/power-set';
+// Or
+import {powerSet} from 'obliterator';
+
+const iterator = powerSet(['A', 'B', 'C']);
+
+iterator.next().value;
+>>> []
+iterator.next().value;
+>>> ['A']
+```
+
+## some
+
+Returns whether the given iterable-like has some item matching the given predicate function.
+
+```js
+import some from 'obliterator/some';
+// Or
+import {some} from 'obliterator';
+
+some(new Set([1, 2, 3]), n => n % 2 === 0);
+>>> true
+
+some('test', c => c === 'a');
+>>> false
+```
+
+## split
+
+Returns an iterator over the splits of the target string, according to the given RegExp pattern.
+
+```js
+import split from 'obliterator/split';
+// Or
+import {split} from 'obliterator';
+
+const iterator = split(/;/g, 'hello;world;super');
+
+iterator.next().value;
+>>> 'hello'
+iterator.next().value;
+>>> 'world'
+```
+
+## take
+
+Function taking values from given iterator and returning them in an array.
+
+```js
+import take from 'obliterator/take';
+// Or
+import {take} from 'obliterator';
+
+const set = new Set([1, 2, 3]);
+
+// To take n values from the iterator
+take(set.values(), 2);
+>>> [1, 2]
+
+// To convert the full iterator into an array
+take(set.values());
+>>> [1, 2, 3]
+```
+
+# Contribution
+
+Contributions are obviously welcome. Please be sure to lint the code & add the relevant unit tests before submitting any PR.
+
+```
+git clone git@github.com:Yomguithereal/obliterator.git
+cd obliterator
+npm install
+
+# To lint the code
+npm run lint
+
+# To run the unit tests
+npm test
+```
+
+# License
+
+[MIT](LICENSE.txt)
diff --git a/libs/shared/graph-layout/node_modules/obliterator/chain.d.ts b/libs/shared/graph-layout/node_modules/obliterator/chain.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cc4c78db9a19d49256e93ba0d9ae811cd51635d
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/chain.d.ts
@@ -0,0 +1,5 @@
+import type {IntoInterator} from './types';
+
+export default function chain<T>(
+  ...iterables: IntoInterator<T>[]
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/chain.js b/libs/shared/graph-layout/node_modules/obliterator/chain.js
new file mode 100644
index 0000000000000000000000000000000000000000..00eb68dced0dd4e5253f5bbee1e96e5090acb509
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/chain.js
@@ -0,0 +1,46 @@
+/**
+ * Obliterator Chain Function
+ * ===========================
+ *
+ * Variadic function combining the given iterables.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Chain.
+ *
+ * @param  {...Iterator} iterables - Target iterables.
+ * @return {Iterator}
+ */
+module.exports = function chain() {
+  var iterables = arguments;
+  var current = null;
+  var i = -1;
+
+  /* eslint-disable no-constant-condition */
+  return new Iterator(function next() {
+    var step = null;
+
+    do {
+      if (current === null) {
+        i++;
+
+        if (i >= iterables.length) return {done: true};
+
+        current = iter(iterables[i]);
+      }
+
+      step = current.next();
+
+      if (step.done === true) {
+        current = null;
+        continue;
+      }
+
+      break;
+    } while (true);
+
+    return step;
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/combinations.d.ts b/libs/shared/graph-layout/node_modules/obliterator/combinations.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..901a0eaa9bfba7e92b6521f2d500fc42cff9438b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/combinations.d.ts
@@ -0,0 +1,4 @@
+export default function combinations<T>(
+  array: Array<T>,
+  r: number
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/combinations.js b/libs/shared/graph-layout/node_modules/obliterator/combinations.js
new file mode 100644
index 0000000000000000000000000000000000000000..c60099d328b9badc21a5099b4fbc26219bd31f00
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/combinations.js
@@ -0,0 +1,76 @@
+/**
+ * Obliterator Combinations Function
+ * ==================================
+ *
+ * Iterator returning combinations of the given array.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Helper mapping indices to items.
+ */
+function indicesToItems(target, items, indices, r) {
+  for (var i = 0; i < r; i++) target[i] = items[indices[i]];
+}
+
+/**
+ * Combinations.
+ *
+ * @param  {array}    array - Target array.
+ * @param  {number}   r     - Size of the subsequences.
+ * @return {Iterator}
+ */
+module.exports = function combinations(array, r) {
+  if (!Array.isArray(array))
+    throw new Error(
+      'obliterator/combinations: first argument should be an array.'
+    );
+
+  var n = array.length;
+
+  if (typeof r !== 'number')
+    throw new Error(
+      'obliterator/combinations: second argument should be omitted or a number.'
+    );
+
+  if (r > n)
+    throw new Error(
+      'obliterator/combinations: the size of the subsequences should not exceed the length of the array.'
+    );
+
+  if (r === n) return Iterator.of(array.slice());
+
+  var indices = new Array(r),
+    subsequence = new Array(r),
+    first = true,
+    i;
+
+  for (i = 0; i < r; i++) indices[i] = i;
+
+  return new Iterator(function next() {
+    if (first) {
+      first = false;
+
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    if (indices[r - 1]++ < n - 1) {
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    i = r - 2;
+
+    while (i >= 0 && indices[i] >= n - (r - i)) --i;
+
+    if (i < 0) return {done: true};
+
+    indices[i]++;
+
+    while (++i < r) indices[i] = indices[i - 1] + 1;
+
+    indicesToItems(subsequence, array, indices, r);
+    return {value: subsequence, done: false};
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/consume.d.ts b/libs/shared/graph-layout/node_modules/obliterator/consume.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16812ee0ec670149dec8046f62f638e8715a5619
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/consume.d.ts
@@ -0,0 +1 @@
+export default function consume<T>(iterator: Iterator<T>, steps?: number): void;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/consume.js b/libs/shared/graph-layout/node_modules/obliterator/consume.js
new file mode 100644
index 0000000000000000000000000000000000000000..1fa7007490deca7579d442a462bf1b0ce9828266
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/consume.js
@@ -0,0 +1,29 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Consume Function
+ * =============================
+ *
+ * Function consuming the given iterator for n or every steps.
+ */
+
+/**
+ * Consume.
+ *
+ * @param  {Iterator} iterator - Target iterator.
+ * @param  {number}   [steps]  - Optional steps.
+ */
+module.exports = function consume(iterator, steps) {
+  var step,
+    l = arguments.length > 1 ? steps : Infinity,
+    i = 0;
+
+  while (true) {
+    if (i === l) return;
+
+    step = iterator.next();
+
+    if (step.done) return;
+
+    i++;
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/every.d.ts b/libs/shared/graph-layout/node_modules/obliterator/every.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..361fd48e24e01e03cebae14c544c31ddcef0a288
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/every.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function every<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): boolean;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/every.js b/libs/shared/graph-layout/node_modules/obliterator/every.js
new file mode 100644
index 0000000000000000000000000000000000000000..f2d296c9426c6ce58b88c841057da8b76884c06c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/every.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Every Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning whether all
+ * its items match the given predicate.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Every.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function every(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (!predicate(step.value)) return false;
+  }
+
+  return true;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/filter.d.ts b/libs/shared/graph-layout/node_modules/obliterator/filter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91982950c2f56bdc143e6dca7a857736cff4f6eb
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/filter.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function filter<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/filter.js b/libs/shared/graph-layout/node_modules/obliterator/filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..b17945e0eba88a6f8a3f9a8fc8e5de8488ac911f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/filter.js
@@ -0,0 +1,28 @@
+/**
+ * Obliterator Filter Function
+ * ===========================
+ *
+ * Function returning a iterator filtering the given iterator.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Filter.
+ *
+ * @param  {Iterable} target    - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {Iterator}
+ */
+module.exports = function filter(target, predicate) {
+  var iterator = iter(target);
+  var step;
+
+  return new Iterator(function () {
+    do {
+      step = iterator.next();
+    } while (!step.done && !predicate(step.value));
+
+    return step;
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/find.d.ts b/libs/shared/graph-layout/node_modules/obliterator/find.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3dea30c89a36c675f63dbbc4d0ff89b520251d2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/find.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function find<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): T | undefined;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/find.js b/libs/shared/graph-layout/node_modules/obliterator/find.js
new file mode 100644
index 0000000000000000000000000000000000000000..d7641cebf1b57238034d8e5ad9178743d632b63e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/find.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Find Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning the first item
+ * matching the given predicate.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Find.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function find(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (predicate(step.value)) return step.value;
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.d.ts b/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b30738264f40293497a2b67f1f8d657fa9943c3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.d.ts
@@ -0,0 +1,29 @@
+import type {Sequence} from './types';
+
+interface ForEachTrait<K, V> {
+  forEach(callback: (value: V, key: K, self: this) => void): void;
+}
+
+interface PlainObject<T> {
+  [key: string]: T;
+}
+
+export default function forEachWithNullKeys<V>(
+  iterable: Set<V>,
+  callback: (value: V, key: null) => void
+): void;
+
+export default function forEachWithNullKeys<K, V>(
+  iterable: ForEachTrait<K, V>,
+  callback: (value: V, key: K) => void
+): void;
+
+export default function forEachWithNullKeys<T>(
+  iterable: Iterator<T> | Iterable<T> | Sequence<T>,
+  callback: (item: T, key: null) => void
+): void;
+
+export default function forEachWithNullKeys<T>(
+  object: PlainObject<T>,
+  callback: (value: T, key: string) => void
+): void;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.js b/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.js
new file mode 100644
index 0000000000000000000000000000000000000000..97135add134c8529fcd1d1885b07626cd48255f5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/foreach-with-null-keys.js
@@ -0,0 +1,83 @@
+/**
+ * Obliterator ForEachWithNullKeys Function
+ * =========================================
+ *
+ * Helper function used to easily iterate over mixed values.
+ */
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+/**
+ * Same function as the `forEach` but will yield `null` when the target
+ * does not have keys.
+ *
+ * @param  {any}      iterable - Iterable value.
+ * @param  {function} callback - Callback function.
+ */
+module.exports = function forEachWithNullKeys(iterable, callback) {
+  var iterator, k, i, l, s;
+
+  if (!iterable)
+    throw new Error('obliterator/forEachWithNullKeys: invalid iterable.');
+
+  if (typeof callback !== 'function')
+    throw new Error('obliterator/forEachWithNullKeys: expecting a callback.');
+
+  // The target is an array or a string or function arguments
+  if (
+    Array.isArray(iterable) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(iterable)) ||
+    typeof iterable === 'string' ||
+    iterable.toString() === '[object Arguments]'
+  ) {
+    for (i = 0, l = iterable.length; i < l; i++) callback(iterable[i], null);
+    return;
+  }
+
+  // The target is a Set
+  if (iterable instanceof Set) {
+    iterable.forEach(function (value) {
+      callback(value, null);
+    });
+    return;
+  }
+
+  // The target has a #.forEach method
+  if (typeof iterable.forEach === 'function') {
+    iterable.forEach(callback);
+    return;
+  }
+
+  // The target is iterable
+  if (
+    SYMBOL_SUPPORT &&
+    Symbol.iterator in iterable &&
+    typeof iterable.next !== 'function'
+  ) {
+    iterable = iterable[Symbol.iterator]();
+  }
+
+  // The target is an iterator
+  if (typeof iterable.next === 'function') {
+    iterator = iterable;
+    i = 0;
+
+    while (((s = iterator.next()), s.done !== true)) {
+      callback(s.value, null);
+      i++;
+    }
+
+    return;
+  }
+
+  // The target is a plain object
+  for (k in iterable) {
+    if (iterable.hasOwnProperty(k)) {
+      callback(iterable[k], k);
+    }
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/foreach.d.ts b/libs/shared/graph-layout/node_modules/obliterator/foreach.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db0bac77e612ed92e8a7bac4b82c87656280fdf9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/foreach.d.ts
@@ -0,0 +1,24 @@
+import type {Sequence} from './types';
+
+interface ForEachTrait<K, V> {
+  forEach(callback: (value: V, key: K, self: this) => void): void;
+}
+
+interface PlainObject<T> {
+  [key: string]: T;
+}
+
+export default function forEach<K, V>(
+  iterable: ForEachTrait<K, V>,
+  callback: (value: V, key: K) => void
+): void;
+
+export default function forEach<T>(
+  iterable: Iterator<T> | Iterable<T> | Sequence<T>,
+  callback: (item: T, index: number) => void
+): void;
+
+export default function forEach<T>(
+  object: PlainObject<T>,
+  callback: (value: T, key: string) => void
+): void;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/foreach.js b/libs/shared/graph-layout/node_modules/obliterator/foreach.js
new file mode 100644
index 0000000000000000000000000000000000000000..84af94fe97a3f4b65368e024ac15dc396c33a70b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/foreach.js
@@ -0,0 +1,73 @@
+/**
+ * Obliterator ForEach Function
+ * =============================
+ *
+ * Helper function used to easily iterate over mixed values.
+ */
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+/**
+ * Function able to iterate over almost any iterable JS value.
+ *
+ * @param  {any}      iterable - Iterable value.
+ * @param  {function} callback - Callback function.
+ */
+module.exports = function forEach(iterable, callback) {
+  var iterator, k, i, l, s;
+
+  if (!iterable) throw new Error('obliterator/forEach: invalid iterable.');
+
+  if (typeof callback !== 'function')
+    throw new Error('obliterator/forEach: expecting a callback.');
+
+  // The target is an array or a string or function arguments
+  if (
+    Array.isArray(iterable) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(iterable)) ||
+    typeof iterable === 'string' ||
+    iterable.toString() === '[object Arguments]'
+  ) {
+    for (i = 0, l = iterable.length; i < l; i++) callback(iterable[i], i);
+    return;
+  }
+
+  // The target has a #.forEach method
+  if (typeof iterable.forEach === 'function') {
+    iterable.forEach(callback);
+    return;
+  }
+
+  // The target is iterable
+  if (
+    SYMBOL_SUPPORT &&
+    Symbol.iterator in iterable &&
+    typeof iterable.next !== 'function'
+  ) {
+    iterable = iterable[Symbol.iterator]();
+  }
+
+  // The target is an iterator
+  if (typeof iterable.next === 'function') {
+    iterator = iterable;
+    i = 0;
+
+    while (((s = iterator.next()), s.done !== true)) {
+      callback(s.value, i);
+      i++;
+    }
+
+    return;
+  }
+
+  // The target is a plain object
+  for (k in iterable) {
+    if (iterable.hasOwnProperty(k)) {
+      callback(iterable[k], k);
+    }
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/includes.d.ts b/libs/shared/graph-layout/node_modules/obliterator/includes.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1a8f9de2b39fc55cc9d46ab5b6c78fc8bbc27dce
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/includes.d.ts
@@ -0,0 +1,6 @@
+import type {IntoInterator} from './types';
+
+export default function includes<T>(
+  target: IntoInterator<T>,
+  value: T
+): boolean;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/includes.js b/libs/shared/graph-layout/node_modules/obliterator/includes.js
new file mode 100644
index 0000000000000000000000000000000000000000..01c788b577c5f259ae1b3aa27ef9bd599f69a79c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/includes.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Includes Function
+ * ==============================
+ *
+ * Function taking an iterable and returning whether the given item can be
+ * found in it.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Includes.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} value     - Searched value.
+ * @return {boolean}
+ */
+module.exports = function includes(iterable, value) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (step.value === value) return true;
+  }
+
+  return false;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/index.d.ts b/libs/shared/graph-layout/node_modules/obliterator/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a0371419f4d643b59eb9aec194a9786ac7e3966
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/index.d.ts
@@ -0,0 +1,20 @@
+export {default as Iterator} from './iterator';
+export {default as iter} from './iter';
+export {default as chain} from './chain';
+export {default as combinations} from './combinations';
+export {default as consume} from './consume';
+export {default as every} from './every';
+export {default as filter} from './filter';
+export {default as find} from './find';
+export {default as forEach} from './foreach';
+export {default as forEachWithNullKeys} from './foreach-with-null-keys';
+export {default as includes} from './includes';
+export {default as map} from './map';
+export {default as match} from './match';
+export {default as permutations} from './permutations';
+export {default as powerSet} from './power-set';
+export {default as range} from './range';
+export {default as some} from './some';
+export {default as split} from './split';
+export {default as take} from './take';
+export {default as takeInto} from './take-into';
diff --git a/libs/shared/graph-layout/node_modules/obliterator/index.js b/libs/shared/graph-layout/node_modules/obliterator/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a55393a5d32a88c1fb794d90cafed28e8ccef64c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/index.js
@@ -0,0 +1,26 @@
+/**
+ * Obliterator Library Endpoint
+ * =============================
+ *
+ * Exporting the library's functions.
+ */
+exports.Iterator = require('./iterator.js');
+exports.iter = require('./iter.js');
+exports.chain = require('./chain.js');
+exports.combinations = require('./combinations.js');
+exports.consume = require('./consume.js');
+exports.every = require('./every.js');
+exports.filter = require('./filter.js');
+exports.find = require('./find.js');
+exports.forEach = require('./foreach.js');
+exports.forEachWithNullKeys = require('./foreach-with-null-keys.js');
+exports.includes = require('./includes.js');
+exports.map = require('./map.js');
+exports.match = require('./match.js');
+exports.permutations = require('./permutations.js');
+exports.powerSet = require('./power-set.js');
+exports.range = require('./range.js');
+exports.some = require('./some.js');
+exports.split = require('./split.js');
+exports.take = require('./take.js');
+exports.takeInto = require('./take-into.js');
diff --git a/libs/shared/graph-layout/node_modules/obliterator/iter.d.ts b/libs/shared/graph-layout/node_modules/obliterator/iter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..55377454d702e299afd045bcd4718cf2ea47fc23
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/iter.d.ts
@@ -0,0 +1 @@
+export default function iter<T>(target: unknown): Iterator<T>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/iter.js b/libs/shared/graph-layout/node_modules/obliterator/iter.js
new file mode 100644
index 0000000000000000000000000000000000000000..12ab82f305a1ba077ddaaa616f3e653ac5ff5d85
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/iter.js
@@ -0,0 +1,46 @@
+/**
+ * Obliterator Iter Function
+ * ==========================
+ *
+ * Function coercing values to an iterator. It can be quite useful when needing
+ * to handle iterables and iterators the same way.
+ */
+var Iterator = require('./iterator.js');
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+function iterOrNull(target) {
+  // Indexed sequence
+  if (
+    typeof target === 'string' ||
+    Array.isArray(target) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(target))
+  )
+    return Iterator.fromSequence(target);
+
+  // Invalid value
+  if (typeof target !== 'object' || target === null) return null;
+
+  // Iterable
+  if (SYMBOL_SUPPORT && typeof target[Symbol.iterator] === 'function')
+    return target[Symbol.iterator]();
+
+  // Iterator duck-typing
+  if (typeof target.next === 'function') return target;
+
+  // Invalid object
+  return null;
+}
+
+module.exports = function iter(target) {
+  var iterator = iterOrNull(target);
+
+  if (!iterator)
+    throw new Error(
+      'obliterator: target is not iterable nor a valid iterator.'
+    );
+
+  return iterator;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/iterator.d.ts b/libs/shared/graph-layout/node_modules/obliterator/iterator.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8b10451a6be5f03adeea2c38e92f08f8109121a5
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/iterator.d.ts
@@ -0,0 +1,18 @@
+import type {Sequence} from './types';
+
+type NextFunction<V> = () => IteratorResult<V>;
+
+export default class ObliteratorIterator<V> implements IterableIterator<V> {
+  // Constructor
+  constructor(next: NextFunction<V>);
+
+  // Well-known methods
+  next(): IteratorResult<V>;
+  [Symbol.iterator](): IterableIterator<V>;
+
+  // Static methods
+  static of<T>(...args: T[]): ObliteratorIterator<T>;
+  static empty<T>(): ObliteratorIterator<T>;
+  static is(value: any): boolean;
+  static fromSequence<T>(sequence: Sequence<T>): ObliteratorIterator<T>;
+}
diff --git a/libs/shared/graph-layout/node_modules/obliterator/iterator.js b/libs/shared/graph-layout/node_modules/obliterator/iterator.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa241892460e89232c0e5e155845eaa2a537e0b9
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/iterator.js
@@ -0,0 +1,96 @@
+/**
+ * Obliterator Iterator Class
+ * ===========================
+ *
+ * Simple class representing the library's iterators.
+ */
+
+/**
+ * Iterator class.
+ *
+ * @constructor
+ * @param {function} next - Next function.
+ */
+function Iterator(next) {
+  if (typeof next !== 'function')
+    throw new Error('obliterator/iterator: expecting a function!');
+
+  this.next = next;
+}
+
+/**
+ * If symbols are supported, we add `next` to `Symbol.iterator`.
+ */
+if (typeof Symbol !== 'undefined')
+  Iterator.prototype[Symbol.iterator] = function () {
+    return this;
+  };
+
+/**
+ * Returning an iterator of the given values.
+ *
+ * @param  {any...} values - Values.
+ * @return {Iterator}
+ */
+Iterator.of = function () {
+  var args = arguments,
+    l = args.length,
+    i = 0;
+
+  return new Iterator(function () {
+    if (i >= l) return {done: true};
+
+    return {done: false, value: args[i++]};
+  });
+};
+
+/**
+ * Returning an empty iterator.
+ *
+ * @return {Iterator}
+ */
+Iterator.empty = function () {
+  var iterator = new Iterator(function () {
+    return {done: true};
+  });
+
+  return iterator;
+};
+
+/**
+ * Returning an iterator over the given indexed sequence.
+ *
+ * @param  {string|Array} sequence - Target sequence.
+ * @return {Iterator}
+ */
+Iterator.fromSequence = function (sequence) {
+  var i = 0,
+    l = sequence.length;
+
+  return new Iterator(function () {
+    if (i >= l) return {done: true};
+
+    return {done: false, value: sequence[i++]};
+  });
+};
+
+/**
+ * Returning whether the given value is an iterator.
+ *
+ * @param  {any} value - Value.
+ * @return {boolean}
+ */
+Iterator.is = function (value) {
+  if (value instanceof Iterator) return true;
+
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    typeof value.next === 'function'
+  );
+};
+
+/**
+ * Exporting.
+ */
+module.exports = Iterator;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/map.d.ts b/libs/shared/graph-layout/node_modules/obliterator/map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf087aae593614b1435bd1e7d24f362dad11d0d0
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/map.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type MapFunction<S, T> = (item: S) => T;
+
+export default function map<S, T>(
+  target: IntoInterator<S>,
+  predicate: MapFunction<S, T>
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/map.js b/libs/shared/graph-layout/node_modules/obliterator/map.js
new file mode 100644
index 0000000000000000000000000000000000000000..9efb94100fb98d35d257be42e45d4ed37d55a3cb
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/map.js
@@ -0,0 +1,29 @@
+/**
+ * Obliterator Map Function
+ * ===========================
+ *
+ * Function returning a iterator mapping the given iterator's values.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Map.
+ *
+ * @param  {Iterator} target - Target iterable.
+ * @param  {function} mapper - Map function.
+ * @return {Iterator}
+ */
+module.exports = function map(target, mapper) {
+  var iterator = iter(target);
+
+  return new Iterator(function next() {
+    var step = iterator.next();
+
+    if (step.done) return step;
+
+    return {
+      value: mapper(step.value)
+    };
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/match.d.ts b/libs/shared/graph-layout/node_modules/obliterator/match.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3ff382efdd69836f89c84530f8fda3f210a0ce4e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/match.d.ts
@@ -0,0 +1,4 @@
+export default function match(
+  pattern: RegExp,
+  string: string
+): IterableIterator<string>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/match.js b/libs/shared/graph-layout/node_modules/obliterator/match.js
new file mode 100644
index 0000000000000000000000000000000000000000..86c17fce555004920e6e1dd70b62c52daf761263
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/match.js
@@ -0,0 +1,43 @@
+/**
+ * Obliterator Match Function
+ * ===========================
+ *
+ * Function returning an iterator over the matches of the given regex on the
+ * target string.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Match.
+ *
+ * @param  {RegExp}   pattern - Regular expression to use.
+ * @param  {string}   string  - Target string.
+ * @return {Iterator}
+ */
+module.exports = function match(pattern, string) {
+  var executed = false;
+
+  if (!(pattern instanceof RegExp))
+    throw new Error(
+      'obliterator/match: invalid pattern. Expecting a regular expression.'
+    );
+
+  if (typeof string !== 'string')
+    throw new Error('obliterator/match: invalid target. Expecting a string.');
+
+  return new Iterator(function () {
+    if (executed && !pattern.global) {
+      pattern.lastIndex = 0;
+      return {done: true};
+    }
+
+    executed = true;
+
+    var m = pattern.exec(string);
+
+    if (m) return {value: m};
+
+    pattern.lastIndex = 0;
+    return {done: true};
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/package.json b/libs/shared/graph-layout/node_modules/obliterator/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c36cb964f8985191f503bcfd3b9de44eb42ff770
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/package.json
@@ -0,0 +1,46 @@
+{
+  "name": "obliterator",
+  "version": "2.0.2",
+  "description": "Higher order iterator library for JavaScript/TypeScript.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "scripts": {
+    "lint": "eslint *.js",
+    "prepublishOnly": "npm run lint && npm test",
+    "prettier": "prettier --write '*.js' '*.ts'",
+    "test": "mocha test.js && npm run test:types",
+    "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns ./test-types.ts"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/yomguithereal/obliterator.git"
+  },
+  "keywords": [
+    "iterator"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/yomguithereal/obliterator/issues"
+  },
+  "homepage": "https://github.com/yomguithereal/obliterator#readme",
+  "devDependencies": {
+    "@yomguithereal/eslint-config": "^4.4.0",
+    "@yomguithereal/prettier-config": "^1.2.0",
+    "eslint": "^8.2.0",
+    "eslint-config-prettier": "^8.3.0",
+    "mocha": "^9.1.3",
+    "prettier": "^2.4.1",
+    "typescript": "^4.4.4"
+  },
+  "eslintConfig": {
+    "extends": [
+      "@yomguithereal/eslint-config",
+      "eslint-config-prettier"
+    ]
+  },
+  "prettier": "@yomguithereal/prettier-config"
+}
diff --git a/libs/shared/graph-layout/node_modules/obliterator/permutations.d.ts b/libs/shared/graph-layout/node_modules/obliterator/permutations.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e8ef37b668b6261dd04e2845ea7161ccb3caf38
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/permutations.d.ts
@@ -0,0 +1,4 @@
+export default function permutations<T>(
+  array: Array<T>,
+  r: number
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/permutations.js b/libs/shared/graph-layout/node_modules/obliterator/permutations.js
new file mode 100644
index 0000000000000000000000000000000000000000..3410ca42c33dfc2a86720f913f4d8665659914dc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/permutations.js
@@ -0,0 +1,94 @@
+/**
+ * Obliterator Permutations Function
+ * ==================================
+ *
+ * Iterator returning permutations of the given array.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Helper mapping indices to items.
+ */
+function indicesToItems(target, items, indices, r) {
+  for (var i = 0; i < r; i++) target[i] = items[indices[i]];
+}
+
+/**
+ * Permutations.
+ *
+ * @param  {array}    array - Target array.
+ * @param  {number}   r     - Size of the subsequences.
+ * @return {Iterator}
+ */
+module.exports = function permutations(array, r) {
+  if (!Array.isArray(array))
+    throw new Error(
+      'obliterator/permutations: first argument should be an array.'
+    );
+
+  var n = array.length;
+
+  if (arguments.length < 2) r = n;
+
+  if (typeof r !== 'number')
+    throw new Error(
+      'obliterator/permutations: second argument should be omitted or a number.'
+    );
+
+  if (r > n)
+    throw new Error(
+      'obliterator/permutations: the size of the subsequences should not exceed the length of the array.'
+    );
+
+  var indices = new Uint32Array(n),
+    subsequence = new Array(r),
+    cycles = new Uint32Array(r),
+    first = true,
+    i;
+
+  for (i = 0; i < n; i++) {
+    indices[i] = i;
+
+    if (i < r) cycles[i] = n - i;
+  }
+
+  i = r;
+
+  return new Iterator(function next() {
+    if (first) {
+      first = false;
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    var tmp, j;
+
+    i--;
+
+    if (i < 0) return {done: true};
+
+    cycles[i]--;
+
+    if (cycles[i] === 0) {
+      tmp = indices[i];
+
+      for (j = i; j < n - 1; j++) indices[j] = indices[j + 1];
+
+      indices[n - 1] = tmp;
+
+      cycles[i] = n - i;
+      return next();
+    } else {
+      j = cycles[i];
+      tmp = indices[i];
+
+      indices[i] = indices[n - j];
+      indices[n - j] = tmp;
+
+      i = r;
+
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/power-set.d.ts b/libs/shared/graph-layout/node_modules/obliterator/power-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..254094ad73496279c862503e50510543b2f8def3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/power-set.d.ts
@@ -0,0 +1,3 @@
+export default function powerSet<T>(
+  array: Array<T>
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/power-set.js b/libs/shared/graph-layout/node_modules/obliterator/power-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd63f9574e15253d4c603e57b0d8df19d89ce39b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/power-set.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Power Set Function
+ * ===============================
+ *
+ * Iterator returning the power set of the given array.
+ */
+var Iterator = require('./iterator.js'),
+  combinations = require('./combinations.js'),
+  chain = require('./chain.js');
+
+/**
+ * Power set.
+ *
+ * @param  {array}    array - Target array.
+ * @return {Iterator}
+ */
+module.exports = function powerSet(array) {
+  var n = array.length;
+
+  var iterators = new Array(n + 1);
+
+  iterators[0] = Iterator.of([]);
+
+  for (var i = 1; i < n + 1; i++) iterators[i] = combinations(array, i);
+
+  return chain.apply(null, iterators);
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/range.d.ts b/libs/shared/graph-layout/node_modules/obliterator/range.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bff37072dda3f70bfa59b6c8743bb17638d314c3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/range.d.ts
@@ -0,0 +1,10 @@
+export default function range(end: number): IterableIterator<number>;
+export default function range(
+  start: number,
+  end: number
+): IterableIterator<number>;
+export default function range(
+  start: number,
+  end: number,
+  step: number
+): IterableIterator<number>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/range.js b/libs/shared/graph-layout/node_modules/obliterator/range.js
new file mode 100644
index 0000000000000000000000000000000000000000..b00f9f02baa2e72bf075d86dac59ac180ee7e7f4
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/range.js
@@ -0,0 +1,44 @@
+/**
+ * Obliterator Range Function
+ * ===========================
+ *
+ * Function returning a range iterator.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Range.
+ *
+ * @param  {number} start - Start.
+ * @param  {number} end   - End.
+ * @param  {number} step  - Step.
+ * @return {Iterator}
+ */
+module.exports = function range(start, end, step) {
+  if (arguments.length === 1) {
+    end = start;
+    start = 0;
+  }
+
+  if (arguments.length < 3) step = 1;
+
+  var i = start;
+
+  var iterator = new Iterator(function () {
+    if (i < end) {
+      var value = i;
+
+      i += step;
+
+      return {value: value, done: false};
+    }
+
+    return {done: true};
+  });
+
+  iterator.start = start;
+  iterator.end = end;
+  iterator.step = step;
+
+  return iterator;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/some.d.ts b/libs/shared/graph-layout/node_modules/obliterator/some.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cccd35f13461d4ee8a0bc3b0f7ae9c37823da20f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/some.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function some<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): boolean;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/some.js b/libs/shared/graph-layout/node_modules/obliterator/some.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7923ee45b938275fa9d612c2e348cd04418b430
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/some.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Some Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning whether a
+ * matching item can be found.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Some.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function some(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (predicate(step.value)) return true;
+  }
+
+  return false;
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/split.d.ts b/libs/shared/graph-layout/node_modules/obliterator/split.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f54145b482debf402c4cfe2a48754da3a590395
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/split.d.ts
@@ -0,0 +1,4 @@
+export default function split(
+  pattern: RegExp,
+  string: string
+): IterableIterator<string>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/split.js b/libs/shared/graph-layout/node_modules/obliterator/split.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a765c0e9b18e97cd335a921861c1c1d3fcc1f25
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/split.js
@@ -0,0 +1,68 @@
+/**
+ * Obliterator Split Function
+ * ===========================
+ *
+ * Function returning an iterator over the pieces of a regex split.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Function used to make the given pattern global.
+ *
+ * @param  {RegExp} pattern - Regular expression to make global.
+ * @return {RegExp}
+ */
+function makeGlobal(pattern) {
+  var flags = 'g';
+
+  if (pattern.multiline) flags += 'm';
+  if (pattern.ignoreCase) flags += 'i';
+  if (pattern.sticky) flags += 'y';
+  if (pattern.unicode) flags += 'u';
+
+  return new RegExp(pattern.source, flags);
+}
+
+/**
+ * Split.
+ *
+ * @param  {RegExp}   pattern - Regular expression to use.
+ * @param  {string}   string  - Target string.
+ * @return {Iterator}
+ */
+module.exports = function split(pattern, string) {
+  if (!(pattern instanceof RegExp))
+    throw new Error(
+      'obliterator/split: invalid pattern. Expecting a regular expression.'
+    );
+
+  if (typeof string !== 'string')
+    throw new Error('obliterator/split: invalid target. Expecting a string.');
+
+  // NOTE: cloning the pattern has a performance cost but side effects for not
+  // doing so might be worse.
+  pattern = makeGlobal(pattern);
+
+  var consumed = false,
+    current = 0;
+
+  return new Iterator(function () {
+    if (consumed) return {done: true};
+
+    var match = pattern.exec(string),
+      value,
+      length;
+
+    if (match) {
+      length = match.index + match[0].length;
+
+      value = string.slice(current, match.index);
+      current = length;
+    } else {
+      consumed = true;
+      value = string.slice(current);
+    }
+
+    return {value: value, done: false};
+  });
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/support.js b/libs/shared/graph-layout/node_modules/obliterator/support.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c00649aea5d906ab34fbb382fd37e7411ffc314
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/support.js
@@ -0,0 +1,2 @@
+exports.ARRAY_BUFFER_SUPPORT = typeof ArrayBuffer !== 'undefined';
+exports.SYMBOL_SUPPORT = typeof Symbol !== 'undefined';
diff --git a/libs/shared/graph-layout/node_modules/obliterator/take-into.d.ts b/libs/shared/graph-layout/node_modules/obliterator/take-into.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb1d4dbe16c98e2bf10dad362ccafa6a8a6e508a
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/take-into.d.ts
@@ -0,0 +1,9 @@
+import type {IntoInterator} from './types';
+
+// Requires a resolution of https://github.com/microsoft/TypeScript/issues/1213
+// export default function takeInto<C<~>, T>(ArrayClass: new <T>(n: number) => C<T>, iterator: Iterator<T>, n: number): C<T>;
+export default function takeInto<T>(
+  ArrayClass: new <T>(arrayLength: number) => T[],
+  iterator: IntoInterator<T>,
+  n: number
+): T[];
diff --git a/libs/shared/graph-layout/node_modules/obliterator/take-into.js b/libs/shared/graph-layout/node_modules/obliterator/take-into.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc2db1875d6a959e1b262c88ee22188abe2c8b16
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/take-into.js
@@ -0,0 +1,39 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Take Into Function
+ * ===============================
+ *
+ * Same as the take function but enables the user to select an array class
+ * in which to insert the retrieved values.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Take Into.
+ *
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {number}   n          - Number of items to take.
+ * @return {array}
+ */
+module.exports = function takeInto(ArrayClass, iterable, n) {
+  var array = new ArrayClass(n),
+    step,
+    i = 0;
+
+  var iterator = iter(iterable);
+
+  while (true) {
+    if (i === n) return array;
+
+    step = iterator.next();
+
+    if (step.done) {
+      if (i !== n) return array.slice(0, i);
+
+      return array;
+    }
+
+    array[i++] = step.value;
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/take.d.ts b/libs/shared/graph-layout/node_modules/obliterator/take.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..346084841dd5a3cf0bea789556830f1a09985160
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/take.d.ts
@@ -0,0 +1,6 @@
+import type {IntoInterator} from './types';
+
+export default function take<T>(
+  iterator: IntoInterator<T>,
+  n: number
+): Array<T>;
diff --git a/libs/shared/graph-layout/node_modules/obliterator/take.js b/libs/shared/graph-layout/node_modules/obliterator/take.js
new file mode 100644
index 0000000000000000000000000000000000000000..defe775aa642640ab5496c99d8bacecb11defa61
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/take.js
@@ -0,0 +1,39 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Take Function
+ * ==========================
+ *
+ * Function taking n or every value of the given iterator and returns them
+ * into an array.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Take.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {number}   [n]      - Optional number of items to take.
+ * @return {array}
+ */
+module.exports = function take(iterable, n) {
+  var l = arguments.length > 1 ? n : Infinity,
+    array = l !== Infinity ? new Array(l) : [],
+    step,
+    i = 0;
+
+  var iterator = iter(iterable);
+
+  while (true) {
+    if (i === l) return array;
+
+    step = iterator.next();
+
+    if (step.done) {
+      if (i !== n) array.length = i;
+
+      return array;
+    }
+
+    array[i++] = step.value;
+  }
+};
diff --git a/libs/shared/graph-layout/node_modules/obliterator/types.d.ts b/libs/shared/graph-layout/node_modules/obliterator/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be9cca50e67f1bfdebce4d015837394535f8358e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/obliterator/types.d.ts
@@ -0,0 +1,7 @@
+export interface Sequence<T> {
+  length: number;
+  slice(from: number, to?: number): Sequence<T>;
+  [index: number]: T;
+}
+
+export type IntoInterator<T> = Iterable<T> | Iterator<T> | Sequence<T>;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/LICENSE.txt b/libs/shared/graph-layout/node_modules/pandemonium/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ca37c96edeaf346cf7f3818ea001ab03f60e161b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/README.md b/libs/shared/graph-layout/node_modules/pandemonium/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2aa12dd9264cce192e25b93723f064b2c9ea6b59
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/README.md
@@ -0,0 +1,354 @@
+[![Build Status](https://travis-ci.org/Yomguithereal/pandemonium.svg)](https://travis-ci.org/Yomguithereal/pandemonium)
+
+# Pandemonium
+
+Pandemonium is a dead simple JavaScript library providing typical random-related functions such as `choice`, `sample` etc.
+
+The library also provides a way to create any of the available functions using a custom random source ([seedrandom](https://www.npmjs.com/package/seedrandom), for instance).
+
+# Installation
+
+```
+npm install --save pandemonium
+```
+
+# Usage
+
+## Summary
+
+* [choice](#choice)
+* [dangerousButPerformantSample](#dangerousbutperformantsample)
+* [naiveSample](#naivesample)
+* [random](#random)
+* [randomIndex](#randomindex)
+* [randomString](#randomstring)
+* [sample](#sample)
+* [sampleWithReplacements](#samplewithreplacements)
+* [shuffle](#shuffle)
+* [shuffleInPlace](#shuffleinplace)
+* [weightedChoice](#weightedchoice)
+* [weightedRandomIndex](#weightedrandomindex)
+
+## choice
+
+Function returning a random item from the given array.
+
+```js
+import choice from 'pandemonium/choice';
+// Or
+import {choice} from 'pandemonium';
+
+choice(['apple', 'orange', 'pear']);
+>>> 'orange'
+
+// To create your own function using custom RNG
+import {createChoice} from 'pandemonium/choice';
+
+const customChoice = createChoice(rng);
+```
+
+## dangerousButPerformantSample
+
+Function returning a sample of size `k` from the given array.
+
+This function runs in `O(k)` time & memory but is somewhat dangerous because it will mutate the given array while performing its Fisher-Yates shuffle before reverting the mutations at the end.
+
+```js
+import dangerousButPerformantSample from 'pandemonium/dangerous-but-performant-sample';
+// Or
+import {dangerousButPerformantSample} from 'pandemonium';
+
+dangerousButPerformantSample(2, ['apple', 'orange', 'pear', 'pineapple']);
+>>> ['apple', 'pear']
+
+// To create your own function using custom RNG
+import {createDangerousButPerformantSample} from 'pandemonium/dangerous-but-performant-sample';
+
+const customSample = createDangerousButPerformantSample(rng);
+```
+
+## naiveSample
+
+Function returning a sample of size `k` from the given array.
+
+This function works by keeping a `Set` of the already picked items and choosing a random item in the array until we have the desired `k` items.
+
+While it is a good pick for cases when `k` is little compared to the size of your array, this function will see its performance drop really fast when `k` becomes proportionally bigger.
+
+```js
+import naiveSample from 'pandemonium/naive-sample';
+// Or
+import {naiveSample} from 'pandemonium';
+
+naiveSample(2, ['apple', 'orange', 'pear', 'pineapple']);
+>>> ['apple', 'pear']
+
+// Alternatively, you can pass the array's length and get
+// a sample of indices back
+naiveSample(2, 4);
+>>> [0, 2]
+
+// To create your own function using custom RNG
+import {createNaiveSample} from 'pandemonium/naive-sample';
+
+const customSample = createNaiveSample(rng);
+```
+
+## random
+
+Function returning a random integer between given `a` & `b`.
+
+```js
+import random from 'pandemonium/random';
+// Or
+import {random} from 'pandemonium';
+
+random(3, 7);
+>>> 4
+
+// To create your own function using custom RNG
+import {createRandom} from 'pandemonium/random';
+
+const customRandom = createRandom(rng);
+```
+
+## randomIndex
+
+Function returning a random index of the given array.
+
+```js
+import randomIndex from 'pandemonium/random-index';
+// Or
+import {randomIndex} from 'pandemonium';
+
+randomIndex(['apple', 'orange', 'pear']);
+>>> 1
+
+// Alternatively, you can give the array's length instead
+randomIndex(3);
+>>> 2
+
+// To create your own function using custom RNG
+import {createRandomIndex} from 'pandemonium/random-index';
+
+const customRandomIndex = createRandomIndex(rng);
+```
+
+## randomString
+
+Function returning a random string.
+
+```js
+import randomString from 'pandemonium/random-string';
+// Or
+import {randomString} from 'pandemonium';
+
+// To generate a string of fixed length
+randomString(5);
+>>> 'gHepM'
+
+// To generate a string of variable length
+randomString(3, 7);
+>>> 'hySf3'
+
+// To create your own function using custom RNG
+import {createRandomString} from 'pandemonium/random-string';
+
+const customRandomString = createRandomString(rng);
+
+// If you need a custom alphabet
+const customRandomString = createRandomString(rng, 'ATGC');
+```
+
+## sample
+
+Function returning a sample of size `k` from the given array.
+
+This function uses a partial Fisher-Yates shuffle and runs therefore in `O(k)` time but requires `O(n)` memory.
+
+If you need faster sampling, check out [`dangerousButPerformantSample`](#dangerousbutperformantsample) or [`naiveSample`](#naivesample).
+
+```js
+import sample from 'pandemonium/sample';
+// Or
+import {sample} from 'pandemonium';
+
+sample(2, ['apple', 'orange', 'pear', 'pineapple']);
+>>> ['apple', 'pear']
+
+// To create your own function using custom RNG
+import {createSample} from 'pandemonium/sample';
+
+const customSample = createSample(rng);
+```
+
+## sampleWithReplacements
+
+Function returning a sample of size `k` with replacements from the given array. This prosaically means that an items from the array might occur several times in the resulting sample.
+
+The function runs in both `O(k)` time & space complexity.
+
+```js
+import sampleWithReplacements from 'pandemonium/sample-with-replacements';
+// Or
+import {sampleWithReplacements} from 'pandemonium';
+
+sampleWithReplacements(3, ['apple', 'orange', 'pear', 'pineapple']);
+>>> ['apple', 'pear', 'apple']
+
+// To create your own function using custom RNG
+import {createSampleWithReplacements} from 'pandemonium/sample-with-replacements';
+
+const customSample = createSampleWithReplacements(rng);
+```
+
+## shuffle
+
+Function returning a shuffled version of the given array using the Fisher-Yates algorithm.
+
+If what you need is to shuffle the original array in place, check out [`shuffleInPlace`](#shuffleinplace).
+
+```js
+import shuffle from 'pandemonium/shuffle';
+// Or
+import {shuffle} from 'pandemonium';
+
+shuffle(['apple', 'orange', 'pear', 'pineapple']);
+>>> ['pear', 'orange', 'apple', 'pineapple']
+
+// To create your own function using custom RNG
+import {createShuffle} from 'pandemonium/shuffle';
+
+const customShuffle = createShuffle(rng);
+```
+
+## shuffleInPlace
+
+Function shuffling the given array in place using the Fisher-Yates algorithm.
+
+```js
+import shuffleInPlace from 'pandemonium/shuffle-in-place';
+// Or
+import {shuffleInPlace} from 'pandemonium';
+
+const array = ['apple', 'orange', 'pear', 'pineapple'];
+shuffleInPlace(array);
+
+// Array was mutated:
+array
+>>> ['pear', 'orange', 'apple', 'pineapple']
+
+// To create your own function using custom RNG
+import {createShuffleInPlace} from 'pandemonium/shuffle-in-place';
+
+const customShuffleInPlace = createShuffleInPlace(rng);
+```
+
+## weightedChoice
+
+Function returning a random item from the given array of weights.
+
+Note that weights don't need to be relative.
+
+```js
+import weightedChoice from 'pandemonium/weighted-choice';
+// Or
+import {weightedChoice} from 'pandemonium';
+
+const array = [.1, .1, .4, .3, .1];
+weightedChoice(array);
+>>> .4
+
+// To create your own function using custom RNG
+import {createWeightedChoice} from 'pandemonium/weighted-choice';
+
+const customWeightedChoice = createWeightedChoice(rng);
+
+// If you have an array of objects
+const customWeightedChoice = createWeightedChoice({
+  rng: rng,
+  getWeight: (item, index) => {
+    return item.weight;
+  }
+});
+
+const array = [{fruit: 'pear', weight: 4}, {fruit: 'apple', weight: 30}];
+customWeightedChoice(array);
+>>> 'apple'
+
+
+// If you intent to call the function multiple times on the same array,
+// you should use the cached version instead:
+import {createCachedWeightedChoice} from 'pandemonium/weighted-choice';
+
+const array = [.1, .1, .4, .3, .1];
+const customWeightedChoice = createCachedWeightedChoice(rng, array);
+
+customWeightedChoice();
+>>> .3
+```
+
+## weightedRandomIndex
+
+Function returning a random index from the given array of weights.
+
+Note that weights don't need to be relative.
+
+```js
+import weightedRandomIndex from 'pandemonium/weighted-random-index';
+// Or
+import {weightedRandomIndex} from 'pandemonium';
+
+const array = [.1, .1, .4, .3, .1];
+weightedRandomIndex(array);
+>>> 2
+
+// To create your own function using custom RNG
+import {createWeightedRandomIndex} from 'pandemonium/weighted-random-index';
+
+const customWeightedRandomIndex = createWeightedRandomIndex(rng);
+
+// If you have an array of objects
+const customWeightedRandomIndex = createWeightedRandomIndex({
+  rng: rng,
+  getWeight: (item, index) => {
+    return item.weight;
+  }
+});
+
+const array = [{fruit: 'pear', weight: 4}, {fruit: 'apple', weight: 30}];
+customWeightedRandomIndex(array);
+>>> 1
+
+
+// If you intent to call the function multiple times on the same array,
+// you should use the cached version instead:
+import {createCachedWeightedRandomIndex} from 'pandemonium/weighted-random-index';
+
+const array = [.1, .1, .4, .3, .1];
+const customWeightedRandomIndex = createCachedWeightedRandomIndex(rng, array);
+
+customWeightedRandomIndex();
+>>> 3
+```
+
+
+# Contribution
+
+Contributions are obviously welcome. Please be sure to lint the code & add the relevant unit tests before submitting any PR.
+
+```
+git clone git@github.com:Yomguithereal/pandemonium.git
+cd pandemonium
+npm install
+
+# To lint the code
+npm run lint
+
+# To run the unit tests
+npm test
+```
+
+# License
+
+[MIT](LICENSE.txt)
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/choice.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/choice.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3aa4229c469d2f6b7f1594369b34f5d4c3785169
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/choice.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type ChoiceFunction<T> = (array: Array<T>) => T;
+
+declare const choice: {
+  <T>(array: Array<T>): T;
+  createChoice<T>(rng: RNGFunction): ChoiceFunction<T>;
+};
+
+export function createChoice<T>(rng: RNGFunction): ChoiceFunction<T>;
+
+export default choice;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/choice.js b/libs/shared/graph-layout/node_modules/pandemonium/choice.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6fa99fa3b71cd72c1edda196139fa5d628164bd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/choice.js
@@ -0,0 +1,38 @@
+/**
+ * Pandemonium Choice
+ * ===================
+ *
+ * Choice function.
+ */
+var createRandomIndex = require('./random-index.js').createRandomIndex;
+
+/**
+ * Creating a function returning a random item from the given array.
+ *
+ * @param  {function} rng - RNG function returning uniform random.
+ * @return {function}     - The created function.
+ */
+function createChoice(rng) {
+  var customRandomIndex = createRandomIndex(rng);
+
+  /**
+   * Random function.
+   *
+   * @param  {array}  array - Target array.
+   * @return {any}
+   */
+  return function(array) {
+    return array[customRandomIndex(array)];
+  };
+}
+
+/**
+ * Default choice using `Math.random`.
+ */
+var choice = createChoice(Math.random);
+
+/**
+ * Exporting.
+ */
+choice.createChoice = createChoice;
+module.exports = choice;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..86bc9692629b18a95a5206a4f666b71080c98df8
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type DangerousButPerformantSampleFunction<T> = (n: number, array: Array<T>) => Array<T>;
+
+declare const dangerousButPerformantSample: {
+  <T>(n: number, array: Array<T>): Array<T>;
+  createDangerousButPerformantSample<T>(rng: RNGFunction): DangerousButPerformantSampleFunction<T>;
+};
+
+export function createDangerousButPerformantSample<T>(rng: RNGFunction): DangerousButPerformantSampleFunction<T>;
+
+export default dangerousButPerformantSample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.js b/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.js
new file mode 100644
index 0000000000000000000000000000000000000000..de85525afa6f7a98e6d83a8bdaf6746de7a0c8e7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/dangerous-but-performant-sample.js
@@ -0,0 +1,70 @@
+/**
+ * Pandemonium Dangerous But Performant Sample
+ * ============================================
+ *
+ * Performant sampling function running in O(k) memory & time but mutating the
+ * given array before reversing the mutations at the end of the process.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Creating a function returning a sample of size n using the provided RNG.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createDangerousButPerformantSample(rng) {
+  var customRandom = createRandom(rng);
+
+  /**
+   * Function returning sample of size n from array.
+   *
+   * @param  {number} n        - Size of the sample.
+   * @param  {array}  sequence - Target sequence.
+   * @return {array}           - The random sample.
+   */
+  return function(n, sequence) {
+    var result = new Array(n),
+        swaps = new Array(n),
+        lastIndex = sequence.length - 1;
+
+    var index = -1,
+        value,
+        swap,
+        r;
+
+    while (++index < n) {
+      r = customRandom(index, lastIndex);
+      value = sequence[r];
+
+      sequence[r] = sequence[index];
+      sequence[index] = value;
+      result[index] = value;
+
+      // Storing the swap so we can reverse it
+      swaps[index] = r;
+    }
+
+    // Reversing the mutations
+    while (--index >= 0) {
+      swap = swaps[index];
+      value = sequence[index];
+
+      sequence[index] = sequence[swap];
+      sequence[swap] = value;
+    }
+
+    return result;
+  };
+}
+
+/**
+ * Default dangerous sample using `Math.random`.
+ */
+var dangerousButPerformantSample = createDangerousButPerformantSample(Math.random);
+
+/**
+ * Exporting.
+ */
+dangerousButPerformantSample.createDangerousButPerformantSample = createDangerousButPerformantSample;
+module.exports = dangerousButPerformantSample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/index.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d2a784f1f0b536b1b4e47c8c59ed8db955fecd3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/index.d.ts
@@ -0,0 +1,12 @@
+export {default as choice} from './choice.js';
+export {default as dangerousButPerformantSample} from './dangerous-but-performant-sample.js';
+export {default as naiveSample} from './naive-sample.js';
+export {default as random} from './random.js';
+export {default as randomIndex} from './random-index.js';
+export {default as randomString} from './random-string.js';
+export {default as sample} from './sample.js';
+export {default as sampleWithReplacements} from './sample-with-replacements.js';
+export {default as shuffle} from './shuffle.js';
+export {default as shuffleInPlace} from './shuffle-in-place.js';
+export {default as weightedChoice} from './weighted-choice.js';
+export {default as weightedRandomIndex} from './weighted-random-index.js';
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/index.js b/libs/shared/graph-layout/node_modules/pandemonium/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9f9dd5764bbaba139b287703df52ac54f18b755
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/index.js
@@ -0,0 +1,18 @@
+/**
+ * Pandemonium Library Endpoint
+ * =============================
+ *
+ * Exporting the library's functions.
+ */
+exports.choice = require('./choice.js');
+exports.dangerousButPerformantSample = require('./dangerous-but-performant-sample.js');
+exports.naiveSample = require('./naive-sample.js');
+exports.random = require('./random.js');
+exports.randomIndex = require('./random-index.js');
+exports.randomString = require('./random-string.js');
+exports.sample = require('./sample.js');
+exports.sampleWithReplacements = require('./sample-with-replacements.js');
+exports.shuffle = require('./shuffle.js');
+exports.shuffleInPlace = require('./shuffle-in-place.js');
+exports.weightedChoice = require('./weighted-choice.js');
+exports.weightedRandomIndex = require('./weighted-random-index.js');
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce870d19914d13ec5f5ba13f35d38feb54bcc95b
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type NaiveSampleFunction<T> = (n: number, array: Array<T>) => Array<T>;
+
+declare const naiveSample: {
+  <T>(n: number, array: Array<T>): Array<T>;
+  createNaiveSample<T>(rng: RNGFunction): NaiveSampleFunction<T>;
+};
+
+export function createNaiveSample<T>(rng: RNGFunction): NaiveSampleFunction<T>;
+
+export default naiveSample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.js b/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.js
new file mode 100644
index 0000000000000000000000000000000000000000..9978c52d3ae24f58d3840c60c321365e952bf3f1
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/naive-sample.js
@@ -0,0 +1,60 @@
+/**
+ * Pandemonium Naive Sample
+ * =========================
+ *
+ * Naive sampling function storing the already picked values in a Set.
+ * Performance of this function will decrease dramatically when `k` is a
+ * high proportion of `n`.
+ */
+var createRandomIndex = require('./random-index.js').createRandomIndex;
+
+/**
+ * Creating a function returning a sample of size n using the provided RNG.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createNaiveSample(rng) {
+  var customRandomIndex = createRandomIndex(rng);
+
+  /**
+   * Function returning sample of size n from array.
+   *
+   * @param  {number} n              - Size of the sample.
+   * @param  {array|number} sequence - Target sequence or its length.
+   * @return {array}                 - The random sample.
+   */
+  return function(n, sequence) {
+    var items = new Set(),
+        array = new Array(n),
+        size = 0,
+        index;
+
+    var needItems = typeof sequence !== 'number',
+        i = 0;
+
+    while (items.size < n) {
+      index = customRandomIndex(sequence);
+
+      items.add(index);
+
+      if (items.size > size) {
+        array[i++] = needItems ? sequence[index] : index;
+        size = items.size;
+      }
+    }
+
+    return array;
+  };
+}
+
+/**
+ * Default naive sample using `Math.random`.
+ */
+var naiveSample = createNaiveSample(Math.random);
+
+/**
+ * Exporting.
+ */
+naiveSample.createNaiveSample = createNaiveSample;
+module.exports = naiveSample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/package.json b/libs/shared/graph-layout/node_modules/pandemonium/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..7c7b87c422e45659c5f1ff3257845e5cc95b5064
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/package.json
@@ -0,0 +1,60 @@
+{
+  "name": "pandemonium",
+  "version": "1.5.0",
+  "description": "Typical random-related functions for JavaScript.",
+  "main": "index.js",
+  "scripts": {
+    "lint": "eslint *.js",
+    "prepublish": "npm run lint && npm test",
+    "test": "mocha && npm run test:types",
+    "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns ./test-types.ts"
+  },
+  "files": [
+    "*.d.ts",
+    "choice.js",
+    "dangerous-but-performant-sample.js",
+    "index.js",
+    "naive-sample.js",
+    "random-index.js",
+    "random-string.js",
+    "random.js",
+    "sample.js",
+    "sample-with-replacements.js",
+    "shuffle-in-place.js",
+    "shuffle.js",
+    "weighted-choice.js",
+    "weighted-random-index.js"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/yomguithereal/pandemonium.git"
+  },
+  "keywords": [
+    "random",
+    "choice",
+    "sample",
+    "shuffle"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/yomguithereal/pandemonium/issues"
+  },
+  "homepage": "https://github.com/yomguithereal/pandemonium#readme",
+  "devDependencies": {
+    "@yomguithereal/eslint-config": "^4.0.0",
+    "eslint": "^6.8.0",
+    "mocha": "^7.1.1",
+    "seedrandom": "^3.0.5",
+    "typescript": "^3.8.3"
+  },
+  "eslintConfig": {
+    "extends": "@yomguithereal/eslint-config",
+    "globals": {
+      "Set": true
+    }
+  }
+}
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random-index.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/random-index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..815e54200f15a4ab4dcc2865475538cbb0d2c2f3
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random-index.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type RandomIndexFunction<T> = (array: Array<T>) => number;
+
+declare const randomIndex: {
+  <T>(array: Array<T>): number;
+  createRandomIndex<T>(rng: RNGFunction): RandomIndexFunction<T>;
+};
+
+export function createRandomIndex<T>(rng: RNGFunction): RandomIndexFunction<T>;
+
+export default randomIndex;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random-index.js b/libs/shared/graph-layout/node_modules/pandemonium/random-index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8a0e23f467ae2bd5c08b3520f30440c74e1fcbf
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random-index.js
@@ -0,0 +1,41 @@
+/**
+ * Pandemonium Random Index
+ * =========================
+ *
+ * Random index function.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Creating a function returning a random index from the given array.
+ *
+ * @param  {function} rng - RNG function returning uniform random.
+ * @return {function}     - The created function.
+ */
+function createRandomIndex(rng) {
+  var customRandom = createRandom(rng);
+
+  /**
+   * Random function.
+   *
+   * @param  {array|number}  array - Target array or length of the array.
+   * @return {number}
+   */
+  return function(length) {
+    if (typeof length !== 'number')
+      length = length.length;
+
+    return customRandom(0, length - 1);
+  };
+}
+
+/**
+ * Default random index using `Math.random`.
+ */
+var randomIndex = createRandomIndex(Math.random);
+
+/**
+ * Exporting.
+ */
+randomIndex.createRandomIndex = createRandomIndex;
+module.exports = randomIndex;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random-string.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/random-string.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a061ad127efdd9f92596f0007b02110c64cf1668
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random-string.d.ts
@@ -0,0 +1,13 @@
+import {RNGFunction} from './types';
+
+type Alphabet = Array<string> | string;
+type RandomStringFunction = (minLength: number, maxLength?: number) => string;
+
+declare const randomString: {
+  (minLength: number, maxLength?: number): string;
+  createRandomString(rng: RNGFunction, alphabet?: Alphabet): RandomStringFunction;
+};
+
+export function createRandomString(rng: RNGFunction, alphabet?: Alphabet): RandomStringFunction;
+
+export default randomString;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random-string.js b/libs/shared/graph-layout/node_modules/pandemonium/random-string.js
new file mode 100644
index 0000000000000000000000000000000000000000..1197ec3f11c841ff936f7659c27252a21ebefd93
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random-string.js
@@ -0,0 +1,64 @@
+/**
+ * Pandemonium Random String
+ * ==========================
+ *
+ * Function generating random strings.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Constants.
+ */
+var DEFAULT_ALPHABET = (
+  'abcdefghijklmnopqrstuvwxyz' +
+  'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
+  '0123456789'
+);
+
+/**
+ * Creating a function returning a random string composed from characters of
+ * the given alphabet.
+ *
+ * @param  {function}     rng      - RNG function returning uniform random.
+ * @param  {string|array} alphabet - Target alphabet.
+ * @return {function}              - The created function.
+ */
+function createRandomString(rng, alphabet) {
+  if (!alphabet)
+    alphabet = DEFAULT_ALPHABET;
+
+  var customRandom = createRandom(rng),
+      randomCharacterIndex = customRandom.bind(null, 0, alphabet.length - 1);
+
+  /**
+   * Random string function.
+   *
+   * @param  {number} length - Desired string length.
+   * @return {number}
+   */
+  return function(length) {
+    if (arguments.length > 1) {
+
+      // We want to generate a string of variable length
+      length = customRandom(arguments[0], arguments[1]);
+    }
+
+    var characters = new Array(length);
+
+    for (var i = 0; i < length; i++)
+      characters[i] = alphabet[randomCharacterIndex()];
+
+    return characters.join('');
+  };
+}
+
+/**
+ * Default random string using `Math.random`.
+ */
+var randomString = createRandomString(Math.random);
+
+/**
+ * Exporting.
+ */
+randomString.createRandomString = createRandomString;
+module.exports = randomString;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/random.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a5e779d4bd54bfa1a406f289100246bb6fef810
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type RandomFunction = (min: number, max: number) => number;
+
+declare const random: {
+  (min: number, max: number): number;
+  createRandom(rng: RNGFunction): RandomFunction;
+};
+
+export function createRandom(rng: RNGFunction): RandomFunction;
+
+export default random;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/random.js b/libs/shared/graph-layout/node_modules/pandemonium/random.js
new file mode 100644
index 0000000000000000000000000000000000000000..24c80742c64c53ae93d08803ec6bc4edf342c413
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/random.js
@@ -0,0 +1,37 @@
+/**
+ * Pandemonium Random
+ * ===================
+ *
+ * Random function.
+ */
+
+/**
+ * Creating a function returning a random integer such as a <= N <= b.
+ *
+ * @param  {function} rng - RNG function returning uniform random.
+ * @return {function}     - The created function.
+ */
+function createRandom(rng) {
+
+  /**
+   * Random function.
+   *
+   * @param  {number} a - From.
+   * @param  {number} b - To.
+   * @return {number}
+   */
+  return function(a, b) {
+    return a + Math.floor(rng() * (b - a + 1));
+  };
+}
+
+/**
+ * Default random using `Math.random`.
+ */
+var random = createRandom(Math.random);
+
+/**
+ * Exporting.
+ */
+random.createRandom = createRandom;
+module.exports = random;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3bfc319ebba3f10f01f549593dde6f28affeb431
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type SampleWithReplacementsFunction<T> = (n: number, array: Array<T>) => Array<T>;
+
+declare const naiveSampleWithReplacements: {
+  <T>(n: number, array: Array<T>): Array<T>;
+  createSampleWithReplacements<T>(rng: RNGFunction): SampleWithReplacementsFunction<T>;
+};
+
+export function createSampleWithReplacements<T>(rng: RNGFunction): SampleWithReplacementsFunction<T>;
+
+export default naiveSampleWithReplacements;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.js b/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.js
new file mode 100644
index 0000000000000000000000000000000000000000..4482f03077dd30dcef7cdd8d462170dac7972bcd
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/sample-with-replacements.js
@@ -0,0 +1,51 @@
+/**
+ * Pandemonium Sample With Replacements
+ * =====================================
+ *
+ * Straightforward sampling function that allows an item to exist multiple
+ * times in the resulting sample.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Creating a function returning a sample of size n with replacements
+ * using the provided RNG.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createSampleWithReplacements(rng) {
+  var customRandom = createRandom(rng);
+
+  /**
+   * Function returning sample of size n from array with replacements.
+   *
+   * @param  {number} n        - Size of the sample.
+   * @param  {array}  sequence - Target sequence.
+   * @return {array}           - The random sample.
+   */
+  return function(n, sequence) {
+    var sample = new Array(n),
+        m = sequence.length - 1,
+        i,
+        r;
+
+    for (i = 0; i < n; i++) {
+      r = customRandom(0, m);
+      sample[i] = sequence[r];
+    }
+
+    return sample;
+  };
+}
+
+/**
+ * Default sample with replacements using `Math.random`.
+ */
+var sampleWithReplacements = createSampleWithReplacements(Math.random);
+
+/**
+ * Exporting.
+ */
+sampleWithReplacements.createSampleWithReplacements = createSampleWithReplacements;
+module.exports = sampleWithReplacements;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/sample.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/sample.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5cca84a6b29e8ebaee26b1689f2154fc394588d7
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/sample.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type SampleFunction<T> = (n: number, array: Array<T>) => Array<T>;
+
+declare const sample: {
+  <T>(n: number, array: Array<T>): Array<T>;
+  createSample<T>(rng: RNGFunction): SampleFunction<T>;
+};
+
+export function createSample<T>(rng: RNGFunction): SampleFunction<T>;
+
+export default sample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/sample.js b/libs/shared/graph-layout/node_modules/pandemonium/sample.js
new file mode 100644
index 0000000000000000000000000000000000000000..a95e7f15b2433a41b70dcd5604ab21acab7f49f2
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/sample.js
@@ -0,0 +1,56 @@
+/**
+ * Pandemonium Sample
+ * ===================
+ *
+ * Sample function using `k` iterations of the Fisher-Yates over a copy of the
+ * provided array.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Creating a function returning a sample of size n using the provided RNG.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createSample(rng) {
+  var customRandom = createRandom(rng);
+
+  /**
+   * Function returning sample of size n from array.
+   *
+   * @param  {number} n        - Size of the sample.
+   * @param  {array}  sequence - Target sequence.
+   * @return {array}           - The random sample.
+   */
+  return function(n, sequence) {
+    var result = sequence.slice(),
+        lastIndex = result.length - 1;
+
+    var index = -1;
+
+    while (++index < n) {
+      var r = customRandom(index, lastIndex),
+          value = result[r];
+
+      result[r] = result[index];
+      result[index] = value;
+    }
+
+    // Clamping the array
+    result.length = n;
+
+    return result;
+  };
+}
+
+/**
+ * Default sample using `Math.random`.
+ */
+var sample = createSample(Math.random);
+
+/**
+ * Exporting.
+ */
+sample.createSample = createSample;
+module.exports = sample;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cdae50391be3f5c075d79f4cb2273e0183988e29
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type ShuffleInPlaceFunction<T> = (array: Array<T>) => void;
+
+declare const shuffleInPlace: {
+  <T>(array: Array<T>): void;
+  createShuffleInPlace<T>(rng: RNGFunction): ShuffleInPlaceFunction<T>;
+};
+
+export function createShuffleInPlace<T>(rng: RNGFunction): ShuffleInPlaceFunction<T>;
+
+export default shuffleInPlace;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.js b/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4d4bff362654eac60da7543bfdc95d1cd92036f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/shuffle-in-place.js
@@ -0,0 +1,49 @@
+/**
+ * Pandemonium Shuffle In Place
+ * =============================
+ *
+ * Shuffle function applying the Fisher-Yates algorithm to the provided array.
+ */
+var createRandom = require('./random.js').createRandom;
+
+/**
+ * Creating a function returning the given array shuffled.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createShuffleInPlace(rng) {
+  var customRandom = createRandom(rng);
+
+  /**
+   * Function returning the shuffled array.
+   *
+   * @param  {array}  sequence - Target sequence.
+   * @return {array}           - The shuffled sequence.
+   */
+  return function(sequence) {
+    var length = sequence.length,
+        lastIndex = length - 1;
+
+    var index = -1;
+
+    while (++index < length) {
+      var r = customRandom(index, lastIndex),
+          value = sequence[r];
+
+      sequence[r] = sequence[index];
+      sequence[index] = value;
+    }
+  };
+}
+
+/**
+ * Default shuffle in place using `Math.random`.
+ */
+var shuffleInPlace = createShuffleInPlace(Math.random);
+
+/**
+ * Exporting.
+ */
+shuffleInPlace.createShuffleInPlace = createShuffleInPlace;
+module.exports = shuffleInPlace;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/shuffle.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/shuffle.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c54f44d9d1f28f4322e94deeab9eec0222e8a87e
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/shuffle.d.ts
@@ -0,0 +1,12 @@
+import {RNGFunction} from './types';
+
+type ShuffleFunction<T> = (array: Array<T>) => Array<T>;
+
+declare const shuffle: {
+  <T>(array: Array<T>): Array<T>;
+  createShuffle<T>(rng: RNGFunction): ShuffleFunction<T>;
+};
+
+export function createShuffle<T>(rng: RNGFunction): ShuffleFunction<T>;
+
+export default shuffle;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/shuffle.js b/libs/shared/graph-layout/node_modules/pandemonium/shuffle.js
new file mode 100644
index 0000000000000000000000000000000000000000..16a99150389b94bfd2d361395cc02a1d288be954
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/shuffle.js
@@ -0,0 +1,39 @@
+/**
+ * Pandemonium Shuffle
+ * ====================
+ *
+ * Shuffle function which is basically just applying the Fisher-Yates sampling
+ * function over the whole array.
+ */
+var createSample = require('./sample.js').createSample;
+
+/**
+ * Creating a function returning the given array shuffled.
+ *
+ * @param  {function} rng - The RNG to use.
+ * @return {function}     - The created function.
+ */
+function createShuffle(rng) {
+  var customSample = createSample(rng);
+
+  /**
+   * Function returning the shuffled array.
+   *
+   * @param  {array}  sequence - Target sequence.
+   * @return {array}           - The shuffled sequence.
+   */
+  return function(sequence) {
+    return customSample(sequence.length, sequence);
+  };
+}
+
+/**
+ * Default shuffle using `Math.random`.
+ */
+var shuffle = createShuffle(Math.random);
+
+/**
+ * Exporting.
+ */
+shuffle.createShuffle = createShuffle;
+module.exports = shuffle;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/types.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4eba78290d721a2d2ad9e981be3f7374f9f8b350
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/types.d.ts
@@ -0,0 +1 @@
+export type RNGFunction = () => number;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb45300a3bc36903e5c195c2495268963a212b7f
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.d.ts
@@ -0,0 +1,27 @@
+import {RNGFunction} from './types';
+
+type WeightedChoiceOptions<T> = {
+  rng: RNGFunction,
+  getWeight: (item: T, index?: number) => number
+};
+
+type WeightedChoiceFunction<T> = (array: Array<T>) => T;
+type CachedWeightedChoiceFunction<T> = () => T;
+
+declare const weightedChoice: {
+  <T>(array: Array<T>): T;
+
+  createWeightedChoice<T>(rng: RNGFunction): WeightedChoiceFunction<T>;
+  createWeightedChoice<T>(options: WeightedChoiceOptions<T>): WeightedChoiceFunction<T>;
+
+  createCachedWeightedChoice<T>(rng: RNGFunction, array: Array<T>): CachedWeightedChoiceFunction<T>;
+  createCachedWeightedChoice<T>(options: WeightedChoiceOptions<T>, array: Array<T>): CachedWeightedChoiceFunction<T>;
+};
+
+export function createWeightedChoice<T>(rng: RNGFunction): WeightedChoiceFunction<T>;
+export function createWeightedChoice<T>(options: WeightedChoiceFunction<T>): WeightedChoiceFunction<T>;
+
+export function createCachedWeightedChoice<T>(rng: RNGFunction, array: Array<T>): CachedWeightedChoiceFunction<T>;
+export function createCachedWeightedChoice<T>(options: WeightedChoiceOptions<T>, array: Array<T>): CachedWeightedChoiceFunction<T>;
+
+export default weightedChoice;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.js b/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.js
new file mode 100644
index 0000000000000000000000000000000000000000..81a8d6e4821885a5efb09bbac2234c4ca4a23a06
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/weighted-choice.js
@@ -0,0 +1,67 @@
+/**
+ * Pandemonium Weighted Choice
+ * ============================
+ *
+ * Function returning a random item from a weighted list.
+ */
+var lib = require('./weighted-random-index.js');
+
+/**
+ * Creating a function returning a weighted random item from a cached
+ * cumulative density function.
+ *
+ * @param  {object|function} rngOrOptions - Either RNG function or options:
+ * @param  {function}          rng        - Custom RNG.
+ * @param  {function}          getWeight  - Weight getter.
+ * @return {function}
+ */
+function createCachedWeightedChoice(rngOrOptions, sequence) {
+  var randomIndex = lib.createCachedWeightedRandomIndex(rngOrOptions, sequence);
+
+  /**
+   * Weighted random item from the given sequence.
+   *
+   * @return {number}
+   */
+  return function() {
+    var index = randomIndex();
+
+    return sequence[index];
+  };
+}
+
+/**
+ * Creating a function returning a weighted random item.
+ *
+ * @param  {object|function} rngOrOptions - Either RNG function or options:
+ * @param  {function}          rng        - Custom RNG.
+ * @param  {function}          getWeight  - Weight getter.
+ * @return {function}
+ */
+function createWeightedChoice(rngOrOptions) {
+  var randomIndex = lib.createWeightedRandomIndex(rngOrOptions);
+
+  /**
+   * Weighted random item from the given sequence.
+   *
+   * @param  {array} sequence - Target sequence.
+   * @return {number}
+   */
+  return function(sequence) {
+    var index = randomIndex(sequence);
+
+    return sequence[index];
+  };
+}
+
+/**
+ * Default weighted choice using `Math.random`.
+ */
+var weightedChoice = createWeightedChoice(Math.random);
+
+/**
+ * Exporting.
+ */
+weightedChoice.createCachedWeightedChoice = createCachedWeightedChoice;
+weightedChoice.createWeightedChoice = createWeightedChoice;
+module.exports = weightedChoice;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.d.ts b/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf6b3ac377b51e825eb3c1d32fa266e7f1f239fc
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.d.ts
@@ -0,0 +1,27 @@
+import {RNGFunction} from './types';
+
+type WeightedRandomIndexOptions<T> = {
+  rng: RNGFunction,
+  getWeight: (item: T, index?: number) => number
+};
+
+type WeightedRandomIndexFunction<T> = (array: Array<T>) => number;
+type CachedWeightedRandomIndexFunction = () => number;
+
+declare const weightedRandomIndex: {
+  <T>(array: Array<T>): number;
+
+  createWeightedRandomIndex<T>(rng: RNGFunction): WeightedRandomIndexFunction<T>;
+  createWeightedRandomIndex<T>(options: WeightedRandomIndexOptions<T>): WeightedRandomIndexFunction<T>;
+
+  createCachedWeightedRandomIndex<T>(rng: RNGFunction, array: Array<T>): CachedWeightedRandomIndexFunction;
+  createCachedWeightedRandomIndex<T>(options: WeightedRandomIndexOptions<T>, array: Array<T>): CachedWeightedRandomIndexFunction;
+};
+
+export function createWeightedRandomIndex<T>(rng: RNGFunction): WeightedRandomIndexFunction<T>;
+export function createWeightedRandomIndex<T>(options: WeightedRandomIndexFunction<T>): WeightedRandomIndexFunction<T>;
+
+export function createCachedWeightedRandomIndex<T>(rng: RNGFunction, array: Array<T>): CachedWeightedRandomIndexFunction;
+export function createCachedWeightedRandomIndex<T>(options: WeightedRandomIndexOptions<T>, array: Array<T>): CachedWeightedRandomIndexFunction;
+
+export default weightedRandomIndex;
diff --git a/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.js b/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ae87d301da3cea9002135010b868c1c8ddf364c
--- /dev/null
+++ b/libs/shared/graph-layout/node_modules/pandemonium/weighted-random-index.js
@@ -0,0 +1,168 @@
+/**
+ * Pandemonium Weighted Index
+ * ===========================
+ *
+ * Function returning a random index from a weighted list of items.
+ *
+ * [Reference]:
+ * http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python
+ */
+
+/**
+ * Defaults.
+ */
+var DEFAULTS = {
+  rng: Math.random,
+  getWeight: null
+};
+
+/**
+ * Function returning upper bound of value in the given sorted array. This
+ * is the equivalent of python's `bisect_right`.
+ *
+ * @param  {array}  array - Target array.
+ * @param  {number} value - Number to position.
+ * @return {number}
+ */
+function upperBound(array, value) {
+  var l = array.length,
+      d,
+      c,
+      i = 0;
+
+  while (l) {
+    d = l >>> 1;
+    c = i + d;
+
+    if (value < array[c]) {
+      l = d;
+    }
+    else {
+      i = c + 1;
+      l -= d + 1;
+    }
+  }
+
+  return i;
+}
+
+/**
+ * Creating a function returning a weighted random index from a cached
+ * cumulative density function.
+ *
+ * This algorithm is more costly in space because it needs to store O(n)
+ * items to cache the CDF but is way faster if one is going to need a random
+ * weighted index several times from the same sequence.
+ *
+ * @param  {object|function} rngOrOptions - Either RNG function or options:
+ * @param  {function}          rng        - Custom RNG.
+ * @param  {function}          getWeight  - Weight getter.
+ * @return {function}
+ */
+function createCachedWeightedRandomIndex(rngOrOptions, sequence) {
+  var rng,
+      options;
+
+  if (typeof rngOrOptions === 'function') {
+    rng = rngOrOptions;
+    options = {};
+  }
+  else {
+    rng = rngOrOptions.rng || DEFAULTS.rng;
+    options = rngOrOptions;
+  }
+
+  var getWeight = typeof options.getWeight === 'function' ?
+    options.getWeight :
+    null;
+
+  // Computing the cumulative density function of the sequence (CDF)
+  var l = sequence.length;
+
+  var CDF = new Array(l),
+      total = 0,
+      weight;
+
+  for (var i = 0; i < l; i++) {
+    weight = getWeight ? getWeight(sequence[i], i) : sequence[i];
+    total += weight;
+    CDF[i] = total;
+  }
+
+  /**
+   * Weighted random index from the given sequence.
+   *
+   * @return {number}
+   */
+  return function() {
+    var random = rng() * total;
+
+    return upperBound(CDF, random);
+  };
+}
+
+/**
+ * Creating a function returning a weighted random index.
+ *
+ * Note that this function uses the "King of the hill" algorithm which runs in
+ * linear time O(n) and has some advantages, one being that one does not need
+ * to know the weight's sum in advance. However, it may be slower that some
+ * other methods because we have to run the RNG function n times.
+ *
+ * @param  {object|function} rngOrOptions - Either RNG function or options:
+ * @param  {function}          rng        - Custom RNG.
+ * @param  {function}          getWeight  - Weight getter.
+ * @return {function}
+ */
+function createWeightedRandomIndex(rngOrOptions) {
+  var rng,
+      options;
+
+  if (typeof rngOrOptions === 'function') {
+    rng = rngOrOptions;
+    options = {};
+  }
+  else {
+    rng = rngOrOptions.rng || DEFAULTS.rng;
+    options = rngOrOptions;
+  }
+
+  var getWeight = typeof options.getWeight === 'function' ?
+    options.getWeight :
+    null;
+
+  /**
+   * Weighted random index from the given sequence.
+   *
+   * @param  {array} sequence - Target sequence.
+   * @return {number}
+   */
+  return function(sequence) {
+    var total = 0,
+        winner = 0,
+        weight;
+
+    for (var i = 0, l = sequence.length; i < l; i++) {
+      weight = getWeight ? getWeight(sequence[i], i) : sequence[i];
+
+      total += weight;
+
+      if (rng() * total < weight)
+        winner = i;
+    }
+
+    return winner;
+  };
+}
+
+/**
+ * Default weighted index using `Math.random`.
+ */
+var weightedRandomIndex = createWeightedRandomIndex(Math.random);
+
+/**
+ * Exporting.
+ */
+weightedRandomIndex.createCachedWeightedRandomIndex = createCachedWeightedRandomIndex;
+weightedRandomIndex.createWeightedRandomIndex = createWeightedRandomIndex;
+module.exports = weightedRandomIndex;
diff --git a/libs/shared/graph-layout/package.json b/libs/shared/graph-layout/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..1c00f487d0cd02e441e94f31a14178242e3378dc
--- /dev/null
+++ b/libs/shared/graph-layout/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "@graphpolaris/graph-layout",
+  "version": "0.0.1",
+  "license": "MIT",
+  "dependencies": {
+    "graphology": "^0.24.1",
+    "graphology-layout": "^0.5.0",
+    "graphology-layout-forceatlas2": "^0.8.2",
+    "graphology-layout-noverlap": "^0.4.2"
+  },
+  "devDependencies": {
+    "graphology-generators": "^0.11.2"
+  }
+}
diff --git a/libs/shared/graph-layout/project.json b/libs/shared/graph-layout/project.json
new file mode 100644
index 0000000000000000000000000000000000000000..2dff301f3f3d902bcd8817ad261a8213e573c2ba
--- /dev/null
+++ b/libs/shared/graph-layout/project.json
@@ -0,0 +1,43 @@
+{
+  "root": "libs/shared/graph-layout",
+  "sourceRoot": "libs/shared/graph-layout/src",
+  "projectType": "library",
+  "tags": [],
+  "targets": {
+    "build": {
+      "executor": "@nrwl/web:rollup",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/libs/shared/graph-layout",
+        "tsConfig": "libs/shared/graph-layout/tsconfig.lib.json",
+        "project": "libs/shared/graph-layout/package.json",
+        "entryFile": "libs/shared/graph-layout/src/index.ts",
+        "external": ["react/jsx-runtime"],
+        "rollupConfig": "@nrwl/react/plugins/bundle-rollup",
+        "compiler": "babel",
+        "assets": [
+          {
+            "glob": "libs/shared/graph-layout/README.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["libs/shared/graph-layout/**/*.{ts,tsx,js,jsx}"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/libs/shared/graph-layout"],
+      "options": {
+        "jestConfig": "libs/shared/graph-layout/jest.config.js",
+        "passWithNoTests": true
+      }
+    }
+  }
+}
diff --git a/libs/shared/graph-layout/src/index.ts b/libs/shared/graph-layout/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44fceae06340cc2c2a2c2bf99a09919cf9e3698c
--- /dev/null
+++ b/libs/shared/graph-layout/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/shared-graph-layout';
diff --git a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..325377b8fe541507a00785962dee9fb9c3cc3ef5
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts
@@ -0,0 +1,54 @@
+import { Layout } from './layout';
+import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase';
+
+export type CytoscapeProvider = 'Cytoscape';
+
+export type CytoscapeLayoutAlgorithms =
+  | `${CytoscapeProvider}_coupe`
+  | `${CytoscapeProvider}_i4`;
+
+/**
+ * This is a ConcreteCreator
+ */
+export class CytoscapeFactory
+  implements ILayoutFactory<CytoscapeLayoutAlgorithms>
+{
+  createLayout(LayoutAlgorithm: CytoscapeLayoutAlgorithms): Cytoscape | null {
+    switch (LayoutAlgorithm) {
+      case 'Cytoscape_coupe':
+        return new CytoscapeCoupe();
+      case 'Cytoscape_i4':
+        return new CytoscapeI4();
+      default:
+        return null;
+    }
+  }
+}
+
+export abstract class Cytoscape extends Layout<CytoscapeProvider> {
+  constructor(public override algorithm: LayoutAlgorithm<CytoscapeProvider>) {
+    super('Cytoscape', algorithm);
+  }
+
+  public specialCytoscapeFunction() {
+    console.log('Only Cytoscape Layouts can do this.');
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+class CytoscapeI4 extends Cytoscape {
+  constructor() {
+    super('Cytoscape_i4');
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+class CytoscapeCoupe extends Cytoscape {
+  constructor() {
+    super('Cytoscape_coupe');
+  }
+}
diff --git a/libs/shared/graph-layout/src/lib/graphology-layouts.ts b/libs/shared/graph-layout/src/lib/graphology-layouts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..686648f1392def27b889501e6d7ab2d9267ead1e
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/graphology-layouts.ts
@@ -0,0 +1,169 @@
+import Graph from 'graphology';
+import { circular, random } from 'graphology-layout';
+import forceAtlas2, {
+  ForceAtlas2Settings,
+} from 'graphology-layout-forceatlas2';
+import noverlap from 'graphology-layout-noverlap';
+import { RandomLayoutOptions } from 'graphology-layout/random';
+import { NoverlapSettings } from 'graphology-library/layout-noverlap';
+import { Attributes } from 'graphology-types';
+import { Layout } from './layout';
+import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase';
+
+export type GraphologyProvider = 'Graphology';
+
+export type GraphologyLayoutAlgorithms =
+  | `${GraphologyProvider}_circular`
+  | `${GraphologyProvider}_random`
+  | `${GraphologyProvider}_noverlap`
+  | `${GraphologyProvider}_forceAtlas2`;
+
+/**
+ * This is the Graphology Constructor for the main layouts available at
+ * https://graphology.github.io/
+ */
+export class GraphologyFactory
+  implements ILayoutFactory<GraphologyLayoutAlgorithms>
+{
+  createLayout(layoutAlgorithm: GraphologyLayoutAlgorithms): Graphology | null {
+    switch (layoutAlgorithm) {
+      case 'Graphology_random':
+        return new GraphologyRandom();
+      case 'Graphology_circular':
+        return new GraphologyCircular();
+      case 'Graphology_noverlap':
+        return new GraphologyNoverlap();
+      case 'Graphology_forceAtlas2':
+        return new GraphologyForceAtlas2();
+      default:
+        return null;
+    }
+  }
+}
+
+export abstract class Graphology extends Layout<GraphologyProvider> {
+  height: number = 100;
+  width: number = 100;
+  constructor(public override algorithm: LayoutAlgorithm<GraphologyProvider>) {
+    super('Graphology', algorithm);
+    this.setDimensions(100, 200);
+  }
+
+  public specialGraphologyFunction() {
+    // graph.forEachNode((node, attr) => {
+    //   graph.setNodeAttribute(node, 'x', 0);
+    //   graph.setNodeAttribute(node, 'y', 0);
+    // });
+  }
+
+  public setDimensions(width = 100, height = 100) {
+    this.width = width;
+    this.height = height;
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyCircular extends Graphology {
+  constructor() {
+    super('Graphology_circular');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // To directly assign the positions to the nodes:
+    circular.assign(graph, {
+      scale: 100,
+    });
+  }
+}
+
+const DEFAULT_RANDOM_SETTINGS: RandomLayoutOptions = {
+  scale: 250,
+};
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyRandom extends Graphology {
+  constructor() {
+    super('Graphology_random');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // const positions = random(graph);
+
+    // To directly assign the positions to the nodes:
+    random.assign(graph, DEFAULT_RANDOM_SETTINGS);
+  }
+}
+
+const DEFAULT_NOVERLAP_SETTINGS: NoverlapSettings = {
+  margin: 40,
+  ratio: 40,
+  // gridSize: 50,
+
+  // gridSize ?number 20: number of grid cells horizontally and vertically subdivising the graph’s space. This is used as an optimization scheme. Set it to 1 and you will have O(n²) time complexity, which can sometimes perform better with very few nodes.
+  // margin ?number 5: margin to keep between nodes.
+  // expansion ?number 1.1: percentage of current space that nodes could attempt to move outside of.
+  // ratio ?number 1.0: ratio scaling node sizes.
+  // speed ?number 3: dampening factor that will slow down node movements to ease the overall process.
+};
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyNoverlap extends Graphology {
+  constructor() {
+    super('Graphology_noverlap');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // // // To directly assign the positions to the nodes:
+    noverlap.assign(graph, {
+      maxIterations: 10000,
+      settings: DEFAULT_NOVERLAP_SETTINGS,
+    });
+  }
+}
+
+const DEFAULT_FORCEATLAS2_SETTINGS: ForceAtlas2Settings = {
+  gravity: 1,
+  adjustSizes: true,
+  linLogMode: true,
+  strongGravityMode: true,
+
+  // adjustSizes ?boolean false: should the node’s sizes be taken into account?
+  // barnesHutOptimize ?boolean false: whether to use the Barnes-Hut approximation to compute repulsion in O(n*log(n)) rather than default O(n^2), n being the number of nodes.
+  // barnesHutTheta ?number 0.5: Barnes-Hut approximation theta parameter.
+  // edgeWeightInfluence ?number 1: influence of the edge’s weights on the layout. To consider edge weight, don’t forget to pass weighted as true when applying the synchronous layout or when instantiating the worker.
+  // gravity ?number 1: strength of the layout’s gravity.
+  // linLogMode ?boolean false: whether to use Noack’s LinLog model.
+  // outboundAttractionDistribution ?boolean false
+  // scalingRatio ?number 1
+  // slowDown ?number 1
+  // strongGravityMode ?boolean false
+};
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyForceAtlas2 extends Graphology {
+  constructor() {
+    super('Graphology_forceAtlas2');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    forceAtlas2.assign(graph, {
+      iterations: 500,
+      settings: DEFAULT_FORCEATLAS2_SETTINGS,
+    });
+  }
+}
diff --git a/libs/shared/graph-layout/src/lib/graphology.spec.ts b/libs/shared/graph-layout/src/lib/graphology.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1b790b6fb3cd8f27ac13c2377b5644106b71c397
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/graphology.spec.ts
@@ -0,0 +1,22 @@
+import Graph, { UndirectedGraph } from 'graphology';
+import { MultiGraph } from 'graphology';
+import connectedCaveman from 'graphology-generators/community/connected-caveman';
+import ladder from 'graphology-generators/classic/ladder';
+
+describe('graphology connection', () => {
+  it('should create a graphology caveman', () => {
+    // Creating a connected caveman graph
+    const graph = connectedCaveman(Graph, 6, 8);
+  });
+
+
+  it('should create a graphology ladder', () => {
+    // Creating a connected caveman graph
+    const graph = ladder(Graph, 6, 8);
+  });
+
+  it('should create a graphology graph', () => {
+    const graph = new MultiGraph();
+    expect(graph);
+  });
+});
diff --git a/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts b/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..903a079dcdc43f88b6070f12845b65f01c14db31
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts
@@ -0,0 +1,156 @@
+// import {
+//   movieSchemaRaw,
+//   northwindSchemaRaw,
+//   simpleSchemaRaw,
+//   twitterSchemaRaw,
+// } from '@graphpolaris/shared/mock-data';
+import Graph, { MultiGraph } from 'graphology';
+
+import connectedCaveman from 'graphology-generators/community/connected-caveman';
+import ladder from 'graphology-generators/classic/ladder';
+import { LayoutFactory } from './layout-creator-usecase';
+
+
+const TIMEOUT = 10;
+
+/**
+ * @jest-environment jsdom
+ */
+describe('LayoutFactory', () => {
+  /**
+   * @jest-environment jsdom
+   */
+  it('should work with noverlap from graphology ', () => {
+    const graph = new MultiGraph();
+
+    // Adding some nodes
+    // graph.addNode('John', { x: 0, y: 0, width: 200, height: 200 });
+    // graph.addNode('Martha', { x: 0, y: 0 });
+    graph.addNode('John');
+    graph.addNode('Martha');
+
+    // Adding an edge
+    graph.addEdge('John', 'Martha');
+
+    const layoutFactory = new LayoutFactory();
+    const layoutAlgorithm = layoutFactory.createLayout(
+      'Graphology_noverlap'
+    );
+    layoutAlgorithm?.layout(graph);
+
+    // const positionMap = new Set<string>();
+    graph.forEachNode((node, attr) => {
+      expect(graph.getNodeAttribute(node, 'x')).toBeDefined();
+      expect(graph.getNodeAttribute(node, 'y')).toBeDefined();
+      
+      // const pos = '' + attr['x'] + '' + attr['y'];
+      // expect(positionMap.has(pos)).toBeFalsy();
+      // positionMap.add(pos);
+    });
+  }, TIMEOUT);
+
+  test('should work with noverlap from graphology on generated graph', () => {
+    // Creating a ladder graph
+    const graph = ladder(Graph, 10);
+
+    graph.forEachNode((node, attr) => {
+      graph.setNodeAttribute(node, 'x', 0);
+      graph.setNodeAttribute(node, 'y', 0);
+    });
+
+    const layoutFactory = new LayoutFactory();
+
+    const layout = layoutFactory.createLayout('Graphology_noverlap');
+    layout?.layout(graph);
+
+    const positionMap = new Set<string>();
+    graph.forEachNode((node, attr) => {
+      const pos = '' + attr['x'] + '' + attr['y'];
+
+      expect(positionMap.has(pos)).toBeFalsy();
+      positionMap.add(pos);
+    });
+  }, TIMEOUT);
+
+  test('should work with random from graphology on generated graph', () => {
+    // Creating a ladder graph
+    const graph = ladder(Graph, 10);
+
+    graph.forEachNode((node, attr) => {
+      graph.setNodeAttribute(node, 'x', 0);
+      graph.setNodeAttribute(node, 'y', 0);
+    });
+
+    const layoutFactory = new LayoutFactory();
+
+    const layout = layoutFactory.createLayout('Graphology_random');
+    layout?.setDimensions(100, 100);
+    layout?.layout(graph);
+
+    const positionMap = new Set<string>();
+    graph.forEachNode((node, attr) => {
+      const pos = '' + attr['x'] + '' + attr['y'];
+
+      expect(positionMap.has(pos)).toBeFalsy();
+      positionMap.add(pos);
+    });
+  }, TIMEOUT);
+
+  test('should work with circular from graphology on generated graph', () => {
+    // Creating a ladder graph
+    const graph = ladder(Graph, 100);
+
+    graph.forEachNode((node) => {
+      graph.setNodeAttribute(node, 'x', 0);
+      graph.setNodeAttribute(node, 'y', 0);
+    });
+
+    const layoutFactory = new LayoutFactory();
+
+    const layout = layoutFactory.createLayout('Graphology_circular');
+    layout?.setDimensions(100, 100);
+    layout?.layout(graph);
+
+    const positionMap = new Set<string>();
+    graph.forEachNode((node, attr) => {
+      const pos = '' + attr['x'] + '' + attr['y'];
+
+      expect(positionMap.has(pos)).toBeFalsy();
+      positionMap.add(pos);
+    });
+  }, TIMEOUT);
+
+  test('should work with Graphology_forceAtlas2 from graphology on generated graph', () => {
+    // console.log(Object.keys(AllLayoutAlgorithms))
+
+    const graph = connectedCaveman(Graph, 6, 8);
+
+    graph.forEachNode((node, attr) => {
+      expect(graph.getNodeAttribute(node, 'x')).toBeUndefined();
+      expect(graph.getNodeAttribute(node, 'y')).toBeUndefined();
+    });
+
+    // console.log('before');
+    const layoutFactory = new LayoutFactory();
+    const layout = layoutFactory.createLayout('Graphology_forceAtlas2');
+    layout?.setDimensions(100, 100);
+    layout?.layout(graph);
+
+    // console.log('after');
+
+    const positionMap = new Set<string>();
+    graph.forEachNode((node, attr) => {
+      expect(graph.getNodeAttribute(node, 'x')).toBeDefined();
+      expect(graph.getNodeAttribute(node, 'y')).toBeDefined();
+
+      const pos = '' + attr['x'] + '' + attr['y'];
+      console.log(pos);
+      
+      expect(positionMap.has(pos)).toBeFalsy();
+      positionMap.add(pos);
+
+      expect(isNaN(graph.getNodeAttribute(node, 'x'))).toBeFalsy();
+      expect(isNaN(graph.getNodeAttribute(node, 'y'))).toBeFalsy();
+    });
+  }, TIMEOUT);
+});
diff --git a/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts b/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..930200eea101f2b6c6c977a3705f5986ef2d864d
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts
@@ -0,0 +1,77 @@
+import {
+  Cytoscape,
+  CytoscapeFactory,
+  CytoscapeLayoutAlgorithms,
+  CytoscapeProvider,
+} from './cytoscape-layouts';
+import {
+  Graphology,
+  GraphologyFactory,
+  GraphologyLayoutAlgorithms,
+  GraphologyProvider,
+} from './graphology-layouts';
+import { Layout } from './layout';
+
+export type Providers = GraphologyProvider | CytoscapeProvider;
+export type LayoutAlgorithm<Provider extends Providers> =
+  `${Provider}_${string}`;
+
+export type AllLayoutAlgorithms =
+  | GraphologyLayoutAlgorithms
+  | CytoscapeLayoutAlgorithms;
+
+export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> =
+  Algorithm extends GraphologyLayoutAlgorithms
+    ? Graphology
+    : Algorithm extends CytoscapeLayoutAlgorithms
+    ? Cytoscape
+    : Cytoscape | Graphology;
+
+export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> {
+  createLayout: (
+    Algorithm: Algorithm
+  ) => AlgorithmToLayoutProvider<Algorithm> | null;
+}
+
+/**
+ * This is our Creator
+ */
+export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> {
+  private graphologyFactory = new GraphologyFactory();
+  private cytoscapeFactory = new CytoscapeFactory();
+
+  private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>(
+    LayoutAlgorithm: AllLayoutAlgorithms,
+    startsWith: string
+  ): LayoutAlgorithm is Algorithm {
+    return LayoutAlgorithm.startsWith(startsWith);
+  }
+
+  createLayout<Algorithm extends AllLayoutAlgorithms>(
+    layoutAlgorithm: Algorithm
+  ): AlgorithmToLayoutProvider<Algorithm> | null {
+    if (
+      this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>(
+        layoutAlgorithm,
+        'Graphology'
+      )
+    ) {
+      return this.graphologyFactory.createLayout(
+        layoutAlgorithm
+      ) as AlgorithmToLayoutProvider<Algorithm>;
+    }
+
+    if (
+      this.isSpecificAlgorithm<CytoscapeLayoutAlgorithms>(
+        layoutAlgorithm,
+        'Cytoscape'
+      )
+    ) {
+      return this.cytoscapeFactory.createLayout(
+        layoutAlgorithm
+      ) as AlgorithmToLayoutProvider<Algorithm>;
+    }
+
+    return null;
+  }
+}
diff --git a/libs/shared/graph-layout/src/lib/layout.ts b/libs/shared/graph-layout/src/lib/layout.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b094c1c141c760038ec9de7ed085cf35fae91ee3
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/layout.ts
@@ -0,0 +1,31 @@
+import Graph from 'graphology';
+import { Providers, LayoutAlgorithm } from './layout-creator-usecase';
+
+/**
+ * This is our Product
+ */
+
+export abstract class Layout<provider extends Providers> {
+  constructor(
+    public provider: provider,
+    public algorithm: LayoutAlgorithm<provider>
+  ) {
+    console.log(
+      `Created the following Layout: ${provider} - ${this.algorithm}`
+    );
+  }
+
+  public layout(graph: Graph) {
+    console.log(`${this.provider} [${this.algorithm}] layouting now`);
+
+    graph.forEachNode((node) => {
+      const attr = graph.getNodeAttributes(node);
+      if(!attr.hasOwnProperty('x')){
+        graph.setNodeAttribute(node, 'x', Math.random());
+      }
+      if(!attr.hasOwnProperty('y')){
+        graph.setNodeAttribute(node, 'y', Math.random());
+      }
+    });
+  }
+}
diff --git a/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts b/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8747a6c9d96bc565e539831700dbe6dc93dcd51
--- /dev/null
+++ b/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts
@@ -0,0 +1,12 @@
+import {
+  movieSchema,
+  northWindSchema,
+  simpleSchemaRaw,
+  twitterSchemaRaw,
+} from '@graphpolaris/shared/mock-data';
+
+it('should layout the mock-data movieSchema', () => {
+  // Creating a connected caveman graph
+  const schema = movieSchema;
+  expect(schema).toBeDefined();
+});
diff --git a/libs/shared/graph-layout/tsconfig.json b/libs/shared/graph-layout/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..4b421814593c06b901b07a205f079cc08998a1e0
--- /dev/null
+++ b/libs/shared/graph-layout/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/libs/shared/graph-layout/tsconfig.lib.json b/libs/shared/graph-layout/tsconfig.lib.json
new file mode 100644
index 0000000000000000000000000000000000000000..0a86e9a4481e7c1f10b111060bf05bcd90a5329e
--- /dev/null
+++ b/libs/shared/graph-layout/tsconfig.lib.json
@@ -0,0 +1,23 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "types": ["node"],
+    "composite": true
+  },
+  "files": [
+    "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+    "../../../node_modules/@nrwl/react/typings/image.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+    "**/*.spec.tsx",
+    "**/*.test.tsx",
+    "**/*.spec.js",
+    "**/*.test.js",
+    "**/*.spec.jsx",
+    "**/*.test.jsx"
+  ],
+  "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/shared/graph-layout/tsconfig.spec.json b/libs/shared/graph-layout/tsconfig.spec.json
new file mode 100644
index 0000000000000000000000000000000000000000..4afc999ad429eea63e777308527c4e8f629e4198
--- /dev/null
+++ b/libs/shared/graph-layout/tsconfig.spec.json
@@ -0,0 +1,20 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"],
+    "composite": true
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/libs/shared/graph-layout/yarn.lock b/libs/shared/graph-layout/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..fc3857ee286914b3d6ed68e70133ce3e4f439b27
--- /dev/null
+++ b/libs/shared/graph-layout/yarn.lock
@@ -0,0 +1,100 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@yomguithereal/helpers@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@yomguithereal/helpers/-/helpers-1.1.1.tgz#185dfb0f88ca2beec53d0adf6eed15c33b1c549d"
+  integrity sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==
+
+events@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+graphology-generators@^0.11.2:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/graphology-generators/-/graphology-generators-0.11.2.tgz#eff2c97e4f5bf401e86ab045470dded95f2ebe24"
+  integrity sha512-hx+F0OZRkVdoQ0B1tWrpxoakmHZNex0c6RAoR0PrqJ+6fz/gz6CQ88Qlw78C6yD9nlZVRgepIoDYhRTFV+bEHg==
+  dependencies:
+    graphology-metrics "^2.0.0"
+    graphology-utils "^2.3.0"
+
+graphology-indices@^0.16.3:
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/graphology-indices/-/graphology-indices-0.16.6.tgz#0de112ef0367e44041490933e34ad2075cb24e80"
+  integrity sha512-tozTirLb7pd37wULJ5qeIZfZqKuVln/V+bWmUWJ7MmoTU8YkW5dehOkRz2by/O+5MdJ52imqL8LH4+GCd0yEVw==
+  dependencies:
+    graphology-utils "^2.4.2"
+    mnemonist "^0.39.0"
+
+graphology-layout-forceatlas2@^0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.8.2.tgz#7cb5b2fa00fd5445cb2b73c333e36ef22c8a82a8"
+  integrity sha512-OsmOuQP0xiav5Iau9W9G4eb4cGx5tDcdzx9NudG6fhi6japqD+Z45zUBcwnp/12BPBXp/PKc5pvUe3Va6AsOUA==
+  dependencies:
+    graphology-utils "^2.1.0"
+
+graphology-layout-noverlap@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz#2ffa054ceeebaa31fcffe695d271fc55707cd29c"
+  integrity sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==
+  dependencies:
+    graphology-utils "^2.3.0"
+
+graphology-layout@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/graphology-layout/-/graphology-layout-0.5.0.tgz#a0a54861cebae5f486c778dbdafc6294859f23b5"
+  integrity sha512-aIeXYPLeGMLvXIkO41TlhBv0ROFWUx1bqR2VQoJ7Mp2IW+TF+rxqMeRUrmyLHoe3HtKo8jhloB2KHp7g6fcDSg==
+  dependencies:
+    graphology-utils "^2.3.0"
+    pandemonium "^1.5.0"
+
+graphology-metrics@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/graphology-metrics/-/graphology-metrics-2.1.0.tgz#7d00bae92d8970583afd020e6d40d8a16c378002"
+  integrity sha512-E+y4kgVGxhYl/+bPHEftJeWLS8LgVno4/Wvg+C7IoDIjY6OlIZghgMKDR8LKsxU6GC43mlx08FTZs229cvEkwQ==
+  dependencies:
+    graphology-shortest-path "^2.0.0"
+    graphology-utils "^2.4.4"
+    mnemonist "^0.39.0"
+
+graphology-shortest-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/graphology-shortest-path/-/graphology-shortest-path-2.0.0.tgz#27a01b3a9980872bd44a197fc77114623dd2b302"
+  integrity sha512-6dJWgbr7w4YQKb7Y0w7vhZn2qAkqP+J0IhE9F3vz/HZcx7VSOqnNfTGtYr44BQ5ohdXj0l9iKjlWCb+3vqEINQ==
+  dependencies:
+    "@yomguithereal/helpers" "^1.1.1"
+    graphology-indices "^0.16.3"
+    graphology-utils "^2.4.3"
+    mnemonist "^0.39.0"
+
+graphology-utils@^2.1.0, graphology-utils@^2.3.0, graphology-utils@^2.4.2, graphology-utils@^2.4.3, graphology-utils@^2.4.4:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c"
+  integrity sha512-N6zjqvBHgJvulYnwdDgdJoeuhKXZBNm1zedC2asdN+rexfbJylhey/PVT8Bwr8B1aVxKuK+zQqMbQ50kKikjew==
+
+graphology@^0.24.1:
+  version "0.24.1"
+  resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.24.1.tgz#035e452e294b01168cf5c85d5dd0a4b7e4837d87"
+  integrity sha512-6lNz1PNTAe9Q6ioHKrXu0Lp047sgvOoHa4qmP/8mnJWCGv2iIZPQkuHPUb2/OWDWCqHpw2hKgJLJ55X/66xmHg==
+  dependencies:
+    events "^3.3.0"
+    obliterator "^2.0.2"
+
+mnemonist@^0.39.0:
+  version "0.39.0"
+  resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.0.tgz#4c83dd22e8d9d05dfb721ff66a905fec4c460041"
+  integrity sha512-7v08Ldk1lnlywnIShqfKYN7EW4WKLUnkoWApdmR47N1xA2xmEtWERfEvyRCepbuFCETG5OnfaGQpp/p4Bus6ZQ==
+  dependencies:
+    obliterator "^2.0.1"
+
+obliterator@^2.0.1, obliterator@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21"
+  integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg==
+
+pandemonium@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/pandemonium/-/pandemonium-1.5.0.tgz#93f35af555de1420022b341e730215c51c725be3"
+  integrity sha512-9PU9fy93rJhZHLMjX+4M1RwZPEYl6g7DdWKGmGNhkgBZR5+tOBVExNZc00kzdEGMxbaAvWdQy9MqGAScGwYlcA==
diff --git a/libs/shared/graph-layouts/.eslintrc.json b/libs/shared/graph-layouts/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..3456be9b9036a42c593c82b050281230e4ca0ae4
--- /dev/null
+++ b/libs/shared/graph-layouts/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["../../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/libs/shared/graph-layouts/README.md b/libs/shared/graph-layouts/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b30ee66b0a5bfd0ba196a3333551eeeef0e2b745
--- /dev/null
+++ b/libs/shared/graph-layouts/README.md
@@ -0,0 +1,11 @@
+# shared-graph-layouts
+
+This library was generated with [Nx](https://nx.dev).
+
+## Building
+
+Run `nx build shared-graph-layouts` to build the library.
+
+## Running unit tests
+
+Run `nx test shared-graph-layouts` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/graph-layouts/dist/README.md b/libs/shared/graph-layouts/dist/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b30ee66b0a5bfd0ba196a3333551eeeef0e2b745
--- /dev/null
+++ b/libs/shared/graph-layouts/dist/README.md
@@ -0,0 +1,11 @@
+# shared-graph-layouts
+
+This library was generated with [Nx](https://nx.dev).
+
+## Building
+
+Run `nx build shared-graph-layouts` to build the library.
+
+## Running unit tests
+
+Run `nx test shared-graph-layouts` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/graph-layouts/jest.config.js b/libs/shared/graph-layouts/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..89ea43d51be778fd1ce6551ad9a200e2cc8ee25a
--- /dev/null
+++ b/libs/shared/graph-layouts/jest.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+  displayName: 'shared-graph-layouts',
+  preset: '../../../jest.preset.js',
+  globals: {
+    'ts-jest': {
+      tsconfig: '<rootDir>/tsconfig.spec.json',
+    },
+  },
+  transform: {
+    '^.+\\.[tj]s$': 'ts-jest',
+  },
+  moduleFileExtensions: ['ts', 'js', 'html'],
+  coverageDirectory: '../../../coverage/libs/shared/graph-layouts',
+};
diff --git a/libs/shared/graph-layouts/node_modules/.yarn-integrity b/libs/shared/graph-layouts/node_modules/.yarn-integrity
new file mode 100644
index 0000000000000000000000000000000000000000..f5558a47ef7526457db3827975306a7856715ca3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/.yarn-integrity
@@ -0,0 +1,18 @@
+{
+  "systemParams": "win32-x64-83",
+  "modulesFolders": [
+    "node_modules"
+  ],
+  "flags": [],
+  "linkedModules": [],
+  "topLevelPatterns": [
+    "graphology@^0.24.1"
+  ],
+  "lockfileEntries": {
+    "events@^3.3.0": "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400",
+    "graphology@^0.24.1": "https://registry.yarnpkg.com/graphology/-/graphology-0.24.1.tgz#035e452e294b01168cf5c85d5dd0a4b7e4837d87",
+    "obliterator@^2.0.2": "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21"
+  },
+  "files": [],
+  "artifacts": {}
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/events/.airtap.yml b/libs/shared/graph-layouts/node_modules/events/.airtap.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7a8a87d5e99d1b538b50bb40bd3e8a3d44b6f3b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/.airtap.yml
@@ -0,0 +1,15 @@
+sauce_connect: true
+loopback: airtap.local
+browsers:
+  - name: chrome
+    version: latest
+  - name: firefox
+    version: latest
+  - name: safari
+    version: 9..latest
+  - name: iphone
+    version: latest
+  - name: ie
+    version: 9..latest
+  - name: microsoftedge
+    version: 13..latest
diff --git a/libs/shared/graph-layouts/node_modules/events/.github/FUNDING.yml b/libs/shared/graph-layouts/node_modules/events/.github/FUNDING.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b8cb78ba90207e382cd5384d4a4bcc78e90113a
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: npm/events
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/libs/shared/graph-layouts/node_modules/events/.travis.yml b/libs/shared/graph-layouts/node_modules/events/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..486dc3c4c1df7fe432eb19fa9174b2e33b263e37
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/.travis.yml
@@ -0,0 +1,18 @@
+dist: xenial
+os: linux
+language: node_js
+node_js:
+  - 'stable'
+  - 'lts/*'
+  - '0.12'
+script:
+  - npm test
+  - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ]; then npm run test:browsers; fi
+addons:
+  sauce_connect: true
+  hosts:
+    - airtap.local
+env:
+  global:
+  - secure: XcBiD8yReflut9q7leKsigDZ0mI3qTKH+QrNVY8DaqlomJOZw8aOrVuX9Jz12l86ZJ41nbxmKnRNkFzcVr9mbP9YaeTb3DpeOBWmvaoSfud9Wnc16VfXtc1FCcwDhSVcSiM3UtnrmFU5cH+Dw1LPh5PbfylYOS/nJxUvG0FFLqI=
+  - secure: jNWtEbqhUdQ0xXDHvCYfUbKYeJCi6a7B4LsrcxYCyWWn4NIgncE5x2YbB+FSUUFVYfz0dsn5RKP1oHB99f0laUEo18HBNkrAS/rtyOdVzcpJjbQ6kgSILGjnJD/Ty1B57Rcz3iyev5Y7bLZ6Y1FbDnk/i9/l0faOGz8vTC3Vdkc=
diff --git a/libs/shared/graph-layouts/node_modules/events/History.md b/libs/shared/graph-layouts/node_modules/events/History.md
new file mode 100644
index 0000000000000000000000000000000000000000..f48bf210da3ea23213ce37ebd13cd5788c9563c9
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/History.md
@@ -0,0 +1,118 @@
+# 3.3.0
+
+ - Support EventTarget emitters in `events.once` from Node.js 12.11.0.
+
+   Now you can use the `events.once` function with objects that implement the EventTarget interface. This interface is used widely in
+   the DOM and other web APIs.
+
+   ```js
+   var events = require('events');
+   var assert = require('assert');
+
+   async function connect() {
+     var ws = new WebSocket('wss://example.com');
+     await events.once(ws, 'open');
+     assert(ws.readyState === WebSocket.OPEN);
+   }
+
+   async function onClick() {
+     await events.once(document.body, 'click');
+     alert('you clicked the page!');
+   }
+   ```
+
+# 3.2.0
+
+ - Add `events.once` from Node.js 11.13.0.
+
+   To use this function, Promises must be supported in the environment. Use a polyfill like `es6-promise` if you support older browsers.
+
+# 3.1.0 (2020-01-08)
+
+`events` now matches the Node.js 11.12.0 API.
+
+  - pass through return value in wrapped `emitter.once()` listeners
+
+    Now, this works:
+    ```js
+    emitter.once('myevent', function () { return 1; });
+    var listener = emitter.rawListeners('myevent')[0]
+    assert(listener() === 1);
+    ```
+    Previously, `listener()` would return undefined regardless of the implementation.
+
+    Ported from https://github.com/nodejs/node/commit/acc506c2d2771dab8d7bba6d3452bc5180dff7cf
+
+  - Reduce code duplication in listener type check ([#67](https://github.com/Gozala/events/pull/67) by [@friederbluemle](https://github.com/friederbluemle)).
+  - Improve `emitter.once()` performance in some engines
+
+# 3.0.0 (2018-05-25)
+
+**This version drops support for IE8.** `events` no longer includes polyfills
+for ES5 features. If you need to support older environments, use an ES5 shim
+like [es5-shim](https://npmjs.com/package/es5-shim). Both the shim and sham
+versions of es5-shim are necessary.
+
+  - Update to events code from Node.js 10.x
+    - (semver major) Adds `off()` method
+  - Port more tests from Node.js
+  - Switch browser tests to airtap, making things more reliable
+
+# 2.1.0 (2018-05-25)
+
+  - add Emitter#rawListeners from Node.js v9.4
+
+# 2.0.0 (2018-02-02)
+
+  - Update to events code from node.js 8.x
+    - Adds `prependListener()` and `prependOnceListener()`
+    - Adds `eventNames()` method
+    - (semver major) Unwrap `once()` listeners in `listeners()`
+  - copy tests from node.js
+
+Note that this version doubles the gzipped size, jumping from 1.1KB to 2.1KB,
+due to new methods and runtime performance improvements. Be aware of that when
+upgrading.
+
+# 1.1.1 (2016-06-22)
+
+  - add more context to errors if they are not instanceof Error
+
+# 1.1.0 (2015-09-29)
+
+  - add Emitter#listerCount (to match node v4 api)
+
+# 1.0.2 (2014-08-28)
+
+  - remove un-reachable code
+  - update devDeps
+
+## 1.0.1 / 2014-05-11
+
+  - check for console.trace before using it
+
+## 1.0.0 / 2013-12-10
+
+  - Update to latest events code from node.js 0.10
+  - copy tests from node.js
+
+## 0.4.0 / 2011-07-03 ##
+
+  - Switching to graphquire@0.8.0
+
+## 0.3.0 / 2011-07-03 ##
+
+  - Switching to URL based module require.
+
+## 0.2.0 / 2011-06-10 ##
+
+  - Simplified package structure.
+  - Graphquire for dependency management.
+
+## 0.1.1 / 2011-05-16 ##
+
+  - Unhandled errors are logged via console.error
+
+## 0.1.0 / 2011-04-22 ##
+
+  - Initial release
diff --git a/libs/shared/graph-layouts/node_modules/events/LICENSE b/libs/shared/graph-layouts/node_modules/events/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..52ed3b0a63274d653474abc893d6f7221fc301fd
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/LICENSE
@@ -0,0 +1,22 @@
+MIT
+
+Copyright Joyent, Inc. and other Node contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/libs/shared/graph-layouts/node_modules/events/Readme.md b/libs/shared/graph-layouts/node_modules/events/Readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..80849c0b2d7c39727a540f369d764ee36ef5ee07
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/Readme.md
@@ -0,0 +1,50 @@
+# events [![Build Status](https://travis-ci.org/Gozala/events.png?branch=master)](https://travis-ci.org/Gozala/events)
+
+> Node's event emitter for all engines.
+
+This implements the Node.js [`events`][node.js docs] module for environments that do not have it, like browsers.
+
+> `events` currently matches the **Node.js 11.13.0** API.
+
+Note that the `events` module uses ES5 features. If you need to support very old browsers like IE8, use a shim like [`es5-shim`](https://www.npmjs.com/package/es5-shim). You need both the shim and the sham versions of `es5-shim`.
+
+This module is maintained, but only by very few people. If you'd like to help, let us know in the [Maintainer Needed](https://github.com/Gozala/events/issues/43) issue!
+
+## Install
+
+You usually do not have to install `events` yourself! If your code runs in Node.js, `events` is built in. If your code runs in the browser, bundlers like [browserify](https://github.com/browserify/browserify) or [webpack](https://github.com/webpack/webpack) also include the `events` module.
+
+But if none of those apply, with npm do:
+
+```
+npm install events
+```
+
+## Usage
+
+```javascript
+var EventEmitter = require('events')
+
+var ee = new EventEmitter()
+ee.on('message', function (text) {
+  console.log(text)
+})
+ee.emit('message', 'hello world')
+```
+
+## API
+
+See the [Node.js EventEmitter docs][node.js docs]. `events` currently matches the Node.js 11.13.0 API.
+
+## Contributing
+
+PRs are very welcome! The main way to contribute to `events` is by porting features, bugfixes and tests from Node.js. Ideally, code contributions to this module are copy-pasted from Node.js and transpiled to ES5, rather than reimplemented from scratch. Matching the Node.js code as closely as possible makes maintenance simpler when new changes land in Node.js.
+This module intends to provide exactly the same API as Node.js, so features that are not available in the core `events` module will not be accepted. Feature requests should instead be directed at [nodejs/node](https://github.com/nodejs/node) and will be added to this module once they are implemented in Node.js.
+
+If there is a difference in behaviour between Node.js's `events` module and this module, please open an issue!
+
+## License
+
+[MIT](./LICENSE)
+
+[node.js docs]: https://nodejs.org/dist/v11.13.0/docs/api/events.html
diff --git a/libs/shared/graph-layouts/node_modules/events/events.js b/libs/shared/graph-layouts/node_modules/events/events.js
new file mode 100644
index 0000000000000000000000000000000000000000..34b69a0b4a6e124e8de119540980bbb4d6cdf4d6
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/events.js
@@ -0,0 +1,497 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+
+var R = typeof Reflect === 'object' ? Reflect : null
+var ReflectApply = R && typeof R.apply === 'function'
+  ? R.apply
+  : function ReflectApply(target, receiver, args) {
+    return Function.prototype.apply.call(target, receiver, args);
+  }
+
+var ReflectOwnKeys
+if (R && typeof R.ownKeys === 'function') {
+  ReflectOwnKeys = R.ownKeys
+} else if (Object.getOwnPropertySymbols) {
+  ReflectOwnKeys = function ReflectOwnKeys(target) {
+    return Object.getOwnPropertyNames(target)
+      .concat(Object.getOwnPropertySymbols(target));
+  };
+} else {
+  ReflectOwnKeys = function ReflectOwnKeys(target) {
+    return Object.getOwnPropertyNames(target);
+  };
+}
+
+function ProcessEmitWarning(warning) {
+  if (console && console.warn) console.warn(warning);
+}
+
+var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
+  return value !== value;
+}
+
+function EventEmitter() {
+  EventEmitter.init.call(this);
+}
+module.exports = EventEmitter;
+module.exports.once = once;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._eventsCount = 0;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+var defaultMaxListeners = 10;
+
+function checkListener(listener) {
+  if (typeof listener !== 'function') {
+    throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
+  }
+}
+
+Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
+  enumerable: true,
+  get: function() {
+    return defaultMaxListeners;
+  },
+  set: function(arg) {
+    if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
+      throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
+    }
+    defaultMaxListeners = arg;
+  }
+});
+
+EventEmitter.init = function() {
+
+  if (this._events === undefined ||
+      this._events === Object.getPrototypeOf(this)._events) {
+    this._events = Object.create(null);
+    this._eventsCount = 0;
+  }
+
+  this._maxListeners = this._maxListeners || undefined;
+};
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+  if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+    throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
+  }
+  this._maxListeners = n;
+  return this;
+};
+
+function _getMaxListeners(that) {
+  if (that._maxListeners === undefined)
+    return EventEmitter.defaultMaxListeners;
+  return that._maxListeners;
+}
+
+EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
+  return _getMaxListeners(this);
+};
+
+EventEmitter.prototype.emit = function emit(type) {
+  var args = [];
+  for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
+  var doError = (type === 'error');
+
+  var events = this._events;
+  if (events !== undefined)
+    doError = (doError && events.error === undefined);
+  else if (!doError)
+    return false;
+
+  // If there is no 'error' event listener then throw.
+  if (doError) {
+    var er;
+    if (args.length > 0)
+      er = args[0];
+    if (er instanceof Error) {
+      // Note: The comments on the `throw` lines are intentional, they show
+      // up in Node's output if this results in an unhandled exception.
+      throw er; // Unhandled 'error' event
+    }
+    // At least give some kind of context to the user
+    var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
+    err.context = er;
+    throw err; // Unhandled 'error' event
+  }
+
+  var handler = events[type];
+
+  if (handler === undefined)
+    return false;
+
+  if (typeof handler === 'function') {
+    ReflectApply(handler, this, args);
+  } else {
+    var len = handler.length;
+    var listeners = arrayClone(handler, len);
+    for (var i = 0; i < len; ++i)
+      ReflectApply(listeners[i], this, args);
+  }
+
+  return true;
+};
+
+function _addListener(target, type, listener, prepend) {
+  var m;
+  var events;
+  var existing;
+
+  checkListener(listener);
+
+  events = target._events;
+  if (events === undefined) {
+    events = target._events = Object.create(null);
+    target._eventsCount = 0;
+  } else {
+    // To avoid recursion in the case that type === "newListener"! Before
+    // adding it to the listeners, first emit "newListener".
+    if (events.newListener !== undefined) {
+      target.emit('newListener', type,
+                  listener.listener ? listener.listener : listener);
+
+      // Re-assign `events` because a newListener handler could have caused the
+      // this._events to be assigned to a new object
+      events = target._events;
+    }
+    existing = events[type];
+  }
+
+  if (existing === undefined) {
+    // Optimize the case of one listener. Don't need the extra array object.
+    existing = events[type] = listener;
+    ++target._eventsCount;
+  } else {
+    if (typeof existing === 'function') {
+      // Adding the second element, need to change to array.
+      existing = events[type] =
+        prepend ? [listener, existing] : [existing, listener];
+      // If we've already got an array, just append.
+    } else if (prepend) {
+      existing.unshift(listener);
+    } else {
+      existing.push(listener);
+    }
+
+    // Check for listener leak
+    m = _getMaxListeners(target);
+    if (m > 0 && existing.length > m && !existing.warned) {
+      existing.warned = true;
+      // No error code for this since it is a Warning
+      // eslint-disable-next-line no-restricted-syntax
+      var w = new Error('Possible EventEmitter memory leak detected. ' +
+                          existing.length + ' ' + String(type) + ' listeners ' +
+                          'added. Use emitter.setMaxListeners() to ' +
+                          'increase limit');
+      w.name = 'MaxListenersExceededWarning';
+      w.emitter = target;
+      w.type = type;
+      w.count = existing.length;
+      ProcessEmitWarning(w);
+    }
+  }
+
+  return target;
+}
+
+EventEmitter.prototype.addListener = function addListener(type, listener) {
+  return _addListener(this, type, listener, false);
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.prependListener =
+    function prependListener(type, listener) {
+      return _addListener(this, type, listener, true);
+    };
+
+function onceWrapper() {
+  if (!this.fired) {
+    this.target.removeListener(this.type, this.wrapFn);
+    this.fired = true;
+    if (arguments.length === 0)
+      return this.listener.call(this.target);
+    return this.listener.apply(this.target, arguments);
+  }
+}
+
+function _onceWrap(target, type, listener) {
+  var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
+  var wrapped = onceWrapper.bind(state);
+  wrapped.listener = listener;
+  state.wrapFn = wrapped;
+  return wrapped;
+}
+
+EventEmitter.prototype.once = function once(type, listener) {
+  checkListener(listener);
+  this.on(type, _onceWrap(this, type, listener));
+  return this;
+};
+
+EventEmitter.prototype.prependOnceListener =
+    function prependOnceListener(type, listener) {
+      checkListener(listener);
+      this.prependListener(type, _onceWrap(this, type, listener));
+      return this;
+    };
+
+// Emits a 'removeListener' event if and only if the listener was removed.
+EventEmitter.prototype.removeListener =
+    function removeListener(type, listener) {
+      var list, events, position, i, originalListener;
+
+      checkListener(listener);
+
+      events = this._events;
+      if (events === undefined)
+        return this;
+
+      list = events[type];
+      if (list === undefined)
+        return this;
+
+      if (list === listener || list.listener === listener) {
+        if (--this._eventsCount === 0)
+          this._events = Object.create(null);
+        else {
+          delete events[type];
+          if (events.removeListener)
+            this.emit('removeListener', type, list.listener || listener);
+        }
+      } else if (typeof list !== 'function') {
+        position = -1;
+
+        for (i = list.length - 1; i >= 0; i--) {
+          if (list[i] === listener || list[i].listener === listener) {
+            originalListener = list[i].listener;
+            position = i;
+            break;
+          }
+        }
+
+        if (position < 0)
+          return this;
+
+        if (position === 0)
+          list.shift();
+        else {
+          spliceOne(list, position);
+        }
+
+        if (list.length === 1)
+          events[type] = list[0];
+
+        if (events.removeListener !== undefined)
+          this.emit('removeListener', type, originalListener || listener);
+      }
+
+      return this;
+    };
+
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+EventEmitter.prototype.removeAllListeners =
+    function removeAllListeners(type) {
+      var listeners, events, i;
+
+      events = this._events;
+      if (events === undefined)
+        return this;
+
+      // not listening for removeListener, no need to emit
+      if (events.removeListener === undefined) {
+        if (arguments.length === 0) {
+          this._events = Object.create(null);
+          this._eventsCount = 0;
+        } else if (events[type] !== undefined) {
+          if (--this._eventsCount === 0)
+            this._events = Object.create(null);
+          else
+            delete events[type];
+        }
+        return this;
+      }
+
+      // emit removeListener for all listeners on all events
+      if (arguments.length === 0) {
+        var keys = Object.keys(events);
+        var key;
+        for (i = 0; i < keys.length; ++i) {
+          key = keys[i];
+          if (key === 'removeListener') continue;
+          this.removeAllListeners(key);
+        }
+        this.removeAllListeners('removeListener');
+        this._events = Object.create(null);
+        this._eventsCount = 0;
+        return this;
+      }
+
+      listeners = events[type];
+
+      if (typeof listeners === 'function') {
+        this.removeListener(type, listeners);
+      } else if (listeners !== undefined) {
+        // LIFO order
+        for (i = listeners.length - 1; i >= 0; i--) {
+          this.removeListener(type, listeners[i]);
+        }
+      }
+
+      return this;
+    };
+
+function _listeners(target, type, unwrap) {
+  var events = target._events;
+
+  if (events === undefined)
+    return [];
+
+  var evlistener = events[type];
+  if (evlistener === undefined)
+    return [];
+
+  if (typeof evlistener === 'function')
+    return unwrap ? [evlistener.listener || evlistener] : [evlistener];
+
+  return unwrap ?
+    unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
+}
+
+EventEmitter.prototype.listeners = function listeners(type) {
+  return _listeners(this, type, true);
+};
+
+EventEmitter.prototype.rawListeners = function rawListeners(type) {
+  return _listeners(this, type, false);
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  if (typeof emitter.listenerCount === 'function') {
+    return emitter.listenerCount(type);
+  } else {
+    return listenerCount.call(emitter, type);
+  }
+};
+
+EventEmitter.prototype.listenerCount = listenerCount;
+function listenerCount(type) {
+  var events = this._events;
+
+  if (events !== undefined) {
+    var evlistener = events[type];
+
+    if (typeof evlistener === 'function') {
+      return 1;
+    } else if (evlistener !== undefined) {
+      return evlistener.length;
+    }
+  }
+
+  return 0;
+}
+
+EventEmitter.prototype.eventNames = function eventNames() {
+  return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
+};
+
+function arrayClone(arr, n) {
+  var copy = new Array(n);
+  for (var i = 0; i < n; ++i)
+    copy[i] = arr[i];
+  return copy;
+}
+
+function spliceOne(list, index) {
+  for (; index + 1 < list.length; index++)
+    list[index] = list[index + 1];
+  list.pop();
+}
+
+function unwrapListeners(arr) {
+  var ret = new Array(arr.length);
+  for (var i = 0; i < ret.length; ++i) {
+    ret[i] = arr[i].listener || arr[i];
+  }
+  return ret;
+}
+
+function once(emitter, name) {
+  return new Promise(function (resolve, reject) {
+    function errorListener(err) {
+      emitter.removeListener(name, resolver);
+      reject(err);
+    }
+
+    function resolver() {
+      if (typeof emitter.removeListener === 'function') {
+        emitter.removeListener('error', errorListener);
+      }
+      resolve([].slice.call(arguments));
+    };
+
+    eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
+    if (name !== 'error') {
+      addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
+    }
+  });
+}
+
+function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
+  if (typeof emitter.on === 'function') {
+    eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
+  }
+}
+
+function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
+  if (typeof emitter.on === 'function') {
+    if (flags.once) {
+      emitter.once(name, listener);
+    } else {
+      emitter.on(name, listener);
+    }
+  } else if (typeof emitter.addEventListener === 'function') {
+    // EventTarget does not have `error` event semantics like Node
+    // EventEmitters, we do not listen for `error` events here.
+    emitter.addEventListener(name, function wrapListener(arg) {
+      // IE does not have builtin `{ once: true }` support so we
+      // have to do it manually.
+      if (flags.once) {
+        emitter.removeEventListener(name, wrapListener);
+      }
+      listener(arg);
+    });
+  } else {
+    throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
+  }
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/package.json b/libs/shared/graph-layouts/node_modules/events/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..b9580d88142d2921273599e2afaaec640906df6d
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "events",
+  "version": "3.3.0",
+  "description": "Node's event emitter for all engines.",
+  "keywords": [
+    "events",
+    "eventEmitter",
+    "eventDispatcher",
+    "listeners"
+  ],
+  "author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/Gozala/events.git",
+    "web": "https://github.com/Gozala/events"
+  },
+  "bugs": {
+    "url": "http://github.com/Gozala/events/issues/"
+  },
+  "main": "./events.js",
+  "engines": {
+    "node": ">=0.8.x"
+  },
+  "devDependencies": {
+    "airtap": "^1.0.0",
+    "functions-have-names": "^1.2.1",
+    "has": "^1.0.3",
+    "has-symbols": "^1.0.1",
+    "isarray": "^2.0.5",
+    "tape": "^5.0.0"
+  },
+  "scripts": {
+    "test": "node tests/index.js",
+    "test:browsers": "airtap -- tests/index.js"
+  },
+  "license": "MIT"
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/security.md b/libs/shared/graph-layouts/node_modules/events/security.md
new file mode 100644
index 0000000000000000000000000000000000000000..a14ace6a57db70992630a568df6a7bf57763d26b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/security.md
@@ -0,0 +1,10 @@
+# Security Policy
+
+## Supported Versions
+Only the latest major version is supported at any given time.
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/add-listeners.js b/libs/shared/graph-layouts/node_modules/events/tests/add-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b578272ba889e9732543badfc9077a59f974640
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/add-listeners.js
@@ -0,0 +1,111 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+{
+  var ee = new EventEmitter();
+  var events_new_listener_emitted = [];
+  var listeners_new_listener_emitted = [];
+
+  // Sanity check
+  assert.strictEqual(ee.addListener, ee.on);
+
+  ee.on('newListener', function(event, listener) {
+    // Don't track newListener listeners.
+    if (event === 'newListener')
+      return;
+
+    events_new_listener_emitted.push(event);
+    listeners_new_listener_emitted.push(listener);
+  });
+
+  var hello = common.mustCall(function(a, b) {
+    assert.strictEqual('a', a);
+    assert.strictEqual('b', b);
+  });
+
+  ee.once('newListener', function(name, listener) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(listener, hello);
+
+    var listeners = this.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  });
+
+  ee.on('hello', hello);
+  ee.once('foo', assert.fail);
+
+  assert.ok(Array.isArray(events_new_listener_emitted));
+  assert.strictEqual(events_new_listener_emitted.length, 2);
+  assert.strictEqual(events_new_listener_emitted[0], 'hello');
+  assert.strictEqual(events_new_listener_emitted[1], 'foo');
+
+  assert.ok(Array.isArray(listeners_new_listener_emitted));
+  assert.strictEqual(listeners_new_listener_emitted.length, 2);
+  assert.strictEqual(listeners_new_listener_emitted[0], hello);
+  assert.strictEqual(listeners_new_listener_emitted[1], assert.fail);
+
+  ee.emit('hello', 'a', 'b');
+}
+
+// just make sure that this doesn't throw:
+{
+  var f = new EventEmitter();
+
+  f.setMaxListeners(0);
+}
+
+{
+  var listen1 = function() {};
+  var listen2 = function() {};
+  var ee = new EventEmitter();
+
+  ee.once('newListener', function() {
+    var listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+    ee.once('newListener', function() {
+      var listeners = ee.listeners('hello');
+      assert.ok(Array.isArray(listeners));
+      assert.strictEqual(listeners.length, 0);
+    });
+    ee.on('hello', listen2);
+  });
+  ee.on('hello', listen1);
+  // The order of listeners on an event is not always the order in which the
+  // listeners were added.
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listen2);
+  assert.strictEqual(listeners[1], listen1);
+}
+
+// Verify that the listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.on('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function. Received type object$/);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/check-listener-leaks.js b/libs/shared/graph-layouts/node_modules/events/tests/check-listener-leaks.js
new file mode 100644
index 0000000000000000000000000000000000000000..7fce48f37bf24cc63a3df3e88a558a03fdbde2fa
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/check-listener-leaks.js
@@ -0,0 +1,101 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+
+// Redirect warning output to tape.
+var consoleWarn = console.warn;
+console.warn = common.test.comment;
+
+common.test.on('end', function () {
+  console.warn = consoleWarn;
+});
+
+// default
+{
+  var e = new events.EventEmitter();
+
+  for (var i = 0; i < 10; i++) {
+    e.on('default', common.mustNotCall());
+  }
+  assert.ok(!e._events['default'].hasOwnProperty('warned'));
+  e.on('default', common.mustNotCall());
+  assert.ok(e._events['default'].warned);
+
+  // specific
+  e.setMaxListeners(5);
+  for (var i = 0; i < 5; i++) {
+    e.on('specific', common.mustNotCall());
+  }
+  assert.ok(!e._events['specific'].hasOwnProperty('warned'));
+  e.on('specific', common.mustNotCall());
+  assert.ok(e._events['specific'].warned);
+
+  // only one
+  e.setMaxListeners(1);
+  e.on('only one', common.mustNotCall());
+  assert.ok(!e._events['only one'].hasOwnProperty('warned'));
+  e.on('only one', common.mustNotCall());
+  assert.ok(e._events['only one'].hasOwnProperty('warned'));
+
+  // unlimited
+  e.setMaxListeners(0);
+  for (var i = 0; i < 1000; i++) {
+    e.on('unlimited', common.mustNotCall());
+  }
+  assert.ok(!e._events['unlimited'].hasOwnProperty('warned'));
+}
+
+// process-wide
+{
+  events.EventEmitter.defaultMaxListeners = 42;
+  var e = new events.EventEmitter();
+
+  for (var i = 0; i < 42; ++i) {
+    e.on('fortytwo', common.mustNotCall());
+  }
+  assert.ok(!e._events['fortytwo'].hasOwnProperty('warned'));
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(e._events['fortytwo'].hasOwnProperty('warned'));
+  delete e._events['fortytwo'].warned;
+
+  events.EventEmitter.defaultMaxListeners = 44;
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(!e._events['fortytwo'].hasOwnProperty('warned'));
+  e.on('fortytwo', common.mustNotCall());
+  assert.ok(e._events['fortytwo'].hasOwnProperty('warned'));
+}
+
+// but _maxListeners still has precedence over defaultMaxListeners
+{
+  events.EventEmitter.defaultMaxListeners = 42;
+  var e = new events.EventEmitter();
+  e.setMaxListeners(1);
+  e.on('uno', common.mustNotCall());
+  assert.ok(!e._events['uno'].hasOwnProperty('warned'));
+  e.on('uno', common.mustNotCall());
+  assert.ok(e._events['uno'].hasOwnProperty('warned'));
+
+  // chainable
+  assert.strictEqual(e, e.setMaxListeners(1));
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/common.js b/libs/shared/graph-layouts/node_modules/events/tests/common.js
new file mode 100644
index 0000000000000000000000000000000000000000..49569b05f59d5a6c4956cc3bcea2dfd3d426d632
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/common.js
@@ -0,0 +1,104 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var test = require('tape');
+var assert = require('assert');
+
+var noop = function() {};
+
+var mustCallChecks = [];
+
+function runCallChecks(exitCode) {
+  if (exitCode !== 0) return;
+
+  var failed = filter(mustCallChecks, function(context) {
+    if ('minimum' in context) {
+      context.messageSegment = 'at least ' + context.minimum;
+      return context.actual < context.minimum;
+    } else {
+      context.messageSegment = 'exactly ' + context.exact;
+      return context.actual !== context.exact;
+    }
+  });
+
+  for (var i = 0; i < failed.length; i++) {
+    var context = failed[i];
+    console.log('Mismatched %s function calls. Expected %s, actual %d.',
+        context.name,
+        context.messageSegment,
+        context.actual);
+    // IE8 has no .stack
+    if (context.stack) console.log(context.stack.split('\n').slice(2).join('\n'));
+  }
+
+  assert.strictEqual(failed.length, 0);
+}
+
+exports.mustCall = function(fn, exact) {
+  return _mustCallInner(fn, exact, 'exact');
+};
+
+function _mustCallInner(fn, criteria, field) {
+  if (typeof criteria == 'undefined') criteria = 1;
+
+  if (typeof fn === 'number') {
+    criteria = fn;
+    fn = noop;
+  } else if (fn === undefined) {
+    fn = noop;
+  }
+
+  if (typeof criteria !== 'number')
+    throw new TypeError('Invalid ' + field + ' value: ' + criteria);
+
+  var context = {
+    actual: 0,
+    stack: (new Error()).stack,
+    name: fn.name || '<anonymous>'
+  };
+
+  context[field] = criteria;
+
+  // add the exit listener only once to avoid listener leak warnings
+  if (mustCallChecks.length === 0) test.onFinish(function() { runCallChecks(0); });
+
+  mustCallChecks.push(context);
+
+  return function() {
+    context.actual++;
+    return fn.apply(this, arguments);
+  };
+}
+
+exports.mustNotCall = function(msg) {
+  return function mustNotCall() {
+    assert.fail(msg || 'function should not have been called');
+  };
+};
+
+function filter(arr, fn) {
+  if (arr.filter) return arr.filter(fn);
+  var filtered = [];
+  for (var i = 0; i < arr.length; i++) {
+    if (fn(arr[i], i, arr)) filtered.push(arr[i]);
+  }
+  return filtered
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/errors.js b/libs/shared/graph-layouts/node_modules/events/tests/errors.js
new file mode 100644
index 0000000000000000000000000000000000000000..a23df437f05d39628251c8fa9dfb72baf72b749e
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/errors.js
@@ -0,0 +1,13 @@
+'use strict';
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var EE = new EventEmitter();
+
+assert.throws(function () {
+  EE.emit('error', 'Accepts a string');
+}, 'Error: Unhandled error. (Accepts a string)');
+
+assert.throws(function () {
+  EE.emit('error', { message: 'Error!' });
+}, 'Unhandled error. ([object Object])');
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/events-list.js b/libs/shared/graph-layouts/node_modules/events/tests/events-list.js
new file mode 100644
index 0000000000000000000000000000000000000000..08aa62177e2c291efcbcc57a9010f4db8625a05c
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/events-list.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var EE = new EventEmitter();
+var m = function() {};
+EE.on('foo', function() {});
+assert.equal(1, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+EE.on('bar', m);
+assert.equal(2, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+assert.equal('bar', EE.eventNames()[1]);
+EE.removeListener('bar', m);
+assert.equal(1, EE.eventNames().length);
+assert.equal('foo', EE.eventNames()[0]);
+
+if (typeof Symbol !== 'undefined') {
+  var s = Symbol('s');
+  EE.on(s, m);
+  assert.equal(2, EE.eventNames().length);
+  assert.equal('foo', EE.eventNames()[0]);
+  assert.equal(s, EE.eventNames()[1]);
+  EE.removeListener(s, m);
+  assert.equal(1, EE.eventNames().length);
+  assert.equal('foo', EE.eventNames()[0]);
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/events-once.js b/libs/shared/graph-layouts/node_modules/events/tests/events-once.js
new file mode 100644
index 0000000000000000000000000000000000000000..dae864963daae6fbac8b459a6358008189e82197
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/events-once.js
@@ -0,0 +1,234 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../').EventEmitter;
+var once = require('../').once;
+var has = require('has');
+var assert = require('assert');
+
+function Event(type) {
+  this.type = type;
+}
+
+function EventTargetMock() {
+  this.events = {};
+
+  this.addEventListener = common.mustCall(this.addEventListener);
+  this.removeEventListener = common.mustCall(this.removeEventListener);
+}
+
+EventTargetMock.prototype.addEventListener = function addEventListener(name, listener, options) {
+  if (!(name in this.events)) {
+    this.events[name] = { listeners: [], options: options || {} }
+  }
+  this.events[name].listeners.push(listener);
+};
+
+EventTargetMock.prototype.removeEventListener = function removeEventListener(name, callback) {
+  if (!(name in this.events)) {
+    return;
+  }
+  var event = this.events[name];
+  var stack = event.listeners;
+
+  for (var i = 0, l = stack.length; i < l; i++) {
+    if (stack[i] === callback) {
+      stack.splice(i, 1);
+      if (stack.length === 0) {
+        delete this.events[name];
+      }
+      return;
+    }
+  }
+};
+
+EventTargetMock.prototype.dispatchEvent = function dispatchEvent(arg) {
+  if (!(arg.type in this.events)) {
+    return true;
+  }
+
+  var event = this.events[arg.type];
+  var stack = event.listeners.slice();
+
+  for (var i = 0, l = stack.length; i < l; i++) {
+    stack[i].call(null, arg);
+    if (event.options.once) {
+      this.removeEventListener(arg.type, stack[i]);
+    }
+  }
+  return !arg.defaultPrevented;
+};
+
+function onceAnEvent() {
+  var ee = new EventEmitter();
+
+  process.nextTick(function () {
+    ee.emit('myevent', 42);
+  });
+
+  return once(ee, 'myevent').then(function (args) {
+    var value = args[0]
+    assert.strictEqual(value, 42);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceAnEventWithTwoArgs() {
+  var ee = new EventEmitter();
+
+  process.nextTick(function () {
+    ee.emit('myevent', 42, 24);
+  });
+
+  return once(ee, 'myevent').then(function (value) {
+    assert.strictEqual(value.length, 2);
+    assert.strictEqual(value[0], 42);
+    assert.strictEqual(value[1], 24);
+  });
+}
+
+function catchesErrors() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  var err;
+  process.nextTick(function () {
+    ee.emit('error', expected);
+  });
+
+  return once(ee, 'myevent').then(function () {
+    throw new Error('should reject')
+  }, function (err) {
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function stopListeningAfterCatchingError() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  var err;
+  process.nextTick(function () {
+    ee.emit('error', expected);
+    ee.emit('myevent', 42, 24);
+  });
+
+  // process.on('multipleResolves', common.mustNotCall());
+
+  return once(ee, 'myevent').then(common.mustNotCall, function (err) {
+    // process.removeAllListeners('multipleResolves');
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceError() {
+  var ee = new EventEmitter();
+
+  var expected = new Error('kaboom');
+  process.nextTick(function () {
+    ee.emit('error', expected);
+  });
+
+  var promise = once(ee, 'error');
+  assert.strictEqual(ee.listenerCount('error'), 1);
+  return promise.then(function (args) {
+    var err = args[0]
+    assert.strictEqual(err, expected);
+    assert.strictEqual(ee.listenerCount('error'), 0);
+    assert.strictEqual(ee.listenerCount('myevent'), 0);
+  });
+}
+
+function onceWithEventTarget() {
+  var et = new EventTargetMock();
+  var event = new Event('myevent');
+  process.nextTick(function () {
+    et.dispatchEvent(event);
+  });
+  return once(et, 'myevent').then(function (args) {
+    var value = args[0];
+    assert.strictEqual(value, event);
+    assert.strictEqual(has(et.events, 'myevent'), false);
+  });
+}
+
+function onceWithEventTargetError() {
+  var et = new EventTargetMock();
+  var error = new Event('error');
+  process.nextTick(function () {
+    et.dispatchEvent(error);
+  });
+  return once(et, 'error').then(function (args) {
+    var err = args[0];
+    assert.strictEqual(err, error);
+    assert.strictEqual(has(et.events, 'error'), false);
+  });
+}
+
+function prioritizesEventEmitter() {
+  var ee = new EventEmitter();
+  ee.addEventListener = assert.fail;
+  ee.removeAllListeners = assert.fail;
+  process.nextTick(function () {
+    ee.emit('foo');
+  });
+  return once(ee, 'foo');
+}
+
+var allTests = [
+  onceAnEvent(),
+  onceAnEventWithTwoArgs(),
+  catchesErrors(),
+  stopListeningAfterCatchingError(),
+  onceError(),
+  onceWithEventTarget(),
+  onceWithEventTargetError(),
+  prioritizesEventEmitter()
+];
+
+var hasBrowserEventTarget = false;
+try {
+  hasBrowserEventTarget = typeof (new window.EventTarget().addEventListener) === 'function' &&
+    new window.Event('xyz').type === 'xyz';
+} catch (err) {}
+
+if (hasBrowserEventTarget) {
+  var onceWithBrowserEventTarget = function onceWithBrowserEventTarget() {
+    var et = new window.EventTarget();
+    var event = new window.Event('myevent');
+    process.nextTick(function () {
+      et.dispatchEvent(event);
+    });
+    return once(et, 'myevent').then(function (args) {
+      var value = args[0];
+      assert.strictEqual(value, event);
+      assert.strictEqual(has(et.events, 'myevent'), false);
+    });
+  }
+
+  var onceWithBrowserEventTargetError = function onceWithBrowserEventTargetError() {
+    var et = new window.EventTarget();
+    var error = new window.Event('error');
+    process.nextTick(function () {
+      et.dispatchEvent(error);
+    });
+    return once(et, 'error').then(function (args) {
+      var err = args[0];
+      assert.strictEqual(err, error);
+      assert.strictEqual(has(et.events, 'error'), false);
+    });
+  }
+
+  common.test.comment('Testing with browser built-in EventTarget');
+  allTests.push([
+    onceWithBrowserEventTarget(),
+    onceWithBrowserEventTargetError()
+  ]);
+}
+
+module.exports = Promise.all(allTests);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/index.js b/libs/shared/graph-layouts/node_modules/events/tests/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d739e670ca0287debfd9bcbc6164e862c74b133
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/index.js
@@ -0,0 +1,64 @@
+var test = require('tape');
+var functionsHaveNames = require('functions-have-names');
+var hasSymbols = require('has-symbols');
+
+require('./legacy-compat');
+var common = require('./common');
+
+// we do this to easily wrap each file in a mocha test
+// and also have browserify be able to statically analyze this file
+var orig_require = require;
+var require = function(file) {
+    test(file, function(t) {
+        // Store the tape object so tests can access it.
+        t.on('end', function () { delete common.test; });
+        common.test = t;
+
+        try {
+          var exp = orig_require(file);
+          if (exp && exp.then) {
+            exp.then(function () { t.end(); }, t.fail);
+            return;
+          }
+        } catch (err) {
+          t.fail(err);
+        }
+        t.end();
+    });
+};
+
+require('./add-listeners.js');
+require('./check-listener-leaks.js');
+require('./errors.js');
+require('./events-list.js');
+if (typeof Promise === 'function') {
+  require('./events-once.js');
+} else {
+  // Promise support is not available.
+  test('./events-once.js', { skip: true }, function () {});
+}
+require('./listener-count.js');
+require('./listeners-side-effects.js');
+require('./listeners.js');
+require('./max-listeners.js');
+if (functionsHaveNames()) {
+  require('./method-names.js');
+} else {
+  // Function.name is not supported in IE
+  test('./method-names.js', { skip: true }, function () {});
+}
+require('./modify-in-emit.js');
+require('./num-args.js');
+require('./once.js');
+require('./prepend.js');
+require('./set-max-listeners-side-effects.js');
+require('./special-event-names.js');
+require('./subclass.js');
+if (hasSymbols()) {
+  require('./symbols.js');
+} else {
+  // Symbol is not available.
+  test('./symbols.js', { skip: true }, function () {});
+}
+require('./remove-all-listeners.js');
+require('./remove-listeners.js');
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/legacy-compat.js b/libs/shared/graph-layouts/node_modules/events/tests/legacy-compat.js
new file mode 100644
index 0000000000000000000000000000000000000000..a402be6e2f42d11acafd5dea42fabe0a21df06c6
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/legacy-compat.js
@@ -0,0 +1,16 @@
+// sigh... life is hard
+if (!global.console) {
+    console = {}
+}
+
+var fns = ['log', 'error', 'trace'];
+for (var i=0 ; i<fns.length ; ++i) {
+    var fn = fns[i];
+    if (!console[fn]) {
+        console[fn] = function() {};
+    }
+}
+
+if (!Array.isArray) {
+    Array.isArray = require('isarray');
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/listener-count.js b/libs/shared/graph-layouts/node_modules/events/tests/listener-count.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d422d872a4ddaa57b323604aa893d53269d32f1
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/listener-count.js
@@ -0,0 +1,37 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var emitter = new EventEmitter();
+emitter.on('foo', function() {});
+emitter.on('foo', function() {});
+emitter.on('baz', function() {});
+// Allow any type
+emitter.on(123, function() {});
+
+assert.strictEqual(EventEmitter.listenerCount(emitter, 'foo'), 2);
+assert.strictEqual(emitter.listenerCount('foo'), 2);
+assert.strictEqual(emitter.listenerCount('bar'), 0);
+assert.strictEqual(emitter.listenerCount('baz'), 1);
+assert.strictEqual(emitter.listenerCount(123), 1);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/listeners-side-effects.js b/libs/shared/graph-layouts/node_modules/events/tests/listeners-side-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..180f833128b0732e4942727f00be02fac2e06d5c
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/listeners-side-effects.js
@@ -0,0 +1,56 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+
+var EventEmitter = require('../').EventEmitter;
+
+var e = new EventEmitter();
+var fl;  // foo listeners
+
+fl = e.listeners('foo');
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 0);
+if (Object.create) assert.ok(!(e._events instanceof Object));
+assert.strictEqual(Object.keys(e._events).length, 0);
+
+e.on('foo', assert.fail);
+fl = e.listeners('foo');
+assert.strictEqual(e._events.foo, assert.fail);
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 1);
+assert.strictEqual(fl[0], assert.fail);
+
+e.listeners('bar');
+
+e.on('foo', assert.ok);
+fl = e.listeners('foo');
+
+assert.ok(Array.isArray(e._events.foo));
+assert.strictEqual(e._events.foo.length, 2);
+assert.strictEqual(e._events.foo[0], assert.fail);
+assert.strictEqual(e._events.foo[1], assert.ok);
+
+assert.ok(Array.isArray(fl));
+assert.strictEqual(fl.length, 2);
+assert.strictEqual(fl[0], assert.fail);
+assert.strictEqual(fl[1], assert.ok);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/listeners.js b/libs/shared/graph-layouts/node_modules/events/tests/listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..1909d2dfe2d1879c57ee2224749b491c9be495da
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/listeners.js
@@ -0,0 +1,168 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+var util = require('util');
+
+function listener() {}
+function listener2() {}
+function listener3() {
+  return 0;
+}
+function listener4() {
+  return 1;
+}
+
+function TestStream() {}
+util.inherits(TestStream, events.EventEmitter);
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var fooListeners = ee.listeners('foo');
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  ee.removeAllListeners('foo');
+  listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+
+  assert.ok(Array.isArray(fooListeners));
+  assert.strictEqual(fooListeners.length, 1);
+  assert.strictEqual(fooListeners[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+
+  var eeListenersCopy = ee.listeners('foo');
+  assert.ok(Array.isArray(eeListenersCopy));
+  assert.strictEqual(eeListenersCopy.length, 1);
+  assert.strictEqual(eeListenersCopy[0], listener);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  eeListenersCopy.push(listener2);
+  listeners = ee.listeners('foo');
+  
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+
+  assert.strictEqual(eeListenersCopy.length, 2);
+  assert.strictEqual(eeListenersCopy[0], listener);
+  assert.strictEqual(eeListenersCopy[1], listener2);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var eeListenersCopy = ee.listeners('foo');
+  ee.on('foo', listener2);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener);
+  assert.strictEqual(listeners[1], listener2);
+
+  assert.ok(Array.isArray(eeListenersCopy));
+  assert.strictEqual(eeListenersCopy.length, 1);
+  assert.strictEqual(eeListenersCopy[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.once('foo', listener);
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  ee.once('foo', listener2);
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener);
+  assert.strictEqual(listeners[1], listener2);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee._events = undefined;
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var s = new TestStream();
+  var listeners = s.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', listener);
+  var wrappedListener = ee.rawListeners('foo');
+  assert.strictEqual(wrappedListener.length, 1);
+  assert.strictEqual(wrappedListener[0], listener);
+  assert.notStrictEqual(wrappedListener, ee.rawListeners('foo'));
+  ee.once('foo', listener);
+  var wrappedListeners = ee.rawListeners('foo');
+  assert.strictEqual(wrappedListeners.length, 2);
+  assert.strictEqual(wrappedListeners[0], listener);
+  assert.notStrictEqual(wrappedListeners[1], listener);
+  assert.strictEqual(wrappedListeners[1].listener, listener);
+  assert.notStrictEqual(wrappedListeners, ee.rawListeners('foo'));
+  ee.emit('foo');
+  assert.strictEqual(wrappedListeners.length, 2);
+  assert.strictEqual(wrappedListeners[1].listener, listener);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.once('foo', listener3);
+  ee.on('foo', listener4);
+  var rawListeners = ee.rawListeners('foo');
+  assert.strictEqual(rawListeners.length, 2);
+  assert.strictEqual(rawListeners[0](), 0);
+  var rawListener = ee.rawListeners('foo');
+  assert.strictEqual(rawListener.length, 1);
+  assert.strictEqual(rawListener[0](), 1);
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/max-listeners.js b/libs/shared/graph-layouts/node_modules/events/tests/max-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b43953853252a2a1a3684899304e568f9259fb9
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/max-listeners.js
@@ -0,0 +1,47 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+var e = new events.EventEmitter();
+
+var hasDefineProperty = !!Object.defineProperty;
+try { Object.defineProperty({}, 'x', { value: 0 }); } catch (err) { hasDefineProperty = false }
+
+e.on('maxListeners', common.mustCall());
+
+// Should not corrupt the 'maxListeners' queue.
+e.setMaxListeners(42);
+
+var throwsObjs = [NaN, -1, 'and even this'];
+var maxError = /^RangeError: The value of "n" is out of range\. It must be a non-negative number\./;
+var defError = /^RangeError: The value of "defaultMaxListeners" is out of range\. It must be a non-negative number\./;
+
+for (var i = 0; i < throwsObjs.length; i++) {
+  var obj = throwsObjs[i];
+  assert.throws(function() { e.setMaxListeners(obj); }, maxError);
+  if (hasDefineProperty) {
+    assert.throws(function() { events.defaultMaxListeners = obj; }, defError);
+  }
+}
+
+e.emit('maxListeners');
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/method-names.js b/libs/shared/graph-layouts/node_modules/events/tests/method-names.js
new file mode 100644
index 0000000000000000000000000000000000000000..364a161fece00a8f525cd8d7f3a61539341a6d59
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/method-names.js
@@ -0,0 +1,35 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var E = events.EventEmitter.prototype;
+assert.strictEqual(E.constructor.name, 'EventEmitter');
+assert.strictEqual(E.on, E.addListener);  // Same method.
+assert.strictEqual(E.off, E.removeListener);  // Same method.
+Object.getOwnPropertyNames(E).forEach(function(name) {
+  if (name === 'constructor' || name === 'on' || name === 'off') return;
+  if (typeof E[name] !== 'function') return;
+  assert.strictEqual(E[name].name, name);
+});
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/modify-in-emit.js b/libs/shared/graph-layouts/node_modules/events/tests/modify-in-emit.js
new file mode 100644
index 0000000000000000000000000000000000000000..53fa63395c620287e36ed5329443474d22daa241
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/modify-in-emit.js
@@ -0,0 +1,90 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var callbacks_called = [];
+
+var e = new events.EventEmitter();
+
+function callback1() {
+  callbacks_called.push('callback1');
+  e.on('foo', callback2);
+  e.on('foo', callback3);
+  e.removeListener('foo', callback1);
+}
+
+function callback2() {
+  callbacks_called.push('callback2');
+  e.removeListener('foo', callback2);
+}
+
+function callback3() {
+  callbacks_called.push('callback3');
+  e.removeListener('foo', callback3);
+}
+
+e.on('foo', callback1);
+assert.strictEqual(e.listeners('foo').length, 1);
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 2);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 1);
+assert.strictEqual(callbacks_called[0], 'callback1');
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 3);
+assert.strictEqual(callbacks_called[0], 'callback1');
+assert.strictEqual(callbacks_called[1], 'callback2');
+assert.strictEqual(callbacks_called[2], 'callback3');
+
+e.emit('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 3);
+assert.strictEqual(callbacks_called[0], 'callback1');
+assert.strictEqual(callbacks_called[1], 'callback2');
+assert.strictEqual(callbacks_called[2], 'callback3');
+
+e.on('foo', callback1);
+e.on('foo', callback2);
+assert.strictEqual(e.listeners('foo').length, 2);
+e.removeAllListeners('foo');
+assert.strictEqual(e.listeners('foo').length, 0);
+
+// Verify that removing callbacks while in emit allows emits to propagate to
+// all listeners
+callbacks_called = [];
+
+e.on('foo', callback2);
+e.on('foo', callback3);
+assert.strictEqual(2, e.listeners('foo').length);
+e.emit('foo');
+assert.ok(Array.isArray(callbacks_called));
+assert.strictEqual(callbacks_called.length, 2);
+assert.strictEqual(callbacks_called[0], 'callback2');
+assert.strictEqual(callbacks_called[1], 'callback3');
+assert.strictEqual(0, e.listeners('foo').length);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/num-args.js b/libs/shared/graph-layouts/node_modules/events/tests/num-args.js
new file mode 100644
index 0000000000000000000000000000000000000000..c9b0deb9c960b3e1037424bf717986da0222bb73
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/num-args.js
@@ -0,0 +1,60 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var e = new events.EventEmitter();
+var num_args_emitted = [];
+
+e.on('numArgs', function() {
+  var numArgs = arguments.length;
+  num_args_emitted.push(numArgs);
+});
+
+e.on('foo', function() {
+  num_args_emitted.push(arguments.length);
+});
+
+e.on('foo', function() {
+  num_args_emitted.push(arguments.length);
+});
+
+e.emit('numArgs');
+e.emit('numArgs', null);
+e.emit('numArgs', null, null);
+e.emit('numArgs', null, null, null);
+e.emit('numArgs', null, null, null, null);
+e.emit('numArgs', null, null, null, null, null);
+
+e.emit('foo', null, null, null, null);
+
+assert.ok(Array.isArray(num_args_emitted));
+assert.strictEqual(num_args_emitted.length, 8);
+assert.strictEqual(num_args_emitted[0], 0);
+assert.strictEqual(num_args_emitted[1], 1);
+assert.strictEqual(num_args_emitted[2], 2);
+assert.strictEqual(num_args_emitted[3], 3);
+assert.strictEqual(num_args_emitted[4], 4);
+assert.strictEqual(num_args_emitted[5], 5);
+assert.strictEqual(num_args_emitted[6], 4);
+assert.strictEqual(num_args_emitted[6], 4);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/once.js b/libs/shared/graph-layouts/node_modules/events/tests/once.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b36c055e4e31b94109f0d089f9eb2244e787f6b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/once.js
@@ -0,0 +1,83 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var e = new EventEmitter();
+
+e.once('hello', common.mustCall());
+
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+e.emit('hello', 'a', 'b');
+
+function remove() {
+  assert.fail('once->foo should not be emitted');
+}
+
+e.once('foo', remove);
+e.removeListener('foo', remove);
+e.emit('foo');
+
+e.once('e', common.mustCall(function() {
+  e.emit('e');
+}));
+
+e.once('e', common.mustCall());
+
+e.emit('e');
+
+// Verify that the listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.once('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function. Received type object$/);
+
+{
+  // once() has different code paths based on the number of arguments being
+  // emitted. Verify that all of the cases are covered.
+  var maxArgs = 4;
+
+  for (var i = 0; i <= maxArgs; ++i) {
+    var ee = new EventEmitter();
+    var args = ['foo'];
+
+    for (var j = 0; j < i; ++j)
+      args.push(j);
+
+    ee.once('foo', common.mustCall(function() {
+      var params = Array.prototype.slice.call(arguments);
+      var restArgs = args.slice(1);
+      assert.ok(Array.isArray(params));
+      assert.strictEqual(params.length, restArgs.length);
+      for (var index = 0; index < params.length; index++) {
+        var param = params[index];
+        assert.strictEqual(param, restArgs[index]);
+      }
+  	}));
+
+    EventEmitter.prototype.emit.apply(ee, args);
+  }
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/prepend.js b/libs/shared/graph-layouts/node_modules/events/tests/prepend.js
new file mode 100644
index 0000000000000000000000000000000000000000..79afde0bf3971c638041df756918c25f239e8d79
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/prepend.js
@@ -0,0 +1,31 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var myEE = new EventEmitter();
+var m = 0;
+// This one comes last.
+myEE.on('foo', common.mustCall(function () {
+  assert.strictEqual(m, 2);
+}));
+
+// This one comes second.
+myEE.prependListener('foo', common.mustCall(function () {
+  assert.strictEqual(m++, 1);
+}));
+
+// This one comes first.
+myEE.prependOnceListener('foo',
+                         common.mustCall(function () {
+                           assert.strictEqual(m++, 0);
+                         }));
+
+myEE.emit('foo');
+
+// Verify that the listener must be a function
+assert.throws(function () {
+  var ee = new EventEmitter();
+  ee.prependOnceListener('foo', null);
+}, 'TypeError: The "listener" argument must be of type Function. Received type object');
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/remove-all-listeners.js b/libs/shared/graph-layouts/node_modules/events/tests/remove-all-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..622941cfa604c0af52faabea35868f32ebf1237f
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/remove-all-listeners.js
@@ -0,0 +1,133 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var events = require('../');
+var test = require('tape');
+
+function expect(expected) {
+  var actual = [];
+  test.onFinish(function() {
+    var sortedActual = actual.sort();
+    var sortedExpected = expected.sort();
+    assert.strictEqual(sortedActual.length, sortedExpected.length);
+    for (var index = 0; index < sortedActual.length; index++) {
+      var value = sortedActual[index];
+      assert.strictEqual(value, sortedExpected[index]);
+    }
+  });
+  function listener(name) {
+    actual.push(name);
+  }
+  return common.mustCall(listener, expected.length);
+}
+
+{
+  var ee = new events.EventEmitter();
+  var noop = common.mustNotCall();
+  ee.on('foo', noop);
+  ee.on('bar', noop);
+  ee.on('baz', noop);
+  ee.on('baz', noop);
+  var fooListeners = ee.listeners('foo');
+  var barListeners = ee.listeners('bar');
+  var bazListeners = ee.listeners('baz');
+  ee.on('removeListener', expect(['bar', 'baz', 'baz']));
+  ee.removeAllListeners('bar');
+  ee.removeAllListeners('baz');
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], noop);
+
+  listeners = ee.listeners('bar');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  listeners = ee.listeners('baz');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  // After calling removeAllListeners(),
+  // the old listeners array should stay unchanged.
+  assert.strictEqual(fooListeners.length, 1);
+  assert.strictEqual(fooListeners[0], noop);
+  assert.strictEqual(barListeners.length, 1);
+  assert.strictEqual(barListeners[0], noop);
+  assert.strictEqual(bazListeners.length, 2);
+  assert.strictEqual(bazListeners[0], noop);
+  assert.strictEqual(bazListeners[1], noop);
+  // After calling removeAllListeners(),
+  // new listeners arrays is different from the old.
+  assert.notStrictEqual(ee.listeners('bar'), barListeners);
+  assert.notStrictEqual(ee.listeners('baz'), bazListeners);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('foo', common.mustNotCall());
+  ee.on('bar', common.mustNotCall());
+  // Expect LIFO order
+  ee.on('removeListener', expect(['foo', 'bar', 'removeListener']));
+  ee.on('removeListener', expect(['foo', 'bar']));
+  ee.removeAllListeners();
+
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+  listeners = ee.listeners('bar');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee.on('removeListener', common.mustNotCall());
+  // Check for regression where removeAllListeners() throws when
+  // there exists a 'removeListener' listener, but there exists
+  // no listeners for the provided event type.
+  assert.doesNotThrow(function () { ee.removeAllListeners(ee, 'foo') });
+}
+
+{
+  var ee = new events.EventEmitter();
+  var expectLength = 2;
+  ee.on('removeListener', function() {
+    assert.strictEqual(expectLength--, this.listeners('baz').length);
+  });
+  ee.on('baz', common.mustNotCall());
+  ee.on('baz', common.mustNotCall());
+  ee.on('baz', common.mustNotCall());
+  assert.strictEqual(ee.listeners('baz').length, expectLength + 1);
+  ee.removeAllListeners('baz');
+  assert.strictEqual(ee.listeners('baz').length, 0);
+}
+
+{
+  var ee = new events.EventEmitter();
+  assert.strictEqual(ee, ee.removeAllListeners());
+}
+
+{
+  var ee = new events.EventEmitter();
+  ee._events = undefined;
+  assert.strictEqual(ee, ee.removeAllListeners());
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/remove-listeners.js b/libs/shared/graph-layouts/node_modules/events/tests/remove-listeners.js
new file mode 100644
index 0000000000000000000000000000000000000000..18e4d1651fa2546a810774fd59147aee25624154
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/remove-listeners.js
@@ -0,0 +1,212 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var assert = require('assert');
+var EventEmitter = require('../');
+
+var listener1 = function listener1() {};
+var listener2 = function listener2() {};
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+  }));
+  ee.removeListener('hello', listener1);
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('removeListener', common.mustNotCall());
+  ee.removeListener('hello', listener2);
+
+  var listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener1);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('hello', listener2);
+
+  var listeners;
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 1);
+    assert.strictEqual(listeners[0], listener2);
+  }));
+  ee.removeListener('hello', listener1);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 1);
+  assert.strictEqual(listeners[0], listener2);
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener2);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  }));
+  ee.removeListener('hello', listener2);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+
+  function remove1() {
+    assert.fail('remove1 should not have been called');
+  }
+
+  function remove2() {
+    assert.fail('remove2 should not have been called');
+  }
+
+  ee.on('removeListener', common.mustCall(function(name, cb) {
+    if (cb !== remove1) return;
+    this.removeListener('quux', remove2);
+    this.emit('quux');
+  }, 2));
+  ee.on('quux', remove1);
+  ee.on('quux', remove2);
+  ee.removeListener('quux', remove1);
+}
+
+{
+  var ee = new EventEmitter();
+  ee.on('hello', listener1);
+  ee.on('hello', listener2);
+
+  var listeners;
+  ee.once('removeListener', common.mustCall(function(name, cb) {
+    assert.strictEqual(name, 'hello');
+    assert.strictEqual(cb, listener1);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 1);
+    assert.strictEqual(listeners[0], listener2);
+    ee.once('removeListener', common.mustCall(function(name, cb) {
+      assert.strictEqual(name, 'hello');
+      assert.strictEqual(cb, listener2);
+      listeners = ee.listeners('hello');
+      assert.ok(Array.isArray(listeners));
+      assert.strictEqual(listeners.length, 0);
+    }));
+    ee.removeListener('hello', listener2);
+    listeners = ee.listeners('hello');
+    assert.ok(Array.isArray(listeners));
+    assert.strictEqual(listeners.length, 0);
+  }));
+  ee.removeListener('hello', listener1);
+  listeners = ee.listeners('hello');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 0);
+}
+
+{
+  var ee = new EventEmitter();
+  var listener3 = common.mustCall(function() {
+    ee.removeListener('hello', listener4);
+  }, 2);
+  var listener4 = common.mustCall();
+
+  ee.on('hello', listener3);
+  ee.on('hello', listener4);
+
+  // listener4 will still be called although it is removed by listener 3.
+  ee.emit('hello');
+  // This is so because the interal listener array at time of emit
+  // was [listener3,listener4]
+
+  // Interal listener array [listener3]
+  ee.emit('hello');
+}
+
+{
+  var ee = new EventEmitter();
+
+  ee.once('hello', listener1);
+  ee.on('removeListener', common.mustCall(function(eventName, listener) {
+    assert.strictEqual(eventName, 'hello');
+    assert.strictEqual(listener, listener1);
+  }));
+  ee.emit('hello');
+}
+
+{
+  var ee = new EventEmitter();
+
+  assert.strictEqual(ee, ee.removeListener('foo', function() {}));
+}
+
+// Verify that the removed listener must be a function
+assert.throws(function() {
+  var ee = new EventEmitter();
+
+  ee.removeListener('foo', null);
+}, /^TypeError: The "listener" argument must be of type Function\. Received type object$/);
+
+{
+  var ee = new EventEmitter();
+  var listener = function() {};
+  ee._events = undefined;
+  var e = ee.removeListener('foo', listener);
+  assert.strictEqual(e, ee);
+}
+
+{
+  var ee = new EventEmitter();
+
+  ee.on('foo', listener1);
+  ee.on('foo', listener2);
+  var listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener1);
+  assert.strictEqual(listeners[1], listener2);
+
+  ee.removeListener('foo', listener1);
+  assert.strictEqual(ee._events.foo, listener2);
+
+  ee.on('foo', listener1);
+  listeners = ee.listeners('foo');
+  assert.ok(Array.isArray(listeners));
+  assert.strictEqual(listeners.length, 2);
+  assert.strictEqual(listeners[0], listener2);
+  assert.strictEqual(listeners[1], listener1);
+
+  ee.removeListener('foo', listener1);
+  assert.strictEqual(ee._events.foo, listener2);
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/set-max-listeners-side-effects.js b/libs/shared/graph-layouts/node_modules/events/tests/set-max-listeners-side-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..13dbb671e9024280b2e9be42206a23b2db355074
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/set-max-listeners-side-effects.js
@@ -0,0 +1,31 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require('./common');
+var assert = require('assert');
+var events = require('../');
+
+var e = new events.EventEmitter();
+
+if (Object.create) assert.ok(!(e._events instanceof Object));
+assert.strictEqual(Object.keys(e._events).length, 0);
+e.setMaxListeners(5);
+assert.strictEqual(Object.keys(e._events).length, 0);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/special-event-names.js b/libs/shared/graph-layouts/node_modules/events/tests/special-event-names.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2f0b744a706c9ae6181a9723b2b92492df7e4d1
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/special-event-names.js
@@ -0,0 +1,45 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var ee = new EventEmitter();
+var handler = function() {};
+
+assert.strictEqual(ee.eventNames().length, 0);
+
+assert.strictEqual(ee._events.hasOwnProperty, undefined);
+assert.strictEqual(ee._events.toString, undefined);
+
+ee.on('__defineGetter__', handler);
+ee.on('toString', handler);
+ee.on('__proto__', handler);
+
+assert.strictEqual(ee.eventNames()[0], '__defineGetter__');
+assert.strictEqual(ee.eventNames()[1], 'toString');
+
+assert.strictEqual(ee.listeners('__defineGetter__').length, 1);
+assert.strictEqual(ee.listeners('__defineGetter__')[0], handler);
+assert.strictEqual(ee.listeners('toString').length, 1);
+assert.strictEqual(ee.listeners('toString')[0], handler);
+
+// Only run __proto__ tests if that property can actually be set
+if ({ __proto__: 'ok' }.__proto__ === 'ok') {
+  assert.strictEqual(ee.eventNames().length, 3);
+  assert.strictEqual(ee.eventNames()[2], '__proto__');
+  assert.strictEqual(ee.listeners('__proto__').length, 1);
+  assert.strictEqual(ee.listeners('__proto__')[0], handler);
+
+  ee.on('__proto__', common.mustCall(function(val) {
+    assert.strictEqual(val, 1);
+  }));
+  ee.emit('__proto__', 1);
+
+  process.on('__proto__', common.mustCall(function(val) {
+    assert.strictEqual(val, 1);
+  }));
+  process.emit('__proto__', 1);
+} else {
+  console.log('# skipped __proto__')
+}
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/subclass.js b/libs/shared/graph-layouts/node_modules/events/tests/subclass.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd033fff4d26696cddcb6a87f25da89d67a04e59
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/subclass.js
@@ -0,0 +1,66 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('./common');
+var test = require('tape');
+var assert = require('assert');
+var EventEmitter = require('../').EventEmitter;
+var util = require('util');
+
+util.inherits(MyEE, EventEmitter);
+
+function MyEE(cb) {
+  this.once(1, cb);
+  this.emit(1);
+  this.removeAllListeners();
+  EventEmitter.call(this);
+}
+
+var myee = new MyEE(common.mustCall());
+
+
+util.inherits(ErrorEE, EventEmitter);
+function ErrorEE() {
+  this.emit('error', new Error('blerg'));
+}
+
+assert.throws(function() {
+  new ErrorEE();
+}, /blerg/);
+
+test.onFinish(function() {
+  assert.ok(!(myee._events instanceof Object));
+  assert.strictEqual(Object.keys(myee._events).length, 0);
+});
+
+
+function MyEE2() {
+  EventEmitter.call(this);
+}
+
+MyEE2.prototype = new EventEmitter();
+
+var ee1 = new MyEE2();
+var ee2 = new MyEE2();
+
+ee1.on('x', function() {});
+
+assert.strictEqual(ee2.listenerCount('x'), 0);
diff --git a/libs/shared/graph-layouts/node_modules/events/tests/symbols.js b/libs/shared/graph-layouts/node_modules/events/tests/symbols.js
new file mode 100644
index 0000000000000000000000000000000000000000..0721f0ec0b5d6ef520f30eb3c711551eaabd2204
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/events/tests/symbols.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var common = require('./common');
+var EventEmitter = require('../');
+var assert = require('assert');
+
+var ee = new EventEmitter();
+var foo = Symbol('foo');
+var listener = common.mustCall();
+
+ee.on(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 1);
+assert.strictEqual(ee.listeners(foo)[0], listener);
+
+ee.emit(foo);
+
+ee.removeAllListeners();
+assert.strictEqual(ee.listeners(foo).length, 0);
+
+ee.on(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 1);
+assert.strictEqual(ee.listeners(foo)[0], listener);
+
+ee.removeListener(foo, listener);
+assert.strictEqual(ee.listeners(foo).length, 0);
diff --git a/libs/shared/graph-layouts/node_modules/graphology/LICENSE.txt b/libs/shared/graph-layouts/node_modules/graphology/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..158967c8da93f1ea5ab5ac8efa7d7269392a0737
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layouts/node_modules/graphology/README.md b/libs/shared/graph-layouts/node_modules/graphology/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..cb1dd44524f0dc851423a35ffaeb09e4c5712936
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/README.md
@@ -0,0 +1,9 @@
+# Graphology
+
+`graphology` is a specification for a robust & multipurpose JavaScript `Graph` object and aiming at supporting various kinds of graphs under a same unified interface.
+
+You will also find here the source for the reference implementation of this specification.
+
+## Documentation
+
+Full documentation for the library/specs is available [here](https://graphology.github.io).
diff --git a/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.cjs.js b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.cjs.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bd8ac9c7830be84464ffb288f5e5deba8119d69
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.cjs.js
@@ -0,0 +1,5463 @@
+'use strict';
+
+var events = require('events');
+var Iterator = require('obliterator/iterator');
+var take = require('obliterator/take');
+var chain = require('obliterator/chain');
+
+function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
+
+var Iterator__default = /*#__PURE__*/_interopDefaultLegacy(Iterator);
+var take__default = /*#__PURE__*/_interopDefaultLegacy(take);
+var chain__default = /*#__PURE__*/_interopDefaultLegacy(chain);
+
+function _typeof(obj) {
+  "@babel/helpers - typeof";
+
+  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+    return typeof obj;
+  } : function (obj) {
+    return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+  }, _typeof(obj);
+}
+
+function _inheritsLoose(subClass, superClass) {
+  subClass.prototype = Object.create(superClass.prototype);
+  subClass.prototype.constructor = subClass;
+
+  _setPrototypeOf(subClass, superClass);
+}
+
+function _getPrototypeOf(o) {
+  _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
+    return o.__proto__ || Object.getPrototypeOf(o);
+  };
+  return _getPrototypeOf(o);
+}
+
+function _setPrototypeOf(o, p) {
+  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
+    o.__proto__ = p;
+    return o;
+  };
+
+  return _setPrototypeOf(o, p);
+}
+
+function _isNativeReflectConstruct() {
+  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
+  if (Reflect.construct.sham) return false;
+  if (typeof Proxy === "function") return true;
+
+  try {
+    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+function _construct(Parent, args, Class) {
+  if (_isNativeReflectConstruct()) {
+    _construct = Reflect.construct;
+  } else {
+    _construct = function _construct(Parent, args, Class) {
+      var a = [null];
+      a.push.apply(a, args);
+      var Constructor = Function.bind.apply(Parent, a);
+      var instance = new Constructor();
+      if (Class) _setPrototypeOf(instance, Class.prototype);
+      return instance;
+    };
+  }
+
+  return _construct.apply(null, arguments);
+}
+
+function _isNativeFunction(fn) {
+  return Function.toString.call(fn).indexOf("[native code]") !== -1;
+}
+
+function _wrapNativeSuper(Class) {
+  var _cache = typeof Map === "function" ? new Map() : undefined;
+
+  _wrapNativeSuper = function _wrapNativeSuper(Class) {
+    if (Class === null || !_isNativeFunction(Class)) return Class;
+
+    if (typeof Class !== "function") {
+      throw new TypeError("Super expression must either be null or a function");
+    }
+
+    if (typeof _cache !== "undefined") {
+      if (_cache.has(Class)) return _cache.get(Class);
+
+      _cache.set(Class, Wrapper);
+    }
+
+    function Wrapper() {
+      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
+    }
+
+    Wrapper.prototype = Object.create(Class.prototype, {
+      constructor: {
+        value: Wrapper,
+        enumerable: false,
+        writable: true,
+        configurable: true
+      }
+    });
+    return _setPrototypeOf(Wrapper, Class);
+  };
+
+  return _wrapNativeSuper(Class);
+}
+
+function _assertThisInitialized(self) {
+  if (self === void 0) {
+    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+  }
+
+  return self;
+}
+
+/**
+ * Graphology Utilities
+ * =====================
+ *
+ * Collection of helpful functions used by the implementation.
+ */
+
+/**
+ * Object.assign-like polyfill.
+ *
+ * @param  {object} target       - First object.
+ * @param  {object} [...objects] - Objects to merge.
+ * @return {object}
+ */
+function assignPolyfill() {
+  var target = arguments[0];
+
+  for (var i = 1, l = arguments.length; i < l; i++) {
+    if (!arguments[i]) continue;
+
+    for (var k in arguments[i]) {
+      target[k] = arguments[i][k];
+    }
+  }
+
+  return target;
+}
+
+var assign = assignPolyfill;
+if (typeof Object.assign === 'function') assign = Object.assign;
+/**
+ * Function returning the first matching edge for given path.
+ * Note: this function does not check the existence of source & target. This
+ * must be performed by the caller.
+ *
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @param  {any}    target - Target node.
+ * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+ * @return {string|null}
+ */
+
+function getMatchingEdge(graph, source, target, type) {
+  var sourceData = graph._nodes.get(source);
+
+  var edge = null;
+  if (!sourceData) return edge;
+
+  if (type === 'mixed') {
+    edge = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target];
+  } else if (type === 'directed') {
+    edge = sourceData.out && sourceData.out[target];
+  } else {
+    edge = sourceData.undirected && sourceData.undirected[target];
+  }
+
+  return edge;
+}
+/**
+ * Checks whether the given value is a Graph implementation instance.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+
+function isGraph(value) {
+  return value !== null && _typeof(value) === 'object' && typeof value.addUndirectedEdgeWithKey === 'function' && typeof value.dropNode === 'function';
+}
+/**
+ * Checks whether the given value is a plain object.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+
+function isPlainObject(value) {
+  return _typeof(value) === 'object' && value !== null && value.constructor === Object;
+}
+/**
+ * Checks whether the given object is empty.
+ *
+ * @param  {object}  o - Target Object.
+ * @return {boolean}
+ */
+
+function isEmpty(o) {
+  var k;
+
+  for (k in o) {
+    return false;
+  }
+
+  return true;
+}
+/**
+ * Creates a "private" property for the given member name by concealing it
+ * using the `enumerable` option.
+ *
+ * @param {object} target - Target object.
+ * @param {string} name   - Member name.
+ */
+
+function privateProperty(target, name, value) {
+  Object.defineProperty(target, name, {
+    enumerable: false,
+    configurable: false,
+    writable: true,
+    value: value
+  });
+}
+/**
+ * Creates a read-only property for the given member name & the given getter.
+ *
+ * @param {object}   target - Target object.
+ * @param {string}   name   - Member name.
+ * @param {mixed}    value  - The attached getter or fixed value.
+ */
+
+function readOnlyProperty(target, name, value) {
+  var descriptor = {
+    enumerable: true,
+    configurable: true
+  };
+
+  if (typeof value === 'function') {
+    descriptor.get = value;
+  } else {
+    descriptor.value = value;
+    descriptor.writable = false;
+  }
+
+  Object.defineProperty(target, name, descriptor);
+}
+/**
+ * Returns whether the given object constitute valid hints.
+ *
+ * @param {object} hints - Target object.
+ */
+
+function validateHints(hints) {
+  if (!isPlainObject(hints)) return false;
+  if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+  return true;
+}
+/**
+ * Creates a function generating incremental ids for edges.
+ *
+ * @return {function}
+ */
+
+function incrementalIdStartingFromRandomByte() {
+  var i = Math.floor(Math.random() * 256) & 0xff;
+  return function () {
+    return i++;
+  };
+}
+
+/**
+ * Graphology Custom Errors
+ * =========================
+ *
+ * Defining custom errors for ease of use & easy unit tests across
+ * implementations (normalized typology rather than relying on error
+ * messages to check whether the correct error was found).
+ */
+var GraphError = /*#__PURE__*/function (_Error) {
+  _inheritsLoose(GraphError, _Error);
+
+  function GraphError(message) {
+    var _this;
+
+    _this = _Error.call(this) || this;
+    _this.name = 'GraphError';
+    _this.message = message;
+    return _this;
+  }
+
+  return GraphError;
+}( /*#__PURE__*/_wrapNativeSuper(Error));
+var InvalidArgumentsGraphError = /*#__PURE__*/function (_GraphError) {
+  _inheritsLoose(InvalidArgumentsGraphError, _GraphError);
+
+  function InvalidArgumentsGraphError(message) {
+    var _this2;
+
+    _this2 = _GraphError.call(this, message) || this;
+    _this2.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this2), InvalidArgumentsGraphError.prototype.constructor);
+    return _this2;
+  }
+
+  return InvalidArgumentsGraphError;
+}(GraphError);
+var NotFoundGraphError = /*#__PURE__*/function (_GraphError2) {
+  _inheritsLoose(NotFoundGraphError, _GraphError2);
+
+  function NotFoundGraphError(message) {
+    var _this3;
+
+    _this3 = _GraphError2.call(this, message) || this;
+    _this3.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this3), NotFoundGraphError.prototype.constructor);
+    return _this3;
+  }
+
+  return NotFoundGraphError;
+}(GraphError);
+var UsageGraphError = /*#__PURE__*/function (_GraphError3) {
+  _inheritsLoose(UsageGraphError, _GraphError3);
+
+  function UsageGraphError(message) {
+    var _this4;
+
+    _this4 = _GraphError3.call(this, message) || this;
+    _this4.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability
+
+    if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this4), UsageGraphError.prototype.constructor);
+    return _this4;
+  }
+
+  return UsageGraphError;
+}(GraphError);
+
+/**
+ * Graphology Internal Data Classes
+ * =================================
+ *
+ * Internal classes hopefully reduced to structs by engines & storing
+ * necessary information for nodes & edges.
+ *
+ * Note that those classes don't rely on the `class` keyword to avoid some
+ * cruft introduced by most of ES2015 transpilers.
+ */
+
+/**
+ * MixedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function MixedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+MixedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+  this.undirectedDegree = 0; // Indices
+
+  this["in"] = {};
+  this.out = {};
+  this.undirected = {};
+};
+/**
+ * DirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+
+
+function DirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+DirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0; // Indices
+
+  this["in"] = {};
+  this.out = {};
+};
+/**
+ * UndirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+
+
+function UndirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.clear();
+}
+
+UndirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.undirectedDegree = 0; // Indices
+
+  this.undirected = {};
+};
+/**
+ * EdgeData class.
+ *
+ * @constructor
+ * @param {boolean} undirected   - Whether the edge is undirected.
+ * @param {string}  string       - The edge's key.
+ * @param {string}  source       - Source of the edge.
+ * @param {string}  target       - Target of the edge.
+ * @param {object}  attributes   - Edge's attributes.
+ */
+
+
+function EdgeData(undirected, key, source, target, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.undirected = undirected; // Extremities
+
+  this.source = source;
+  this.target = target;
+}
+
+EdgeData.prototype.attach = function () {
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected';
+  var source = this.source.key;
+  var target = this.target.key; // Handling source
+
+  this.source[outKey][target] = this;
+  if (this.undirected && source === target) return; // Handling target
+
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.attachMulti = function () {
+  var outKey = 'out';
+  var inKey = 'in';
+  var source = this.source.key;
+  var target = this.target.key;
+  if (this.undirected) outKey = inKey = 'undirected'; // Handling source
+
+  var adj = this.source[outKey];
+  var head = adj[target];
+
+  if (typeof head === 'undefined') {
+    adj[target] = this; // Self-loop optimization
+
+    if (!(this.undirected && source === target)) {
+      // Handling target
+      this.target[inKey][source] = this;
+    }
+
+    return;
+  } // Prepending to doubly-linked list
+
+
+  head.previous = this;
+  this.next = head; // Pointing to new head
+  // NOTE: use mutating swap later to avoid lookup?
+
+  adj[target] = this;
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.detach = function () {
+  var source = this.source.key;
+  var target = this.target.key;
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected';
+  delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+  delete this.target[inKey][source];
+};
+
+EdgeData.prototype.detachMulti = function () {
+  var source = this.source.key;
+  var target = this.target.key;
+  var outKey = 'out';
+  var inKey = 'in';
+  if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list
+
+  if (this.previous === undefined) {
+    // We are dealing with the head
+    // Should we delete the adjacency entry because it is now empty?
+    if (this.next === undefined) {
+      delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+      delete this.target[inKey][source];
+    } else {
+      // Detaching
+      this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method
+
+      this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop
+
+      this.target[inKey][source] = this.next;
+    }
+  } else {
+    // We are dealing with another list node
+    this.previous.next = this.next; // If not last
+
+    if (this.next !== undefined) {
+      this.next.previous = this.previous;
+    }
+  }
+};
+
+/**
+ * Graphology Node Attributes methods
+ * ===================================
+ */
+var NODE = 0;
+var SOURCE = 1;
+var TARGET = 2;
+var OPPOSITE = 3;
+
+function findRelevantNodeData(graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2) {
+  var nodeData, edgeData, arg1, arg2;
+  nodeOrEdge = '' + nodeOrEdge;
+
+  if (mode === NODE) {
+    nodeData = graph._nodes.get(nodeOrEdge);
+    if (!nodeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" node in the graph."));
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  } else if (mode === OPPOSITE) {
+    nameOrEdge = '' + nameOrEdge;
+    edgeData = graph._edges.get(nameOrEdge);
+    if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nameOrEdge, "\" edge in the graph."));
+    var source = edgeData.source.key;
+    var target = edgeData.target.key;
+
+    if (nodeOrEdge === source) {
+      nodeData = edgeData.target;
+    } else if (nodeOrEdge === target) {
+      nodeData = edgeData.source;
+    } else {
+      throw new NotFoundGraphError("Graph.".concat(method, ": the \"").concat(nodeOrEdge, "\" node is not attached to the \"").concat(nameOrEdge, "\" edge (").concat(source, ", ").concat(target, ")."));
+    }
+
+    arg1 = add1;
+    arg2 = add2;
+  } else {
+    edgeData = graph._edges.get(nodeOrEdge);
+    if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" edge in the graph."));
+
+    if (mode === SOURCE) {
+      nodeData = edgeData.source;
+    } else {
+      nodeData = edgeData.target;
+    }
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  }
+
+  return [nodeData, arg1, arg2];
+}
+
+function attachNodeAttributeGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData[0],
+        name = _findRelevantNodeData[1];
+
+    return data.attributes[name];
+  };
+}
+
+function attachNodeAttributesGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+    var _findRelevantNodeData2 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge),
+        data = _findRelevantNodeData2[0];
+
+    return data.attributes;
+  };
+}
+
+function attachNodeAttributeChecker(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData3 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData3[0],
+        name = _findRelevantNodeData3[1];
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+function attachNodeAttributeSetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    var _findRelevantNodeData4 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+        data = _findRelevantNodeData4[0],
+        name = _findRelevantNodeData4[1],
+        value = _findRelevantNodeData4[2];
+
+    data.attributes[name] = value; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributeUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    var _findRelevantNodeData5 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+        data = _findRelevantNodeData5[0],
+        name = _findRelevantNodeData5[1],
+        updater = _findRelevantNodeData5[2];
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+    var attributes = data.attributes;
+    var value = updater(attributes[name]);
+    attributes[name] = value; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributeRemover(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData6 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData6[0],
+        name = _findRelevantNodeData6[1];
+
+    delete data.attributes[name]; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesReplacer(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData7 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData7[0],
+        attributes = _findRelevantNodeData7[1];
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    data.attributes = attributes; // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesMerger(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData8 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData8[0],
+        attributes = _findRelevantNodeData8[1];
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    assign(data.attributes, attributes); // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+    return this;
+  };
+}
+
+function attachNodeAttributesUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    var _findRelevantNodeData9 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+        data = _findRelevantNodeData9[0],
+        updater = _findRelevantNodeData9[1];
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+    data.attributes = updater(data.attributes); // Emitting
+
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * List of methods to attach.
+ */
+
+
+var NODE_ATTRIBUTES_METHODS = [{
+  name: function name(element) {
+    return "get".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeGetter
+}, {
+  name: function name(element) {
+    return "get".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesGetter
+}, {
+  name: function name(element) {
+    return "has".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeChecker
+}, {
+  name: function name(element) {
+    return "set".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeSetter
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeUpdater
+}, {
+  name: function name(element) {
+    return "remove".concat(element, "Attribute");
+  },
+  attacher: attachNodeAttributeRemover
+}, {
+  name: function name(element) {
+    return "replace".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesReplacer
+}, {
+  name: function name(element) {
+    return "merge".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesMerger
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attributes");
+  },
+  attacher: attachNodeAttributesUpdater
+}];
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+
+function attachNodeAttributesMethods(Graph) {
+  NODE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+    var name = _ref.name,
+        attacher = _ref.attacher;
+    // For nodes
+    attacher(Graph, name('Node'), NODE); // For sources
+
+    attacher(Graph, name('Source'), SOURCE); // For targets
+
+    attacher(Graph, name('Target'), TARGET); // For opposites
+
+    attacher(Graph, name('Opposite'), OPPOSITE);
+  });
+}
+
+/**
+ * Graphology Edge Attributes methods
+ * ===================================
+ */
+/**
+ * Attach an attribute getter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+function attachEdgeAttributeGetter(Class, method, type) {
+  /**
+   * Get the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {mixed}          - The attribute's value.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes[name];
+  };
+}
+/**
+ * Attach an attributes getter method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesGetter(Class, method, type) {
+  /**
+   * Retrieves all the target element's attributes.
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   *
+   * @return {object}          - The element's attributes.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 1) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + arguments[1];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes;
+  };
+}
+/**
+ * Attach an attribute checker method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeChecker(Class, method, type) {
+  /**
+   * Checks whether the desired attribute is set for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+/**
+ * Attach an attribute setter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeSetter(Class, method, type) {
+  /**
+   * Set the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, value) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 3) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      value = arguments[3];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    data.attributes[name] = value; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeUpdater(Class, method, type) {
+  /**
+   * Update the desired attribute for the given element (node or edge) using
+   * the provided function.
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, updater) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 3) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      updater = arguments[3];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+    data.attributes[name] = updater(data.attributes[name]); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute remover method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributeRemover(Class, method, type) {
+  /**
+   * Remove the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element;
+      var target = '' + name;
+      name = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    delete data.attributes[name]; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name: name
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute replacer method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesReplacer(Class, method, type) {
+  /**
+   * Replace the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - New attributes.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - New attributes.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + attributes;
+      attributes = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    data.attributes = attributes; // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute merger method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesMerger(Class, method, type) {
+  /**
+   * Merge the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - Attributes to merge.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - Attributes to merge.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + attributes;
+      attributes = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+    assign(data.attributes, attributes); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+    return this;
+  };
+}
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+
+
+function attachEdgeAttributesUpdater(Class, method, type) {
+  /**
+   * Update the attributes of the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, updater) {
+    var data;
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+    if (arguments.length > 2) {
+      if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+      var source = '' + element,
+          target = '' + updater;
+      updater = arguments[2];
+      data = getMatchingEdge(this, source, target, type);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+    } else {
+      if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+      element = '' + element;
+      data = this._edges.get(element);
+      if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+    }
+
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+    data.attributes = updater(data.attributes); // Emitting
+
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+    return this;
+  };
+}
+/**
+ * List of methods to attach.
+ */
+
+
+var EDGE_ATTRIBUTES_METHODS = [{
+  name: function name(element) {
+    return "get".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeGetter
+}, {
+  name: function name(element) {
+    return "get".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesGetter
+}, {
+  name: function name(element) {
+    return "has".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeChecker
+}, {
+  name: function name(element) {
+    return "set".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeSetter
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeUpdater
+}, {
+  name: function name(element) {
+    return "remove".concat(element, "Attribute");
+  },
+  attacher: attachEdgeAttributeRemover
+}, {
+  name: function name(element) {
+    return "replace".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesReplacer
+}, {
+  name: function name(element) {
+    return "merge".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesMerger
+}, {
+  name: function name(element) {
+    return "update".concat(element, "Attributes");
+  },
+  attacher: attachEdgeAttributesUpdater
+}];
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+
+function attachEdgeAttributesMethods(Graph) {
+  EDGE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+    var name = _ref.name,
+        attacher = _ref.attacher;
+    // For edges
+    attacher(Graph, name('Edge'), 'mixed'); // For directed edges
+
+    attacher(Graph, name('DirectedEdge'), 'directed'); // For undirected edges
+
+    attacher(Graph, name('UndirectedEdge'), 'undirected');
+  });
+}
+
+/**
+ * Graphology Edge Iteration
+ * ==========================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's edges.
+ */
+/**
+ * Definitions.
+ */
+
+var EDGES_ITERATION = [{
+  name: 'edges',
+  type: 'mixed'
+}, {
+  name: 'inEdges',
+  type: 'directed',
+  direction: 'in'
+}, {
+  name: 'outEdges',
+  type: 'directed',
+  direction: 'out'
+}, {
+  name: 'inboundEdges',
+  type: 'mixed',
+  direction: 'in'
+}, {
+  name: 'outboundEdges',
+  type: 'mixed',
+  direction: 'out'
+}, {
+  name: 'directedEdges',
+  type: 'directed'
+}, {
+  name: 'undirectedEdges',
+  type: 'undirected'
+}];
+/**
+ * Function iterating over edges from the given object to match one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {function} callback - Function to call.
+ */
+
+function forEachSimple(breakable, object, callback, avoid) {
+  var shouldBreak = false;
+
+  for (var k in object) {
+    if (k === avoid) continue;
+    var edgeData = object[k];
+    shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+    if (breakable && shouldBreak) return edgeData.key;
+  }
+
+  return;
+}
+
+function forEachMulti(breakable, object, callback, avoid) {
+  var edgeData, source, target;
+  var shouldBreak = false;
+
+  for (var k in object) {
+    if (k === avoid) continue;
+    edgeData = object[k];
+
+    do {
+      source = edgeData.source;
+      target = edgeData.target;
+      shouldBreak = callback(edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+  }
+
+  return;
+}
+/**
+ * Function returning an iterator over edges from the given object.
+ *
+ * @param  {object}   object - Target object.
+ * @return {Iterator}
+ */
+
+
+function createIterator(object, avoid) {
+  var keys = Object.keys(object);
+  var l = keys.length;
+  var edgeData;
+  var i = 0;
+  return new Iterator__default["default"](function next() {
+    do {
+      if (!edgeData) {
+        if (i >= l) return {
+          done: true
+        };
+        var k = keys[i++];
+
+        if (k === avoid) {
+          edgeData = undefined;
+          continue;
+        }
+
+        edgeData = object[k];
+      } else {
+        edgeData = edgeData.next;
+      }
+    } while (!edgeData);
+
+    return {
+      done: false,
+      value: {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      }
+    };
+  });
+}
+/**
+ * Function iterating over the egdes from the object at given key to match
+ * one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {mixed}    k        - Neighbor key.
+ * @param {function} callback - Callback to use.
+ */
+
+
+function forEachForKeySimple(breakable, object, k, callback) {
+  var edgeData = object[k];
+  if (!edgeData) return;
+  var sourceData = edgeData.source;
+  var targetData = edgeData.target;
+  if (callback(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected) && breakable) return edgeData.key;
+}
+
+function forEachForKeyMulti(breakable, object, k, callback) {
+  var edgeData = object[k];
+  if (!edgeData) return;
+  var shouldBreak = false;
+
+  do {
+    shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+    if (breakable && shouldBreak) return edgeData.key;
+    edgeData = edgeData.next;
+  } while (edgeData !== undefined);
+
+  return;
+}
+/**
+ * Function returning an iterator over the egdes from the object at given key.
+ *
+ * @param  {object}   object   - Target object.
+ * @param  {mixed}    k        - Neighbor key.
+ * @return {Iterator}
+ */
+
+
+function createIteratorForKey(object, k) {
+  var edgeData = object[k];
+
+  if (edgeData.next !== undefined) {
+    return new Iterator__default["default"](function () {
+      if (!edgeData) return {
+        done: true
+      };
+      var value = {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      };
+      edgeData = edgeData.next;
+      return {
+        done: false,
+        value: value
+      };
+    });
+  }
+
+  return Iterator__default["default"].of({
+    edge: edgeData.key,
+    attributes: edgeData.attributes,
+    source: edgeData.source.key,
+    target: edgeData.target.key,
+    sourceAttributes: edgeData.source.attributes,
+    targetAttributes: edgeData.target.attributes,
+    undirected: edgeData.undirected
+  });
+}
+/**
+ * Function creating an array of edges for the given type.
+ *
+ * @param  {Graph}   graph - Target Graph instance.
+ * @param  {string}  type  - Type of edges to retrieve.
+ * @return {array}         - Array of edges.
+ */
+
+
+function createEdgeArray(graph, type) {
+  if (graph.size === 0) return [];
+
+  if (type === 'mixed' || type === graph.type) {
+    if (typeof Array.from === 'function') return Array.from(graph._edges.keys());
+    return take__default["default"](graph._edges.keys(), graph._edges.size);
+  }
+
+  var size = type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+  var list = new Array(size),
+      mask = type === 'undirected';
+
+  var iterator = graph._edges.values();
+
+  var i = 0;
+  var step, data;
+
+  while (step = iterator.next(), step.done !== true) {
+    data = step.value;
+    if (data.undirected === mask) list[i++] = data.key;
+  }
+
+  return list;
+}
+/**
+ * Function iterating over a graph's edges using a callback to match one of
+ * them.
+ *
+ * @param  {Graph}    graph    - Target Graph instance.
+ * @param  {string}   type     - Type of edges to retrieve.
+ * @param  {function} callback - Function to call.
+ */
+
+
+function forEachEdge(breakable, graph, type, callback) {
+  if (graph.size === 0) return;
+  var shouldFilter = type !== 'mixed' && type !== graph.type;
+  var mask = type === 'undirected';
+  var step, data;
+  var shouldBreak = false;
+
+  var iterator = graph._edges.values();
+
+  while (step = iterator.next(), step.done !== true) {
+    data = step.value;
+    if (shouldFilter && data.undirected !== mask) continue;
+    var _data = data,
+        key = _data.key,
+        attributes = _data.attributes,
+        source = _data.source,
+        target = _data.target;
+    shouldBreak = callback(key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected);
+    if (breakable && shouldBreak) return key;
+  }
+
+  return;
+}
+/**
+ * Function creating an iterator of edges for the given type.
+ *
+ * @param  {Graph}    graph - Target Graph instance.
+ * @param  {string}   type  - Type of edges to retrieve.
+ * @return {Iterator}
+ */
+
+
+function createEdgeIterator(graph, type) {
+  if (graph.size === 0) return Iterator__default["default"].empty();
+  var shouldFilter = type !== 'mixed' && type !== graph.type;
+  var mask = type === 'undirected';
+
+  var iterator = graph._edges.values();
+
+  return new Iterator__default["default"](function next() {
+    var step, data; // eslint-disable-next-line no-constant-condition
+
+    while (true) {
+      step = iterator.next();
+      if (step.done) return step;
+      data = step.value;
+      if (shouldFilter && data.undirected !== mask) continue;
+      break;
+    }
+
+    var value = {
+      edge: data.key,
+      attributes: data.attributes,
+      source: data.source.key,
+      target: data.target.key,
+      sourceAttributes: data.source.attributes,
+      targetAttributes: data.target.attributes,
+      undirected: data.undirected
+    };
+    return {
+      value: value,
+      done: false
+    };
+  });
+}
+/**
+ * Function iterating over a node's edges using a callback to match one of them.
+ *
+ * @param  {boolean}  multi     - Whether the graph is multi or not.
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Function to call.
+ */
+
+
+function forEachEdgeForNode(breakable, multi, type, direction, nodeData, callback) {
+  var fn = multi ? forEachMulti : forEachSimple;
+  var found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = fn(breakable, nodeData["in"], callback);
+      if (breakable && found) return found;
+    }
+
+    if (direction !== 'in') {
+      found = fn(breakable, nodeData.out, callback, !direction ? nodeData.key : undefined);
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    found = fn(breakable, nodeData.undirected, callback);
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+/**
+ * Function creating an array of edges for the given type & the given node.
+ *
+ * @param  {boolean} multi     - Whether the graph is multi or not.
+ * @param  {string}  type      - Type of edges to retrieve.
+ * @param  {string}  direction - In or out?
+ * @param  {any}     nodeData  - Target node's data.
+ * @return {array}             - Array of edges.
+ */
+
+
+function createEdgeArrayForNode(multi, type, direction, nodeData) {
+  var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+    edges.push(key);
+  });
+  return edges;
+}
+/**
+ * Function iterating over a node's edges using a callback.
+ *
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+
+
+function createEdgeIteratorForNode(type, direction, nodeData) {
+  var iterator = Iterator__default["default"].empty();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out' && typeof nodeData["in"] !== 'undefined') iterator = chain__default["default"](iterator, createIterator(nodeData["in"]));
+    if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator = chain__default["default"](iterator, createIterator(nodeData.out, !direction ? nodeData.key : undefined));
+  }
+
+  if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+    iterator = chain__default["default"](iterator, createIterator(nodeData.undirected));
+  }
+
+  return iterator;
+}
+/**
+ * Function iterating over edges for the given path using a callback to match
+ * one of them.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+
+
+function forEachEdgeForPath(breakable, type, multi, direction, sourceData, target, callback) {
+  var fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+  var found;
+
+  if (type !== 'undirected') {
+    if (typeof sourceData["in"] !== 'undefined' && direction !== 'out') {
+      found = fn(breakable, sourceData["in"], target, callback);
+      if (breakable && found) return found;
+    }
+
+    if (typeof sourceData.out !== 'undefined' && direction !== 'in' && (direction || sourceData.key !== target)) {
+      found = fn(breakable, sourceData.out, target, callback);
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined') {
+      found = fn(breakable, sourceData.undirected, target, callback);
+      if (breakable && found) return found;
+    }
+  }
+
+  return;
+}
+/**
+ * Function creating an array of edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {any}      target     - Target node.
+ * @return {array}               - Array of edges.
+ */
+
+
+function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+  var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForPath(false, type, multi, direction, sourceData, target, function (key) {
+    edges.push(key);
+  });
+  return edges;
+}
+/**
+ * Function returning an iterator over edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+
+
+function createEdgeIteratorForPath(type, direction, sourceData, target) {
+  var iterator = Iterator__default["default"].empty();
+
+  if (type !== 'undirected') {
+    if (typeof sourceData["in"] !== 'undefined' && direction !== 'out' && target in sourceData["in"]) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData["in"], target));
+    if (typeof sourceData.out !== 'undefined' && direction !== 'in' && target in sourceData.out && (direction || sourceData.key !== target)) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData.out, target));
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined' && target in sourceData.undirected) iterator = chain__default["default"](iterator, createIteratorForKey(sourceData.undirected, target));
+  }
+
+  return iterator;
+}
+/**
+ * Function attaching an edge array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachEdgeArrayCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  /**
+   * Function returning an array of certain edges.
+   *
+   * Arity 0: Return all the relevant edges.
+   *
+   * Arity 1: Return all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Return the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+    if (!arguments.length) return createEdgeArray(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+      return createEdgeArrayForNode(this.multi, type === 'mixed' ? this.type : type, direction, nodeData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return createEdgeArrayForPath(type, this.multi, direction, sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+  };
+}
+/**
+ * Function attaching a edge callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachForEachEdge(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[forEachName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(false, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+
+      return forEachEdgeForNode(false, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return forEachEdgeForPath(false, type, this.multi, direction, sourceData, target, callback);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(forEachName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+  };
+  /**
+   * Function mapping the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Map all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Map all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Map the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    var result; // We know the result length beforehand
+
+    if (args.length === 0) {
+      var length = 0;
+      if (type !== 'directed') length += this.undirectedSize;
+      if (type !== 'undirected') length += this.directedSize;
+      result = new Array(length);
+      var i = 0;
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        result[i++] = callback(e, ea, s, t, sa, ta, u);
+      });
+    } // We don't know the result length beforehand
+    // TODO: we can in some instances of simple graphs, knowing degree
+    else {
+      result = [];
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        result.push(callback(e, ea, s, t, sa, ta, u));
+      });
+    }
+
+    this[forEachName].apply(this, args);
+    return result;
+  };
+  /**
+   * Function filtering the graph's relevant edges using the provided predicate
+   * function.
+   *
+   * Arity 1: Filter all the relevant edges.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 2: Filter all of a node's relevant edges.
+   * @param  {any}      node      - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 3: Filter the relevant edges across the given path.
+   * @param  {any}      source    - Source node.
+   * @param  {any}      target    - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    var result = [];
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+    });
+    this[forEachName].apply(this, args);
+    return result;
+  };
+  /**
+   * Function reducing the graph's relevant edges using the provided accumulator
+   * function.
+   *
+   * Arity 1: Reduce all the relevant edges.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 2: Reduce all of a node's relevant edges.
+   * @param  {any}      node         - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 3: Reduce the relevant edges across the given path.
+   * @param  {any}      source       - Source node.
+   * @param  {any}      target       - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+
+    if (args.length < 2 || args.length > 4) {
+      throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(args.length, ")."));
+    }
+
+    if (typeof args[args.length - 1] === 'function' && typeof args[args.length - 2] !== 'function') {
+      throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+    }
+
+    var callback;
+    var initialValue;
+
+    if (args.length === 2) {
+      callback = args[0];
+      initialValue = args[1];
+      args = [];
+    } else if (args.length === 3) {
+      callback = args[1];
+      initialValue = args[2];
+      args = [args[0]];
+    } else if (args.length === 4) {
+      callback = args[2];
+      initialValue = args[3];
+      args = [args[0], args[1]];
+    }
+
+    var accumulator = initialValue;
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+    });
+    this[forEachName].apply(this, args);
+    return accumulator;
+  };
+}
+/**
+ * Function attaching a breakable edge callback iterator method to the Graph
+ * prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachFindEdge(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over the graph's relevant edges in order to match
+   * one of them using the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[findEdgeName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return false;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(true, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      var nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findEdgeName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+
+      return forEachEdgeForNode(true, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return forEachEdgeForPath(true, type, this.multi, direction, sourceData, target, callback);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(findEdgeName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+  };
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether any one of them matches the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[someName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      return callback(e, ea, s, t, sa, ta, u);
+    });
+    var found = this[findEdgeName].apply(this, args);
+    if (found) return true;
+    return false;
+  };
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether all of them matche the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[everyName] = function () {
+    var args = Array.prototype.slice.call(arguments);
+    var callback = args.pop();
+    args.push(function (e, ea, s, t, sa, ta, u) {
+      return !callback(e, ea, s, t, sa, ta, u);
+    });
+    var found = this[findEdgeName].apply(this, args);
+    if (found) return false;
+    return true;
+  };
+}
+/**
+ * Function attaching an edge iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachEdgeIteratorCreator(Class, description) {
+  var originalName = description.name,
+      type = description.type,
+      direction = description.direction;
+  var name = originalName.slice(0, -1) + 'Entries';
+  /**
+   * Function returning an iterator over the graph's edges.
+   *
+   * Arity 0: Iterate over all the relevant edges.
+   *
+   * Arity 1: Iterate over all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Iterate over the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator__default["default"].empty();
+    if (!arguments.length) return createEdgeIterator(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+      return createEdgeIteratorForNode(type, direction, sourceData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      var _sourceData = this._nodes.get(source);
+
+      if (!_sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+      return createEdgeIteratorForPath(type, direction, _sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+  };
+}
+/**
+ * Function attaching every edge iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+
+
+function attachEdgeIterationMethods(Graph) {
+  EDGES_ITERATION.forEach(function (description) {
+    attachEdgeArrayCreator(Graph, description);
+    attachForEachEdge(Graph, description);
+    attachFindEdge(Graph, description);
+    attachEdgeIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Neighbor Iteration
+ * ==============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over
+ * neighbors.
+ */
+/**
+ * Definitions.
+ */
+
+var NEIGHBORS_ITERATION = [{
+  name: 'neighbors',
+  type: 'mixed'
+}, {
+  name: 'inNeighbors',
+  type: 'directed',
+  direction: 'in'
+}, {
+  name: 'outNeighbors',
+  type: 'directed',
+  direction: 'out'
+}, {
+  name: 'inboundNeighbors',
+  type: 'mixed',
+  direction: 'in'
+}, {
+  name: 'outboundNeighbors',
+  type: 'mixed',
+  direction: 'out'
+}, {
+  name: 'directedNeighbors',
+  type: 'directed'
+}, {
+  name: 'undirectedNeighbors',
+  type: 'undirected'
+}];
+/**
+ * Helpers.
+ */
+
+function CompositeSetWrapper() {
+  this.A = null;
+  this.B = null;
+}
+
+CompositeSetWrapper.prototype.wrap = function (set) {
+  if (this.A === null) this.A = set;else if (this.B === null) this.B = set;
+};
+
+CompositeSetWrapper.prototype.has = function (key) {
+  if (this.A !== null && key in this.A) return true;
+  if (this.B !== null && key in this.B) return true;
+  return false;
+};
+/**
+ * Function iterating over the given node's relevant neighbors to match
+ * one of them using a predicated function.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Callback to use.
+ */
+
+
+function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+  for (var k in object) {
+    var edgeData = object[k];
+    var sourceData = edgeData.source;
+    var targetData = edgeData.target;
+    var neighborData = sourceData === nodeData ? targetData : sourceData;
+    if (visited && visited.has(neighborData.key)) continue;
+    var shouldBreak = callback(neighborData.key, neighborData.attributes);
+    if (breakable && shouldBreak) return neighborData.key;
+  }
+
+  return;
+}
+
+function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return forEachInObjectOnce(breakable, null, nodeData, nodeData.undirected, callback);
+    if (typeof direction === 'string') return forEachInObjectOnce(breakable, null, nodeData, nodeData[direction], callback);
+  } // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+
+
+  var visited = new CompositeSetWrapper();
+  var found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = forEachInObjectOnce(breakable, null, nodeData, nodeData["in"], callback);
+      if (breakable && found) return found;
+      visited.wrap(nodeData["in"]);
+    }
+
+    if (direction !== 'in') {
+      found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.out, callback);
+      if (breakable && found) return found;
+      visited.wrap(nodeData.out);
+    }
+  }
+
+  if (type !== 'directed') {
+    found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.undirected, callback);
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+/**
+ * Function creating an array of relevant neighbors for the given node.
+ *
+ * @param  {string}       type      - Type of neighbors.
+ * @param  {string}       direction - Direction.
+ * @param  {any}          nodeData  - Target node's data.
+ * @return {Array}                  - The list of neighbors.
+ */
+
+
+function createNeighborArrayForNode(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return Object.keys(nodeData.undirected);
+    if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+  }
+
+  var neighbors = [];
+  forEachNeighbor(false, type, direction, nodeData, function (key) {
+    neighbors.push(key);
+  });
+  return neighbors;
+}
+/**
+ * Function returning an iterator over the given node's relevant neighbors.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+
+
+function createDedupedObjectIterator(visited, nodeData, object) {
+  var keys = Object.keys(object);
+  var l = keys.length;
+  var i = 0;
+  return new Iterator__default["default"](function next() {
+    var neighborData = null;
+
+    do {
+      if (i >= l) {
+        if (visited) visited.wrap(object);
+        return {
+          done: true
+        };
+      }
+
+      var edgeData = object[keys[i++]];
+      var sourceData = edgeData.source;
+      var targetData = edgeData.target;
+      neighborData = sourceData === nodeData ? targetData : sourceData;
+
+      if (visited && visited.has(neighborData.key)) {
+        neighborData = null;
+        continue;
+      }
+    } while (neighborData === null);
+
+    return {
+      done: false,
+      value: {
+        neighbor: neighborData.key,
+        attributes: neighborData.attributes
+      }
+    };
+  });
+}
+
+function createNeighborIterator(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+    if (typeof direction === 'string') return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+  }
+
+  var iterator = Iterator__default["default"].empty(); // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+
+  var visited = new CompositeSetWrapper();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData["in"]));
+    }
+
+    if (direction !== 'in') {
+      iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData.out));
+    }
+  }
+
+  if (type !== 'directed') {
+    iterator = chain__default["default"](iterator, createDedupedObjectIterator(visited, nodeData, nodeData.undirected));
+  }
+
+  return iterator;
+}
+/**
+ * Function attaching a neighbors array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachNeighborArrayCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  /**
+   * Function returning an array of certain neighbors.
+   *
+   * @param  {any}   node   - Target node.
+   * @return {array} - The neighbors of neighbors.
+   *
+   * @throws {Error} - Will throw if node is not found in the graph.
+   */
+
+  Class.prototype[name] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return createNeighborArrayForNode(type === 'mixed' ? this.type : type, direction, nodeData);
+  };
+}
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachForEachNeighbor(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[forEachName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    forEachNeighbor(false, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+  };
+  /**
+   * Function mapping the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function (node, callback) {
+    // TODO: optimize when size is known beforehand
+    var result = [];
+    this[forEachName](node, function (n, a) {
+      result.push(callback(n, a));
+    });
+    return result;
+  };
+  /**
+   * Function filtering the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function (node, callback) {
+    var result = [];
+    this[forEachName](node, function (n, a) {
+      if (callback(n, a)) result.push(n);
+    });
+    return result;
+  };
+  /**
+   * Function reducing the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function (node, callback, initialValue) {
+    if (arguments.length < 3) throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+    var accumulator = initialValue;
+    this[forEachName](node, function (n, a) {
+      accumulator = callback(accumulator, n, a);
+    });
+    return accumulator;
+  };
+}
+/**
+ * Function attaching a breakable neighbors callback iterator method to the
+ * Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachFindNeighbor(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+  var findName = 'find' + capitalizedSingular;
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[findName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return forEachNeighbor(true, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+  };
+  /**
+   * Function iterating over all the relevant neighbors to find if any of them
+   * matches the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var someName = 'some' + capitalizedSingular;
+
+  Class.prototype[someName] = function (node, callback) {
+    var found = this[findName](node, callback);
+    if (found) return true;
+    return false;
+  };
+  /**
+   * Function iterating over all the relevant neighbors to find if all of them
+   * matche the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+
+  var everyName = 'every' + capitalizedSingular;
+
+  Class.prototype[everyName] = function (node, callback) {
+    var found = this[findName](node, function (n, a) {
+      return !callback(n, a);
+    });
+    if (found) return false;
+    return true;
+  };
+}
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+
+
+function attachNeighborIteratorCreator(Class, description) {
+  var name = description.name,
+      type = description.type,
+      direction = description.direction;
+  var iteratorName = name.slice(0, -1) + 'Entries';
+  /**
+   * Function returning an iterator over all the relevant neighbors.
+   *
+   * @param  {any}      node     - Target node.
+   * @return {Iterator}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+
+  Class.prototype[iteratorName] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return Iterator__default["default"].empty();
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(iteratorName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+    return createNeighborIterator(type === 'mixed' ? this.type : type, direction, nodeData);
+  };
+}
+/**
+ * Function attaching every neighbor iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+
+
+function attachNeighborIterationMethods(Graph) {
+  NEIGHBORS_ITERATION.forEach(function (description) {
+    attachNeighborArrayCreator(Graph, description);
+    attachForEachNeighbor(Graph, description);
+    attachFindNeighbor(Graph, description);
+    attachNeighborIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Adjacency Iteration
+ * ===============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's adjacency.
+ */
+
+/**
+ * Function iterating over a simple graph's adjacency using a callback.
+ *
+ * @param {boolean}  breakable         - Can we break?
+ * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+ * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+ * @param {Graph}    graph             - Target Graph instance.
+ * @param {callback} function          - Iteration callback.
+ */
+function forEachAdjacency(breakable, assymetric, disconnectedNodes, graph, callback) {
+  var iterator = graph._nodes.values();
+
+  var type = graph.type;
+  var step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+  while (step = iterator.next(), step.done !== true) {
+    var hasEdges = false;
+    sourceData = step.value;
+
+    if (type !== 'undirected') {
+      adj = sourceData.out;
+
+      for (neighbor in adj) {
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+          hasEdges = true;
+          shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+          if (breakable && shouldBreak) return edgeData;
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (type !== 'directed') {
+      adj = sourceData.undirected;
+
+      for (neighbor in adj) {
+        if (assymetric && sourceData.key > neighbor) continue;
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+          if (targetData.key !== neighbor) targetData = edgeData.source;
+          hasEdges = true;
+          shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+          if (breakable && shouldBreak) return edgeData;
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (disconnectedNodes && !hasEdges) {
+      shouldBreak = callback(sourceData.key, null, sourceData.attributes, null, null, null, null);
+      if (breakable && shouldBreak) return null;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Graphology Serialization Utilities
+ * ===================================
+ *
+ * Collection of functions used by the graph serialization schemes.
+ */
+/**
+ * Formats internal node data into a serialized node.
+ *
+ * @param  {any}    key  - The node's key.
+ * @param  {object} data - Internal node's data.
+ * @return {array}       - The serialized node.
+ */
+
+function serializeNode(key, data) {
+  var serialized = {
+    key: key
+  };
+  if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+  return serialized;
+}
+/**
+ * Formats internal edge data into a serialized edge.
+ *
+ * @param  {any}    key  - The edge's key.
+ * @param  {object} data - Internal edge's data.
+ * @return {array}       - The serialized edge.
+ */
+
+function serializeEdge(key, data) {
+  var serialized = {
+    key: key,
+    source: data.source.key,
+    target: data.target.key
+  };
+  if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+  if (data.undirected) serialized.undirected = true;
+  return serialized;
+}
+/**
+ * Checks whether the given value is a serialized node.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+
+function validateSerializedNode(value) {
+  if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');
+  if (!('key' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized node is missing its key.');
+  if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+}
+/**
+ * Checks whether the given value is a serialized edge.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+
+function validateSerializedEdge(value) {
+  if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');
+  if (!('source' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its source.');
+  if (!('target' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its target.');
+  if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+  if ('undirected' in value && typeof value.undirected !== 'boolean') throw new InvalidArgumentsGraphError('Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.');
+}
+
+/**
+ * Constants.
+ */
+
+var INSTANCE_ID = incrementalIdStartingFromRandomByte();
+/**
+ * Enums.
+ */
+
+var TYPES = new Set(['directed', 'undirected', 'mixed']);
+var EMITTER_PROPS = new Set(['domain', '_events', '_eventsCount', '_maxListeners']);
+var EDGE_ADD_METHODS = [{
+  name: function name(verb) {
+    return "".concat(verb, "Edge");
+  },
+  generateKey: true
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "DirectedEdge");
+  },
+  generateKey: true,
+  type: 'directed'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "UndirectedEdge");
+  },
+  generateKey: true,
+  type: 'undirected'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "EdgeWithKey");
+  }
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "DirectedEdgeWithKey");
+  },
+  type: 'directed'
+}, {
+  name: function name(verb) {
+    return "".concat(verb, "UndirectedEdgeWithKey");
+  },
+  type: 'undirected'
+}];
+/**
+ * Default options.
+ */
+
+var DEFAULTS = {
+  allowSelfLoops: true,
+  multi: false,
+  type: 'mixed'
+};
+/**
+ * Abstract functions used by the Graph class for various methods.
+ */
+
+/**
+ * Internal method used to add a node to the given graph
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {any}     node            - The node's key.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {NodeData}                - Created node data.
+ */
+
+function _addNode(graph, node, attributes) {
+  if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.addNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+  node = '' + node;
+  attributes = attributes || {};
+  if (graph._nodes.has(node)) throw new UsageGraphError("Graph.addNode: the \"".concat(node, "\" node already exist in the graph."));
+  var data = new graph.NodeDataClass(node, attributes); // Adding the node to internal register
+
+  graph._nodes.set(node, data); // Emitting
+
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes: attributes
+  });
+  return data;
+}
+/**
+ * Same as the above but without sanity checks because we call this in contexts
+ * where necessary checks were already done.
+ */
+
+
+function unsafeAddNode(graph, node, attributes) {
+  var data = new graph.NodeDataClass(node, attributes);
+
+  graph._nodes.set(node, data);
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes: attributes
+  });
+  return data;
+}
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+
+
+function addEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));
+  if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));
+  if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\"")); // Coercion of source & target:
+
+  source = '' + source;
+  target = '' + target;
+  attributes = attributes || {};
+  if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+  var sourceData = graph._nodes.get(source),
+      targetData = graph._nodes.get(target);
+
+  if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": source node \"").concat(source, "\" not found."));
+  if (!targetData) throw new NotFoundGraphError("Graph.".concat(name, ": target node \"").concat(target, "\" not found.")); // Must the graph generate an id for this edge?
+
+  var eventData = {
+    key: null,
+    undirected: undirected,
+    source: source,
+    target: target,
+    attributes: attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge; // Here, we have a key collision
+
+    if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+  } // Here, we might have a source / target collision
+
+
+  if (!graph.multi && (undirected ? typeof sourceData.undirected[target] !== 'undefined' : typeof sourceData.out[target] !== 'undefined')) {
+    throw new UsageGraphError("Graph.".concat(name, ": an edge linking \"").concat(source, "\" to \"").concat(target, "\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));
+  } // Storing some data
+
+
+  var edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+  graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+  var isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  } // Updating relevant index
+
+
+  if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+  if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+  eventData.key = edge;
+  graph.emit('edgeAdded', eventData);
+  return edge;
+}
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @param  {boolean} [asUpdater]       - Are we updating or merging?
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+
+
+function mergeEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes, asUpdater) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));
+  if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));
+
+  if (attributes) {
+    if (asUpdater) {
+      if (typeof attributes !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid updater function. Expecting a function but got \"").concat(attributes, "\""));
+    } else {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\""));
+    }
+  } // Coercion of source & target:
+
+
+  source = '' + source;
+  target = '' + target;
+  var updater;
+
+  if (asUpdater) {
+    updater = attributes;
+    attributes = undefined;
+  }
+
+  if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+  var sourceData = graph._nodes.get(source);
+
+  var targetData = graph._nodes.get(target);
+
+  var edgeData; // Do we need to handle duplicate?
+
+  var alreadyExistingEdgeData;
+
+  if (!mustGenerateKey) {
+    edgeData = graph._edges.get(edge);
+
+    if (edgeData) {
+      // Here, we need to ensure, if the user gave a key, that source & target
+      // are consistent
+      if (edgeData.source.key !== source || edgeData.target.key !== target) {
+        // If source or target inconsistent
+        if (!undirected || edgeData.source.key !== target || edgeData.target.key !== source) {
+          // If directed, or source/target aren't flipped
+          throw new UsageGraphError("Graph.".concat(name, ": inconsistency detected when attempting to merge the \"").concat(edge, "\" edge with \"").concat(source, "\" source & \"").concat(target, "\" target vs. (\"").concat(edgeData.source.key, "\", \"").concat(edgeData.target.key, "\")."));
+        }
+      }
+
+      alreadyExistingEdgeData = edgeData;
+    }
+  } // Here, we might have a source / target collision
+
+
+  if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+    alreadyExistingEdgeData = undirected ? sourceData.undirected[target] : sourceData.out[target];
+  } // Handling duplicates
+
+
+  if (alreadyExistingEdgeData) {
+    var info = [alreadyExistingEdgeData.key, false, false, false]; // We can skip the attribute merging part if the user did not provide them
+
+    if (asUpdater ? !updater : !attributes) return info; // Updating the attributes
+
+    if (asUpdater) {
+      var oldAttributes = alreadyExistingEdgeData.attributes;
+      alreadyExistingEdgeData.attributes = updater(oldAttributes);
+      graph.emit('edgeAttributesUpdated', {
+        type: 'replace',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes
+      });
+    } // Merging the attributes
+    else {
+      assign(alreadyExistingEdgeData.attributes, attributes);
+      graph.emit('edgeAttributesUpdated', {
+        type: 'merge',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes,
+        data: attributes
+      });
+    }
+
+    return info;
+  }
+
+  attributes = attributes || {};
+  if (asUpdater && updater) attributes = updater(attributes); // Must the graph generate an id for this edge?
+
+  var eventData = {
+    key: null,
+    undirected: undirected,
+    source: source,
+    target: target,
+    attributes: attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge; // Here, we have a key collision
+
+    if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+  }
+
+  var sourceWasAdded = false;
+  var targetWasAdded = false;
+
+  if (!sourceData) {
+    sourceData = unsafeAddNode(graph, source, {});
+    sourceWasAdded = true;
+
+    if (source === target) {
+      targetData = sourceData;
+      targetWasAdded = true;
+    }
+  }
+
+  if (!targetData) {
+    targetData = unsafeAddNode(graph, target, {});
+    targetWasAdded = true;
+  } // Storing some data
+
+
+  edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+  graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+  var isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  } // Updating relevant index
+
+
+  if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+  if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+  eventData.key = edge;
+  graph.emit('edgeAdded', eventData);
+  return [edge, true, sourceWasAdded, targetWasAdded];
+}
+/**
+ * Internal method used to drop an edge.
+ *
+ * @param  {Graph}    graph    - Target graph.
+ * @param  {EdgeData} edgeData - Data of the edge to drop.
+ */
+
+
+function dropEdgeFromData(graph, edgeData) {
+  // Dropping the edge from the register
+  graph._edges["delete"](edgeData.key); // Updating related degrees
+
+
+  var sourceData = edgeData.source,
+      targetData = edgeData.target,
+      attributes = edgeData.attributes;
+  var undirected = edgeData.undirected;
+  var isSelfLoop = sourceData === targetData;
+
+  if (undirected) {
+    sourceData.undirectedDegree--;
+    targetData.undirectedDegree--;
+    if (isSelfLoop) graph._undirectedSelfLoopCount--;
+  } else {
+    sourceData.outDegree--;
+    targetData.inDegree--;
+    if (isSelfLoop) graph._directedSelfLoopCount--;
+  } // Clearing index
+
+
+  if (graph.multi) edgeData.detachMulti();else edgeData.detach();
+  if (undirected) graph._undirectedSize--;else graph._directedSize--; // Emitting
+
+  graph.emit('edgeDropped', {
+    key: edgeData.key,
+    attributes: attributes,
+    source: sourceData.key,
+    target: targetData.key,
+    undirected: undirected
+  });
+}
+/**
+ * Graph class
+ *
+ * @constructor
+ * @param  {object}  [options] - Options:
+ * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+ * @param  {string}    [type]           - Type of the graph.
+ * @param  {boolean}   [map]            - Allow references as keys?
+ * @param  {boolean}   [multi]          - Allow parallel edges?
+ *
+ * @throws {Error} - Will throw if the arguments are not valid.
+ */
+
+
+var Graph = /*#__PURE__*/function (_EventEmitter) {
+  _inheritsLoose(Graph, _EventEmitter);
+
+  function Graph(options) {
+    var _this;
+
+    _this = _EventEmitter.call(this) || this; //-- Solving options
+
+    options = assign({}, DEFAULTS, options); // Enforcing options validity
+
+    if (typeof options.multi !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(options.multi, "\"."));
+    if (!TYPES.has(options.type)) throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'type' option. Should be one of \"mixed\", \"directed\" or \"undirected\" but got \"".concat(options.type, "\"."));
+    if (typeof options.allowSelfLoops !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(options.allowSelfLoops, "\".")); //-- Private properties
+    // Utilities
+
+    var NodeDataClass = options.type === 'mixed' ? MixedNodeData : options.type === 'directed' ? DirectedNodeData : UndirectedNodeData;
+    privateProperty(_assertThisInitialized(_this), 'NodeDataClass', NodeDataClass); // Internal edge key generator
+    // NOTE: this internal generator produce keys that are strings
+    // composed of a weird prefix, an incremental instance id starting from
+    // a random byte and finally an internal instance incremental id.
+    // All this to avoid intra-frame and cross-frame adversarial inputs
+    // that can force a single #.addEdge call to degenerate into a O(n)
+    // available key search loop.
+    // It also ensures that automatically generated edge keys are unlikely
+    // to produce collisions with arbitrary keys given by users.
+
+    var instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+    var edgeId = 0;
+
+    var edgeKeyGenerator = function edgeKeyGenerator() {
+      var availableEdgeKey;
+
+      do {
+        availableEdgeKey = instancePrefix + edgeId++;
+      } while (_this._edges.has(availableEdgeKey));
+
+      return availableEdgeKey;
+    }; // Indexes
+
+
+    privateProperty(_assertThisInitialized(_this), '_attributes', {});
+    privateProperty(_assertThisInitialized(_this), '_nodes', new Map());
+    privateProperty(_assertThisInitialized(_this), '_edges', new Map());
+    privateProperty(_assertThisInitialized(_this), '_directedSize', 0);
+    privateProperty(_assertThisInitialized(_this), '_undirectedSize', 0);
+    privateProperty(_assertThisInitialized(_this), '_directedSelfLoopCount', 0);
+    privateProperty(_assertThisInitialized(_this), '_undirectedSelfLoopCount', 0);
+    privateProperty(_assertThisInitialized(_this), '_edgeKeyGenerator', edgeKeyGenerator); // Options
+
+    privateProperty(_assertThisInitialized(_this), '_options', options); // Emitter properties
+
+    EMITTER_PROPS.forEach(function (prop) {
+      return privateProperty(_assertThisInitialized(_this), prop, _this[prop]);
+    }); //-- Properties readers
+
+    readOnlyProperty(_assertThisInitialized(_this), 'order', function () {
+      return _this._nodes.size;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'size', function () {
+      return _this._edges.size;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'directedSize', function () {
+      return _this._directedSize;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'undirectedSize', function () {
+      return _this._undirectedSize;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'selfLoopCount', function () {
+      return _this._directedSelfLoopCount + _this._undirectedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'directedSelfLoopCount', function () {
+      return _this._directedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'undirectedSelfLoopCount', function () {
+      return _this._undirectedSelfLoopCount;
+    });
+    readOnlyProperty(_assertThisInitialized(_this), 'multi', _this._options.multi);
+    readOnlyProperty(_assertThisInitialized(_this), 'type', _this._options.type);
+    readOnlyProperty(_assertThisInitialized(_this), 'allowSelfLoops', _this._options.allowSelfLoops);
+    readOnlyProperty(_assertThisInitialized(_this), 'implementation', function () {
+      return 'graphology';
+    });
+    return _this;
+  }
+
+  var _proto = Graph.prototype;
+
+  _proto._resetInstanceCounters = function _resetInstanceCounters() {
+    this._directedSize = 0;
+    this._undirectedSize = 0;
+    this._directedSelfLoopCount = 0;
+    this._undirectedSelfLoopCount = 0;
+  }
+  /**---------------------------------------------------------------------------
+   * Read
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning whether the given node is found in the graph.
+   *
+   * @param  {any}     node - The node.
+   * @return {boolean}
+   */
+  ;
+
+  _proto.hasNode = function hasNode(node) {
+    return this._nodes.has('' + node);
+  }
+  /**
+   * Method returning whether the given directed edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasDirectedEdge = function hasDirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'undirected') return false;
+
+    if (arguments.length === 1) {
+      var edge = '' + source;
+
+      var edgeData = this._edges.get(edge);
+
+      return !!edgeData && !edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = nodeData.out[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning whether the given undirected edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasUndirectedEdge = function hasUndirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'directed') return false;
+
+    if (arguments.length === 1) {
+      var edge = '' + source;
+
+      var edgeData = this._edges.get(edge);
+
+      return !!edgeData && edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = nodeData.undirected[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning whether the given edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  ;
+
+  _proto.hasEdge = function hasEdge(source, target) {
+    if (arguments.length === 1) {
+      var edge = '' + source;
+      return this._edges.has(edge);
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target; // If the node source or the target is not in the graph we break
+
+      var nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+      var edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+      if (!edges) edges = typeof nodeData.undirected !== 'undefined' && nodeData.undirected[target];
+      if (!edges) return false;
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError("Graph.hasEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+  }
+  /**
+   * Method returning the edge matching source & target in a directed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.directedEdge = function directedEdge(source, target) {
+    if (this.type === 'undirected') return;
+    source = '' + source;
+    target = '' + target;
+    if (this.multi) throw new UsageGraphError('Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.');
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.out && sourceData.out[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning the edge matching source & target in a undirected fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.undirectedEdge = function undirectedEdge(source, target) {
+    if (this.type === 'directed') return;
+    source = '' + source;
+    target = '' + target;
+    if (this.multi) throw new UsageGraphError('Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.');
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.undirected && sourceData.undirected[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning the edge matching source & target in a mixed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  ;
+
+  _proto.edge = function edge(source, target) {
+    if (this.multi) throw new UsageGraphError('Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.');
+    source = '' + source;
+    target = '' + target;
+
+    var sourceData = this._nodes.get(source);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(source, "\" source node in the graph."));
+    if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(target, "\" target node in the graph."));
+    var edgeData = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target] || undefined;
+    if (edgeData) return edgeData.key;
+  }
+  /**
+   * Method returning whether two nodes are directed neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areDirectedNeighbors = function areDirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areDirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData["in"] || neighbor in nodeData.out;
+  }
+  /**
+   * Method returning whether two nodes are out neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areOutNeighbors = function areOutNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areOutNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData.out;
+  }
+  /**
+   * Method returning whether two nodes are in neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areInNeighbors = function areInNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areInNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return false;
+    return neighbor in nodeData["in"];
+  }
+  /**
+   * Method returning whether two nodes are undirected neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areUndirectedNeighbors = function areUndirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areUndirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return false;
+    return neighbor in nodeData.undirected;
+  }
+  /**
+   * Method returning whether two nodes are neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areNeighbors = function areNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData["in"] || neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether two nodes are inbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areInboundNeighbors = function areInboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areInboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData["in"]) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether two nodes are outbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.areOutboundNeighbors = function areOutboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.areOutboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning the given node's in degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inDegree = function inDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.inDegree;
+  }
+  /**
+   * Method returning the given node's out degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outDegree = function outDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.outDegree;
+  }
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.directedDegree = function directedDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.directedDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    return nodeData.inDegree + nodeData.outDegree;
+  }
+  /**
+   * Method returning the given node's undirected degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.undirectedDegree = function undirectedDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegree: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return 0;
+    return nodeData.undirectedDegree;
+  }
+  /**
+   * Method returning the given node's inbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inboundDegree = function inboundDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's outbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outboundDegree = function outboundDegree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.degree = function degree(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.degree: could not find the \"".concat(node, "\" node in the graph."));
+    var degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+    }
+
+    return degree;
+  }
+  /**
+   * Method returning the given node's in degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inDegreeWithoutSelfLoops = function inDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData["in"][node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.inDegree - loops;
+  }
+  /**
+   * Method returning the given node's out degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outDegreeWithoutSelfLoops = function outDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData.out[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.outDegree - loops;
+  }
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.directedDegreeWithoutSelfLoops = function directedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.directedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'undirected') return 0;
+    var self = nodeData.out[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.inDegree + nodeData.outDegree - loops * 2;
+  }
+  /**
+   * Method returning the given node's undirected degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.undirectedDegreeWithoutSelfLoops = function undirectedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    if (this.type === 'directed') return 0;
+    var self = nodeData.undirected[node];
+    var loops = self ? this.multi ? self.size : 1 : 0;
+    return nodeData.undirectedDegree - loops * 2;
+  }
+  /**
+   * Method returning the given node's inbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.inboundDegreeWithoutSelfLoops = function inboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+      self = nodeData.out[node];
+      loops += self ? this.multi ? self.size : 1 : 0;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given node's outbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.outboundDegreeWithoutSelfLoops = function outboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+      self = nodeData["in"][node];
+      loops += self ? this.multi ? self.size : 1 : 0;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  ;
+
+  _proto.degreeWithoutSelfLoops = function degreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.degreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+    var self;
+    var degree = 0;
+    var loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+      self = nodeData.undirected[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+      self = nodeData.out[node];
+      loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+    }
+
+    return degree - loops;
+  }
+  /**
+   * Method returning the given edge's source.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's source.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.source = function source(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.source: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source.key;
+  }
+  /**
+   * Method returning the given edge's target.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's target.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.target = function target(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.target: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.target.key;
+  }
+  /**
+   * Method returning the given edge's extremities.
+   *
+   * @param  {any}   edge - The edge's key.
+   * @return {array}      - The edge's extremities.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.extremities = function extremities(edge) {
+    edge = '' + edge;
+
+    var edgeData = this._edges.get(edge);
+
+    if (!edgeData) throw new NotFoundGraphError("Graph.extremities: could not find the \"".concat(edge, "\" edge in the graph."));
+    return [edgeData.source.key, edgeData.target.key];
+  }
+  /**
+   * Given a node & an edge, returns the other extremity of the edge.
+   *
+   * @param  {any}   node - The node's key.
+   * @param  {any}   edge - The edge's key.
+   * @return {any}        - The related node.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph or if the
+   *                   edge & node are not related.
+   */
+  ;
+
+  _proto.opposite = function opposite(node, edge) {
+    node = '' + node;
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.opposite: could not find the \"".concat(edge, "\" edge in the graph."));
+    var source = data.source.key;
+    var target = data.target.key;
+    if (node === source) return target;
+    if (node === target) return source;
+    throw new NotFoundGraphError("Graph.opposite: the \"".concat(node, "\" node is not attached to the \"").concat(edge, "\" edge (").concat(source, ", ").concat(target, ")."));
+  }
+  /**
+   * Returns whether the given edge has the given node as extremity.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @param  {any}     node - The node's key.
+   * @return {boolean}      - The related node.
+   *
+   * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+   */
+  ;
+
+  _proto.hasExtremity = function hasExtremity(edge, node) {
+    edge = '' + edge;
+    node = '' + node;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.hasExtremity: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source.key === node || data.target.key === node;
+  }
+  /**
+   * Method returning whether the given edge is undirected.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isUndirected = function isUndirected(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isUndirected: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.undirected;
+  }
+  /**
+   * Method returning whether the given edge is directed.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isDirected = function isDirected(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isDirected: could not find the \"".concat(edge, "\" edge in the graph."));
+    return !data.undirected;
+  }
+  /**
+   * Method returning whether the given edge is a self loop.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  ;
+
+  _proto.isSelfLoop = function isSelfLoop(edge) {
+    edge = '' + edge;
+
+    var data = this._edges.get(edge);
+
+    if (!data) throw new NotFoundGraphError("Graph.isSelfLoop: could not find the \"".concat(edge, "\" edge in the graph."));
+    return data.source === data.target;
+  }
+  /**---------------------------------------------------------------------------
+   * Mutation
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to add a node to the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   *
+   * @throws {Error} - Will throw if the given node already exist.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   */
+  ;
+
+  _proto.addNode = function addNode(node, attributes) {
+    var nodeData = _addNode(this, node, attributes);
+
+    return nodeData.key;
+  }
+  /**
+   * Method used to merge a node into the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   */
+  ;
+
+  _proto.mergeNode = function mergeNode(node, attributes) {
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.mergeNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+    node = '' + node;
+    attributes = attributes || {}; // If the node already exists, we merge the attributes
+
+    var data = this._nodes.get(node);
+
+    if (data) {
+      if (attributes) {
+        assign(data.attributes, attributes);
+        this.emit('nodeAttributesUpdated', {
+          type: 'merge',
+          key: node,
+          attributes: data.attributes,
+          data: attributes
+        });
+      }
+
+      return [node, false];
+    }
+
+    data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    this._nodes.set(node, data); // Emitting
+
+
+    this.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return [node, true];
+  }
+  /**
+   * Method used to add a node if it does not exist in the graph or else to
+   * update its attributes using a function.
+   *
+   * @param  {any}      node      - The node.
+   * @param  {function} [updater] - Optional updater function.
+   * @return {any}                - The node.
+   */
+  ;
+
+  _proto.updateNode = function updateNode(node, updater) {
+    if (updater && typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.updateNode: invalid updater function. Expecting a function but got \"".concat(updater, "\"")); // String coercion
+
+    node = '' + node; // If the node already exists, we update the attributes
+
+    var data = this._nodes.get(node);
+
+    if (data) {
+      if (updater) {
+        var oldAttributes = data.attributes;
+        data.attributes = updater(oldAttributes);
+        this.emit('nodeAttributesUpdated', {
+          type: 'replace',
+          key: node,
+          attributes: data.attributes
+        });
+      }
+
+      return [node, false];
+    }
+
+    var attributes = updater ? updater({}) : {};
+    data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    this._nodes.set(node, data); // Emitting
+
+
+    this.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return [node, true];
+  }
+  /**
+   * Method used to drop a single node & all its attached edges from the graph.
+   *
+   * @param  {any}    node - The node.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the node doesn't exist.
+   */
+  ;
+
+  _proto.dropNode = function dropNode(node) {
+    node = '' + node;
+
+    var nodeData = this._nodes.get(node);
+
+    if (!nodeData) throw new NotFoundGraphError("Graph.dropNode: could not find the \"".concat(node, "\" node in the graph."));
+    var edgeData; // Removing attached edges
+    // NOTE: we could be faster here, but this is such a pain to maintain
+
+    if (this.type !== 'undirected') {
+      for (var neighbor in nodeData.out) {
+        edgeData = nodeData.out[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+
+      for (var _neighbor in nodeData["in"]) {
+        edgeData = nodeData["in"][_neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (this.type !== 'directed') {
+      for (var _neighbor2 in nodeData.undirected) {
+        edgeData = nodeData.undirected[_neighbor2];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    } // Dropping the node from the register
+
+
+    this._nodes["delete"](node); // Emitting
+
+
+    this.emit('nodeDropped', {
+      key: node,
+      attributes: nodeData.attributes
+    });
+  }
+  /**
+   * Method used to drop a single edge from the graph.
+   *
+   * Arity 1:
+   * @param  {any}    edge - The edge.
+   *
+   * Arity 2:
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropEdge = function dropEdge(edge) {
+    var edgeData;
+
+    if (arguments.length > 1) {
+      var source = '' + arguments[0];
+      var target = '' + arguments[1];
+      edgeData = getMatchingEdge(this, source, target, this.type);
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    } else {
+      edge = '' + edge;
+      edgeData = this._edges.get(edge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(edge, "\" edge in the graph."));
+    }
+
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to drop a single directed edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropDirectedEdge = function dropDirectedEdge(source, target) {
+    if (arguments.length < 2) throw new UsageGraphError('Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+    if (this.multi) throw new UsageGraphError('Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+    source = '' + source;
+    target = '' + target;
+    var edgeData = getMatchingEdge(this, source, target, 'directed');
+    if (!edgeData) throw new NotFoundGraphError("Graph.dropDirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to drop a single undirected edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  ;
+
+  _proto.dropUndirectedEdge = function dropUndirectedEdge(source, target) {
+    if (arguments.length < 2) throw new UsageGraphError('Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+    if (this.multi) throw new UsageGraphError('Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+    var edgeData = getMatchingEdge(this, source, target, 'undirected');
+    if (!edgeData) throw new NotFoundGraphError("Graph.dropUndirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+    dropEdgeFromData(this, edgeData);
+    return this;
+  }
+  /**
+   * Method used to remove every edge & every node from the graph.
+   *
+   * @return {Graph}
+   */
+  ;
+
+  _proto.clear = function clear() {
+    // Clearing edges
+    this._edges.clear(); // Clearing nodes
+
+
+    this._nodes.clear(); // Reset counters
+
+
+    this._resetInstanceCounters(); // Emitting
+
+
+    this.emit('cleared');
+  }
+  /**
+   * Method used to remove every edge from the graph.
+   *
+   * @return {Graph}
+   */
+  ;
+
+  _proto.clearEdges = function clearEdges() {
+    // Clearing structure index
+    var iterator = this._nodes.values();
+
+    var step;
+
+    while (step = iterator.next(), step.done !== true) {
+      step.value.clear();
+    } // Clearing edges
+
+
+    this._edges.clear(); // Reset counters
+
+
+    this._resetInstanceCounters(); // Emitting
+
+
+    this.emit('edgesCleared');
+  }
+  /**---------------------------------------------------------------------------
+   * Attributes-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning the desired graph's attribute.
+   *
+   * @param  {string} name - Name of the attribute.
+   * @return {any}
+   */
+  ;
+
+  _proto.getAttribute = function getAttribute(name) {
+    return this._attributes[name];
+  }
+  /**
+   * Method returning the graph's attributes.
+   *
+   * @return {object}
+   */
+  ;
+
+  _proto.getAttributes = function getAttributes() {
+    return this._attributes;
+  }
+  /**
+   * Method returning whether the graph has the desired attribute.
+   *
+   * @param  {string}  name - Name of the attribute.
+   * @return {boolean}
+   */
+  ;
+
+  _proto.hasAttribute = function hasAttribute(name) {
+    return this._attributes.hasOwnProperty(name);
+  }
+  /**
+   * Method setting a value for the desired graph's attribute.
+   *
+   * @param  {string}  name  - Name of the attribute.
+   * @param  {any}     value - Value for the attribute.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.setAttribute = function setAttribute(name, value) {
+    this._attributes[name] = value; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method using a function to update the desired graph's attribute's value.
+   *
+   * @param  {string}   name    - Name of the attribute.
+   * @param  {function} updater - Function use to update the attribute's value.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.updateAttribute = function updateAttribute(name, updater) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttribute: updater should be a function.');
+    var value = this._attributes[name];
+    this._attributes[name] = updater(value); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method removing the desired graph's attribute.
+   *
+   * @param  {string} name  - Name of the attribute.
+   * @return {Graph}
+   */
+  ;
+
+  _proto.removeAttribute = function removeAttribute(name) {
+    delete this._attributes[name]; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'remove',
+      attributes: this._attributes,
+      name: name
+    });
+    return this;
+  }
+  /**
+   * Method replacing the graph's attributes.
+   *
+   * @param  {object} attributes - New attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  ;
+
+  _proto.replaceAttributes = function replaceAttributes(attributes) {
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.replaceAttributes: provided attributes are not a plain object.');
+    this._attributes = attributes; // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'replace',
+      attributes: this._attributes
+    });
+    return this;
+  }
+  /**
+   * Method merging the graph's attributes.
+   *
+   * @param  {object} attributes - Attributes to merge.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  ;
+
+  _proto.mergeAttributes = function mergeAttributes(attributes) {
+    if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.mergeAttributes: provided attributes are not a plain object.');
+    assign(this._attributes, attributes); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'merge',
+      attributes: this._attributes,
+      data: attributes
+    });
+    return this;
+  }
+  /**
+   * Method updating the graph's attributes.
+   *
+   * @param  {function} updater - Function used to update the attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given updater is not a function.
+   */
+  ;
+
+  _proto.updateAttributes = function updateAttributes(updater) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttributes: provided updater is not a function.');
+    this._attributes = updater(this._attributes); // Emitting
+
+    this.emit('attributesUpdated', {
+      type: 'update',
+      attributes: this._attributes
+    });
+    return this;
+  }
+  /**
+   * Method used to update each node's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  ;
+
+  _proto.updateEachNodeAttributes = function updateEachNodeAttributes(updater, hints) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: expecting an updater function.');
+    if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+    }
+
+    this.emit('eachNodeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+  /**
+   * Method used to update each edge's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  ;
+
+  _proto.updateEachEdgeAttributes = function updateEachEdgeAttributes(updater, hints) {
+    if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: expecting an updater function.');
+    if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+    var iterator = this._edges.values();
+
+    var step, edgeData, sourceData, targetData;
+
+    while (step = iterator.next(), step.done !== true) {
+      edgeData = step.value;
+      sourceData = edgeData.source;
+      targetData = edgeData.target;
+      edgeData.attributes = updater(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected);
+    }
+
+    this.emit('eachEdgeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+  /**---------------------------------------------------------------------------
+   * Iteration-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method iterating over the graph's adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  ;
+
+  _proto.forEachAdjacencyEntry = function forEachAdjacencyEntry(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntry: expecting a callback.');
+    forEachAdjacency(false, false, false, this, callback);
+  };
+
+  _proto.forEachAdjacencyEntryWithOrphans = function forEachAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.');
+    forEachAdjacency(false, false, true, this, callback);
+  }
+  /**
+   * Method iterating over the graph's assymetric adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  ;
+
+  _proto.forEachAssymetricAdjacencyEntry = function forEachAssymetricAdjacencyEntry(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntry: expecting a callback.');
+    forEachAdjacency(false, true, false, this, callback);
+  };
+
+  _proto.forEachAssymetricAdjacencyEntryWithOrphans = function forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.');
+    forEachAdjacency(false, true, true, this, callback);
+  }
+  /**
+   * Method returning the list of the graph's nodes.
+   *
+   * @return {array} - The nodes.
+   */
+  ;
+
+  _proto.nodes = function nodes() {
+    if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+    return take__default["default"](this._nodes.keys(), this._nodes.size);
+  }
+  /**
+   * Method iterating over the graph's nodes using the given callback.
+   *
+   * @param  {function}  callback - Callback (key, attributes, index).
+   */
+  ;
+
+  _proto.forEachNode = function forEachNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      callback(nodeData.key, nodeData.attributes);
+    }
+  }
+  /**
+   * Method iterating attempting to find a node matching the given predicate
+   * function.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.findNode = function findNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.findNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+    }
+
+    return;
+  }
+  /**
+   * Method mapping nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.mapNodes = function mapNodes(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.mapNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+    var result = new Array(this.order);
+    var i = 0;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      result[i++] = callback(nodeData.key, nodeData.attributes);
+    }
+
+    return result;
+  }
+  /**
+   * Method returning whether some node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.someNode = function someNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.someNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) return true;
+    }
+
+    return false;
+  }
+  /**
+   * Method returning whether all node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.everyNode = function everyNode(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.everyNode: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (!callback(nodeData.key, nodeData.attributes)) return false;
+    }
+
+    return true;
+  }
+  /**
+   * Method filtering nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  ;
+
+  _proto.filterNodes = function filterNodes(callback) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.filterNodes: expecting a callback.');
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+    var result = [];
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      if (callback(nodeData.key, nodeData.attributes)) result.push(nodeData.key);
+    }
+
+    return result;
+  }
+  /**
+   * Method reducing nodes.
+   *
+   * @param  {function}  callback - Callback (accumulator, key, attributes).
+   */
+  ;
+
+  _proto.reduceNodes = function reduceNodes(callback, initialValue) {
+    if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.reduceNodes: expecting a callback.');
+    if (arguments.length < 2) throw new InvalidArgumentsGraphError('Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.');
+    var accumulator = initialValue;
+
+    var iterator = this._nodes.values();
+
+    var step, nodeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      nodeData = step.value;
+      accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+    }
+
+    return accumulator;
+  }
+  /**
+   * Method returning an iterator over the graph's node entries.
+   *
+   * @return {Iterator}
+   */
+  ;
+
+  _proto.nodeEntries = function nodeEntries() {
+    var iterator = this._nodes.values();
+
+    return new Iterator__default["default"](function () {
+      var step = iterator.next();
+      if (step.done) return step;
+      var data = step.value;
+      return {
+        value: {
+          node: data.key,
+          attributes: data.attributes
+        },
+        done: false
+      };
+    });
+  }
+  /**---------------------------------------------------------------------------
+   * Serialization
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to export the whole graph.
+   *
+   * @return {object} - The serialized graph.
+   */
+  ;
+
+  _proto["export"] = function _export() {
+    var nodes = new Array(this._nodes.size);
+    var i = 0;
+
+    this._nodes.forEach(function (data, key) {
+      nodes[i++] = serializeNode(key, data);
+    });
+
+    var edges = new Array(this._edges.size);
+    i = 0;
+
+    this._edges.forEach(function (data, key) {
+      edges[i++] = serializeEdge(key, data);
+    });
+
+    return {
+      options: {
+        type: this.type,
+        multi: this.multi,
+        allowSelfLoops: this.allowSelfLoops
+      },
+      attributes: this.getAttributes(),
+      nodes: nodes,
+      edges: edges
+    };
+  }
+  /**
+   * Method used to import a serialized graph.
+   *
+   * @param  {object|Graph} data  - The serialized graph.
+   * @param  {boolean}      merge - Whether to merge data.
+   * @return {Graph}              - Returns itself for chaining.
+   */
+  ;
+
+  _proto["import"] = function _import(data) {
+    var _this2 = this;
+
+    var merge = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+    // Importing a Graph instance directly
+    if (isGraph(data)) {
+      // Nodes
+      data.forEachNode(function (n, a) {
+        if (merge) _this2.mergeNode(n, a);else _this2.addNode(n, a);
+      }); // Edges
+
+      data.forEachEdge(function (e, a, s, t, _sa, _ta, u) {
+        if (merge) {
+          if (u) _this2.mergeUndirectedEdgeWithKey(e, s, t, a);else _this2.mergeDirectedEdgeWithKey(e, s, t, a);
+        } else {
+          if (u) _this2.addUndirectedEdgeWithKey(e, s, t, a);else _this2.addDirectedEdgeWithKey(e, s, t, a);
+        }
+      });
+      return this;
+    } // Importing a serialized graph
+
+
+    if (!isPlainObject(data)) throw new InvalidArgumentsGraphError('Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.');
+
+    if (data.attributes) {
+      if (!isPlainObject(data.attributes)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Expecting a plain object.');
+      if (merge) this.mergeAttributes(data.attributes);else this.replaceAttributes(data.attributes);
+    }
+
+    var i, l, list, node, edge;
+
+    if (data.nodes) {
+      list = data.nodes;
+      if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid nodes. Expecting an array.');
+
+      for (i = 0, l = list.length; i < l; i++) {
+        node = list[i]; // Validating
+
+        validateSerializedNode(node); // Adding the node
+
+        var _node = node,
+            key = _node.key,
+            attributes = _node.attributes;
+        if (merge) this.mergeNode(key, attributes);else this.addNode(key, attributes);
+      }
+    }
+
+    if (data.edges) {
+      list = data.edges;
+      if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid edges. Expecting an array.');
+
+      for (i = 0, l = list.length; i < l; i++) {
+        edge = list[i]; // Validating
+
+        validateSerializedEdge(edge); // Adding the edge
+
+        var _edge = edge,
+            source = _edge.source,
+            target = _edge.target,
+            _attributes = _edge.attributes,
+            _edge$undirected = _edge.undirected,
+            undirected = _edge$undirected === void 0 ? false : _edge$undirected;
+        var method = void 0;
+
+        if ('key' in edge) {
+          method = merge ? undirected ? this.mergeUndirectedEdgeWithKey : this.mergeDirectedEdgeWithKey : undirected ? this.addUndirectedEdgeWithKey : this.addDirectedEdgeWithKey;
+          method.call(this, edge.key, source, target, _attributes);
+        } else {
+          method = merge ? undirected ? this.mergeUndirectedEdge : this.mergeDirectedEdge : undirected ? this.addUndirectedEdge : this.addDirectedEdge;
+          method.call(this, source, target, _attributes);
+        }
+      }
+    }
+
+    return this;
+  }
+  /**---------------------------------------------------------------------------
+   * Utils
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning a null copy of the graph, i.e. a graph without nodes
+   * & edges but with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The null copy.
+   */
+  ;
+
+  _proto.nullCopy = function nullCopy(options) {
+    var graph = new Graph(assign({}, this._options, options));
+    graph.replaceAttributes(assign({}, this.getAttributes()));
+    return graph;
+  }
+  /**
+   * Method returning an empty copy of the graph, i.e. a graph without edges but
+   * with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The empty copy.
+   */
+  ;
+
+  _proto.emptyCopy = function emptyCopy(options) {
+    var graph = this.nullCopy(options);
+
+    this._nodes.forEach(function (nodeData, key) {
+      var attributes = assign({}, nodeData.attributes); // NOTE: no need to emit events since user cannot access the instance yet
+
+      nodeData = new graph.NodeDataClass(key, attributes);
+
+      graph._nodes.set(key, nodeData);
+    });
+
+    return graph;
+  }
+  /**
+   * Method returning an exact copy of the graph.
+   *
+   * @param  {object} options - Upgrade options.
+   * @return {Graph}          - The copy.
+   */
+  ;
+
+  _proto.copy = function copy(options) {
+    options = options || {};
+    if (typeof options.type === 'string' && options.type !== this.type && options.type !== 'mixed') throw new UsageGraphError("Graph.copy: cannot create an incompatible copy from \"".concat(this.type, "\" type to \"").concat(options.type, "\" because this would mean losing information about the current graph."));
+    if (typeof options.multi === 'boolean' && options.multi !== this.multi && options.multi !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.');
+    if (typeof options.allowSelfLoops === 'boolean' && options.allowSelfLoops !== this.allowSelfLoops && options.allowSelfLoops !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.');
+    var graph = this.emptyCopy(options);
+
+    var iterator = this._edges.values();
+
+    var step, edgeData;
+
+    while (step = iterator.next(), step.done !== true) {
+      edgeData = step.value; // NOTE: no need to emit events since user cannot access the instance yet
+
+      addEdge(graph, 'copy', false, edgeData.undirected, edgeData.key, edgeData.source.key, edgeData.target.key, assign({}, edgeData.attributes));
+    }
+
+    return graph;
+  }
+  /**---------------------------------------------------------------------------
+   * Known methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used by JavaScript to perform JSON serialization.
+   *
+   * @return {object} - The serialized graph.
+   */
+  ;
+
+  _proto.toJSON = function toJSON() {
+    return this["export"]();
+  }
+  /**
+   * Method returning [object Graph].
+   */
+  ;
+
+  _proto.toString = function toString() {
+    return '[object Graph]';
+  }
+  /**
+   * Method used internally by node's console to display a custom object.
+   *
+   * @return {object} - Formatted object representation of the graph.
+   */
+  ;
+
+  _proto.inspect = function inspect() {
+    var _this3 = this;
+
+    var nodes = {};
+
+    this._nodes.forEach(function (data, key) {
+      nodes[key] = data.attributes;
+    });
+
+    var edges = {},
+        multiIndex = {};
+
+    this._edges.forEach(function (data, key) {
+      var direction = data.undirected ? '--' : '->';
+      var label = '';
+      var source = data.source.key;
+      var target = data.target.key;
+      var tmp;
+
+      if (data.undirected && source > target) {
+        tmp = source;
+        source = target;
+        target = tmp;
+      }
+
+      var desc = "(".concat(source, ")").concat(direction, "(").concat(target, ")");
+
+      if (!key.startsWith('geid_')) {
+        label += "[".concat(key, "]: ");
+      } else if (_this3.multi) {
+        if (typeof multiIndex[desc] === 'undefined') {
+          multiIndex[desc] = 0;
+        } else {
+          multiIndex[desc]++;
+        }
+
+        label += "".concat(multiIndex[desc], ". ");
+      }
+
+      label += desc;
+      edges[label] = data.attributes;
+    });
+
+    var dummy = {};
+
+    for (var k in this) {
+      if (this.hasOwnProperty(k) && !EMITTER_PROPS.has(k) && typeof this[k] !== 'function' && _typeof(k) !== 'symbol') dummy[k] = this[k];
+    }
+
+    dummy.attributes = this._attributes;
+    dummy.nodes = nodes;
+    dummy.edges = edges;
+    privateProperty(dummy, 'constructor', this.constructor);
+    return dummy;
+  };
+
+  return Graph;
+}(events.EventEmitter);
+if (typeof Symbol !== 'undefined') Graph.prototype[Symbol["for"]('nodejs.util.inspect.custom')] = Graph.prototype.inspect;
+/**
+ * Related to edge addition.
+ */
+
+EDGE_ADD_METHODS.forEach(function (method) {
+  ['add', 'merge', 'update'].forEach(function (verb) {
+    var name = method.name(verb);
+    var fn = verb === 'add' ? addEdge : mergeEdge;
+
+    if (method.generateKey) {
+      Graph.prototype[name] = function (source, target, attributes) {
+        return fn(this, name, true, (method.type || this.type) === 'undirected', null, source, target, attributes, verb === 'update');
+      };
+    } else {
+      Graph.prototype[name] = function (edge, source, target, attributes) {
+        return fn(this, name, false, (method.type || this.type) === 'undirected', edge, source, target, attributes, verb === 'update');
+      };
+    }
+  });
+});
+/**
+ * Attributes-related.
+ */
+
+attachNodeAttributesMethods(Graph);
+attachEdgeAttributesMethods(Graph);
+/**
+ * Edge iteration-related.
+ */
+
+attachEdgeIterationMethods(Graph);
+/**
+ * Neighbor iteration-related.
+ */
+
+attachNeighborIterationMethods(Graph);
+
+/**
+ * Alternative constructors.
+ */
+
+var DirectedGraph = /*#__PURE__*/function (_Graph) {
+  _inheritsLoose(DirectedGraph, _Graph);
+
+  function DirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'directed'
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+    if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph.call(this, finalOptions) || this;
+  }
+
+  return DirectedGraph;
+}(Graph);
+
+var UndirectedGraph = /*#__PURE__*/function (_Graph2) {
+  _inheritsLoose(UndirectedGraph, _Graph2);
+
+  function UndirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'undirected'
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+    if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph2.call(this, finalOptions) || this;
+  }
+
+  return UndirectedGraph;
+}(Graph);
+
+var MultiGraph = /*#__PURE__*/function (_Graph3) {
+  _inheritsLoose(MultiGraph, _Graph3);
+
+  function MultiGraph(options) {
+    var finalOptions = assign({
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiGraph.from: inconsistent indication that the graph should be simple in given options!');
+    return _Graph3.call(this, finalOptions) || this;
+  }
+
+  return MultiGraph;
+}(Graph);
+
+var MultiDirectedGraph = /*#__PURE__*/function (_Graph4) {
+  _inheritsLoose(MultiDirectedGraph, _Graph4);
+
+  function MultiDirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'directed',
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+    if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph4.call(this, finalOptions) || this;
+  }
+
+  return MultiDirectedGraph;
+}(Graph);
+
+var MultiUndirectedGraph = /*#__PURE__*/function (_Graph5) {
+  _inheritsLoose(MultiUndirectedGraph, _Graph5);
+
+  function MultiUndirectedGraph(options) {
+    var finalOptions = assign({
+      type: 'undirected',
+      multi: true
+    }, options);
+    if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+    if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+    return _Graph5.call(this, finalOptions) || this;
+  }
+
+  return MultiUndirectedGraph;
+}(Graph);
+/**
+ * Attaching static #.from method to each of the constructors.
+ */
+
+
+function attachStaticFromMethod(Class) {
+  /**
+   * Builds a graph from serialized data or another graph's data.
+   *
+   * @param  {Graph|SerializedGraph} data      - Hydratation data.
+   * @param  {object}                [options] - Options.
+   * @return {Class}
+   */
+  Class.from = function (data, options) {
+    // Merging given options with serialized ones
+    var finalOptions = assign({}, data.options, options);
+    var instance = new Class(finalOptions);
+    instance["import"](data);
+    return instance;
+  };
+}
+
+attachStaticFromMethod(Graph);
+attachStaticFromMethod(DirectedGraph);
+attachStaticFromMethod(UndirectedGraph);
+attachStaticFromMethod(MultiGraph);
+attachStaticFromMethod(MultiDirectedGraph);
+attachStaticFromMethod(MultiUndirectedGraph);
+Graph.Graph = Graph;
+Graph.DirectedGraph = DirectedGraph;
+Graph.UndirectedGraph = UndirectedGraph;
+Graph.MultiGraph = MultiGraph;
+Graph.MultiDirectedGraph = MultiDirectedGraph;
+Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+Graph.NotFoundGraphError = NotFoundGraphError;
+Graph.UsageGraphError = UsageGraphError;
+
+/**
+ * Graphology CommonJS Endoint
+ * ============================
+ *
+ * Endpoint for CommonJS modules consumers.
+ */
+
+module.exports = Graph;
+//# sourceMappingURL=graphology.cjs.js.map
diff --git a/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.d.ts b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d2f14ffca954a992db281fa4780e6b17d2e33ab3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.d.ts
@@ -0,0 +1,36 @@
+import {AbstractGraph, Attributes} from 'graphology-types';
+
+export default class Graph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends AbstractGraph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class DirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class UndirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiDirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+export class MultiUndirectedGraph<
+  NodeAttributes extends Attributes = Attributes,
+  EdgeAttributes extends Attributes = Attributes,
+  GraphAttributes extends Attributes = Attributes
+> extends Graph<NodeAttributes, EdgeAttributes, GraphAttributes> {}
+
+export class InvalidArgumentsGraphError extends Error {}
+export class NotFoundGraphError extends Error {}
+export class UsageGraphError extends Error {}
diff --git a/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.esm.js b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.esm.js
new file mode 100644
index 0000000000000000000000000000000000000000..f89b5be2e04f484e09a562e17b20b2336dc00126
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.esm.js
@@ -0,0 +1,6671 @@
+import { EventEmitter } from 'events';
+import Iterator from 'obliterator/iterator';
+import take from 'obliterator/take';
+import chain from 'obliterator/chain';
+
+/**
+ * Graphology Utilities
+ * =====================
+ *
+ * Collection of helpful functions used by the implementation.
+ */
+
+/**
+ * Object.assign-like polyfill.
+ *
+ * @param  {object} target       - First object.
+ * @param  {object} [...objects] - Objects to merge.
+ * @return {object}
+ */
+function assignPolyfill() {
+  const target = arguments[0];
+
+  for (let i = 1, l = arguments.length; i < l; i++) {
+    if (!arguments[i]) continue;
+
+    for (const k in arguments[i]) target[k] = arguments[i][k];
+  }
+
+  return target;
+}
+
+let assign = assignPolyfill;
+
+if (typeof Object.assign === 'function') assign = Object.assign;
+
+/**
+ * Function returning the first matching edge for given path.
+ * Note: this function does not check the existence of source & target. This
+ * must be performed by the caller.
+ *
+ * @param  {Graph}  graph  - Target graph.
+ * @param  {any}    source - Source node.
+ * @param  {any}    target - Target node.
+ * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+ * @return {string|null}
+ */
+function getMatchingEdge(graph, source, target, type) {
+  const sourceData = graph._nodes.get(source);
+
+  let edge = null;
+
+  if (!sourceData) return edge;
+
+  if (type === 'mixed') {
+    edge =
+      (sourceData.out && sourceData.out[target]) ||
+      (sourceData.undirected && sourceData.undirected[target]);
+  } else if (type === 'directed') {
+    edge = sourceData.out && sourceData.out[target];
+  } else {
+    edge = sourceData.undirected && sourceData.undirected[target];
+  }
+
+  return edge;
+}
+
+/**
+ * Checks whether the given value is a Graph implementation instance.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+function isGraph(value) {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    typeof value.addUndirectedEdgeWithKey === 'function' &&
+    typeof value.dropNode === 'function'
+  );
+}
+
+/**
+ * Checks whether the given value is a plain object.
+ *
+ * @param  {mixed}   value - Target value.
+ * @return {boolean}
+ */
+function isPlainObject(value) {
+  return (
+    typeof value === 'object' && value !== null && value.constructor === Object
+  );
+}
+
+/**
+ * Checks whether the given object is empty.
+ *
+ * @param  {object}  o - Target Object.
+ * @return {boolean}
+ */
+function isEmpty(o) {
+  let k;
+
+  for (k in o) return false;
+
+  return true;
+}
+
+/**
+ * Creates a "private" property for the given member name by concealing it
+ * using the `enumerable` option.
+ *
+ * @param {object} target - Target object.
+ * @param {string} name   - Member name.
+ */
+function privateProperty(target, name, value) {
+  Object.defineProperty(target, name, {
+    enumerable: false,
+    configurable: false,
+    writable: true,
+    value
+  });
+}
+
+/**
+ * Creates a read-only property for the given member name & the given getter.
+ *
+ * @param {object}   target - Target object.
+ * @param {string}   name   - Member name.
+ * @param {mixed}    value  - The attached getter or fixed value.
+ */
+function readOnlyProperty(target, name, value) {
+  const descriptor = {
+    enumerable: true,
+    configurable: true
+  };
+
+  if (typeof value === 'function') {
+    descriptor.get = value;
+  } else {
+    descriptor.value = value;
+    descriptor.writable = false;
+  }
+
+  Object.defineProperty(target, name, descriptor);
+}
+
+/**
+ * Returns whether the given object constitute valid hints.
+ *
+ * @param {object} hints - Target object.
+ */
+function validateHints(hints) {
+  if (!isPlainObject(hints)) return false;
+
+  if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+
+  return true;
+}
+
+/**
+ * Creates a function generating incremental ids for edges.
+ *
+ * @return {function}
+ */
+function incrementalIdStartingFromRandomByte() {
+  let i = Math.floor(Math.random() * 256) & 0xff;
+
+  return () => {
+    return i++;
+  };
+}
+
+/**
+ * Graphology Custom Errors
+ * =========================
+ *
+ * Defining custom errors for ease of use & easy unit tests across
+ * implementations (normalized typology rather than relying on error
+ * messages to check whether the correct error was found).
+ */
+class GraphError extends Error {
+  constructor(message) {
+    super();
+    this.name = 'GraphError';
+    this.message = message;
+  }
+}
+
+class InvalidArgumentsGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'InvalidArgumentsGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(
+        this,
+        InvalidArgumentsGraphError.prototype.constructor
+      );
+  }
+}
+
+class NotFoundGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'NotFoundGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(this, NotFoundGraphError.prototype.constructor);
+  }
+}
+
+class UsageGraphError extends GraphError {
+  constructor(message) {
+    super(message);
+    this.name = 'UsageGraphError';
+
+    // This is V8 specific to enhance stack readability
+    if (typeof Error.captureStackTrace === 'function')
+      Error.captureStackTrace(this, UsageGraphError.prototype.constructor);
+  }
+}
+
+/**
+ * Graphology Internal Data Classes
+ * =================================
+ *
+ * Internal classes hopefully reduced to structs by engines & storing
+ * necessary information for nodes & edges.
+ *
+ * Note that those classes don't rely on the `class` keyword to avoid some
+ * cruft introduced by most of ES2015 transpilers.
+ */
+
+/**
+ * MixedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function MixedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+MixedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+  this.undirectedDegree = 0;
+
+  // Indices
+  this.in = {};
+  this.out = {};
+  this.undirected = {};
+};
+
+/**
+ * DirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function DirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+DirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.inDegree = 0;
+  this.outDegree = 0;
+
+  // Indices
+  this.in = {};
+  this.out = {};
+};
+
+/**
+ * UndirectedNodeData class.
+ *
+ * @constructor
+ * @param {string} string     - The node's key.
+ * @param {object} attributes - Node's attributes.
+ */
+function UndirectedNodeData(key, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+
+  this.clear();
+}
+
+UndirectedNodeData.prototype.clear = function () {
+  // Degrees
+  this.undirectedDegree = 0;
+
+  // Indices
+  this.undirected = {};
+};
+
+/**
+ * EdgeData class.
+ *
+ * @constructor
+ * @param {boolean} undirected   - Whether the edge is undirected.
+ * @param {string}  string       - The edge's key.
+ * @param {string}  source       - Source of the edge.
+ * @param {string}  target       - Target of the edge.
+ * @param {object}  attributes   - Edge's attributes.
+ */
+function EdgeData(undirected, key, source, target, attributes) {
+  // Attributes
+  this.key = key;
+  this.attributes = attributes;
+  this.undirected = undirected;
+
+  // Extremities
+  this.source = source;
+  this.target = target;
+}
+
+EdgeData.prototype.attach = function () {
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  const source = this.source.key;
+  const target = this.target.key;
+
+  // Handling source
+  this.source[outKey][target] = this;
+
+  if (this.undirected && source === target) return;
+
+  // Handling target
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.attachMulti = function () {
+  let outKey = 'out';
+  let inKey = 'in';
+
+  const source = this.source.key;
+  const target = this.target.key;
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  // Handling source
+  const adj = this.source[outKey];
+  const head = adj[target];
+
+  if (typeof head === 'undefined') {
+    adj[target] = this;
+
+    // Self-loop optimization
+    if (!(this.undirected && source === target)) {
+      // Handling target
+      this.target[inKey][source] = this;
+    }
+
+    return;
+  }
+
+  // Prepending to doubly-linked list
+  head.previous = this;
+  this.next = head;
+
+  // Pointing to new head
+  // NOTE: use mutating swap later to avoid lookup?
+  adj[target] = this;
+  this.target[inKey][source] = this;
+};
+
+EdgeData.prototype.detach = function () {
+  const source = this.source.key;
+  const target = this.target.key;
+
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  delete this.source[outKey][target];
+
+  // No-op delete in case of undirected self-loop
+  delete this.target[inKey][source];
+};
+
+EdgeData.prototype.detachMulti = function () {
+  const source = this.source.key;
+  const target = this.target.key;
+
+  let outKey = 'out';
+  let inKey = 'in';
+
+  if (this.undirected) outKey = inKey = 'undirected';
+
+  // Deleting from doubly-linked list
+  if (this.previous === undefined) {
+    // We are dealing with the head
+
+    // Should we delete the adjacency entry because it is now empty?
+    if (this.next === undefined) {
+      delete this.source[outKey][target];
+
+      // No-op delete in case of undirected self-loop
+      delete this.target[inKey][source];
+    } else {
+      // Detaching
+      this.next.previous = undefined;
+
+      // NOTE: could avoid the lookups by creating a #.become mutating method
+      this.source[outKey][target] = this.next;
+
+      // No-op delete in case of undirected self-loop
+      this.target[inKey][source] = this.next;
+    }
+  } else {
+    // We are dealing with another list node
+    this.previous.next = this.next;
+
+    // If not last
+    if (this.next !== undefined) {
+      this.next.previous = this.previous;
+    }
+  }
+};
+
+/**
+ * Graphology Node Attributes methods
+ * ===================================
+ */
+
+const NODE = 0;
+const SOURCE = 1;
+const TARGET = 2;
+const OPPOSITE = 3;
+
+function findRelevantNodeData(
+  graph,
+  method,
+  mode,
+  nodeOrEdge,
+  nameOrEdge,
+  add1,
+  add2
+) {
+  let nodeData, edgeData, arg1, arg2;
+
+  nodeOrEdge = '' + nodeOrEdge;
+
+  if (mode === NODE) {
+    nodeData = graph._nodes.get(nodeOrEdge);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nodeOrEdge}" node in the graph.`
+      );
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  } else if (mode === OPPOSITE) {
+    nameOrEdge = '' + nameOrEdge;
+
+    edgeData = graph._edges.get(nameOrEdge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nameOrEdge}" edge in the graph.`
+      );
+
+    const source = edgeData.source.key;
+    const target = edgeData.target.key;
+
+    if (nodeOrEdge === source) {
+      nodeData = edgeData.target;
+    } else if (nodeOrEdge === target) {
+      nodeData = edgeData.source;
+    } else {
+      throw new NotFoundGraphError(
+        `Graph.${method}: the "${nodeOrEdge}" node is not attached to the "${nameOrEdge}" edge (${source}, ${target}).`
+      );
+    }
+
+    arg1 = add1;
+    arg2 = add2;
+  } else {
+    edgeData = graph._edges.get(nodeOrEdge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.${method}: could not find the "${nodeOrEdge}" edge in the graph.`
+      );
+
+    if (mode === SOURCE) {
+      nodeData = edgeData.source;
+    } else {
+      nodeData = edgeData.target;
+    }
+
+    arg1 = nameOrEdge;
+    arg2 = add1;
+  }
+
+  return [nodeData, arg1, arg2];
+}
+
+function attachNodeAttributeGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    return data.attributes[name];
+  };
+}
+
+function attachNodeAttributesGetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+    const [data] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge
+    );
+
+    return data.attributes;
+  };
+}
+
+function attachNodeAttributeChecker(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+function attachNodeAttributeSetter(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    const [data, name, value] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1,
+      add2
+    );
+
+    data.attributes[name] = value;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributeUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+    const [data, name, updater] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1,
+      add2
+    );
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: updater should be a function.`
+      );
+
+    const attributes = data.attributes;
+    const value = updater(attributes[name]);
+
+    attributes[name] = value;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributeRemover(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, name] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    delete data.attributes[name];
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesReplacer(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, attributes] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    data.attributes = attributes;
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesMerger(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, attributes] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    assign(data.attributes, attributes);
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+
+    return this;
+  };
+}
+
+function attachNodeAttributesUpdater(Class, method, mode) {
+  Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+    const [data, updater] = findRelevantNodeData(
+      this,
+      method,
+      mode,
+      nodeOrEdge,
+      nameOrEdge,
+      add1
+    );
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided updater is not a function.`
+      );
+
+    data.attributes = updater(data.attributes);
+
+    // Emitting
+    this.emit('nodeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * List of methods to attach.
+ */
+const NODE_ATTRIBUTES_METHODS = [
+  {
+    name: element => `get${element}Attribute`,
+    attacher: attachNodeAttributeGetter
+  },
+  {
+    name: element => `get${element}Attributes`,
+    attacher: attachNodeAttributesGetter
+  },
+  {
+    name: element => `has${element}Attribute`,
+    attacher: attachNodeAttributeChecker
+  },
+  {
+    name: element => `set${element}Attribute`,
+    attacher: attachNodeAttributeSetter
+  },
+  {
+    name: element => `update${element}Attribute`,
+    attacher: attachNodeAttributeUpdater
+  },
+  {
+    name: element => `remove${element}Attribute`,
+    attacher: attachNodeAttributeRemover
+  },
+  {
+    name: element => `replace${element}Attributes`,
+    attacher: attachNodeAttributesReplacer
+  },
+  {
+    name: element => `merge${element}Attributes`,
+    attacher: attachNodeAttributesMerger
+  },
+  {
+    name: element => `update${element}Attributes`,
+    attacher: attachNodeAttributesUpdater
+  }
+];
+
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+function attachNodeAttributesMethods(Graph) {
+  NODE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) {
+    // For nodes
+    attacher(Graph, name('Node'), NODE);
+
+    // For sources
+    attacher(Graph, name('Source'), SOURCE);
+
+    // For targets
+    attacher(Graph, name('Target'), TARGET);
+
+    // For opposites
+    attacher(Graph, name('Opposite'), OPPOSITE);
+  });
+}
+
+/**
+ * Graphology Edge Attributes methods
+ * ===================================
+ */
+
+/**
+ * Attach an attribute getter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeGetter(Class, method, type) {
+  /**
+   * Get the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {mixed}          - The attribute's value.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes[name];
+  };
+}
+
+/**
+ * Attach an attributes getter method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+function attachEdgeAttributesGetter(Class, method, type) {
+  /**
+   * Retrieves all the target element's attributes.
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   *
+   * @return {object}          - The element's attributes.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 1) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + arguments[1];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes;
+  };
+}
+
+/**
+ * Attach an attribute checker method onto the provided class.
+ *
+ * @param {function} Class       - Target class.
+ * @param {string}   method      - Method name.
+ * @param {string}   type        - Type of the edge to find.
+ */
+function attachEdgeAttributeChecker(Class, method, type) {
+  /**
+   * Checks whether the desired attribute is set for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    return data.attributes.hasOwnProperty(name);
+  };
+}
+
+/**
+ * Attach an attribute setter method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeSetter(Class, method, type) {
+  /**
+   * Set the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   * @param  {mixed}  value   - New attribute value.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, value) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 3) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+      value = arguments[3];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    data.attributes[name] = value;
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeUpdater(Class, method, type) {
+  /**
+   * Update the desired attribute for the given element (node or edge) using
+   * the provided function.
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {string}   name    - Attribute's name.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name, updater) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 3) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+      updater = arguments[3];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: updater should be a function.`
+      );
+
+    data.attributes[name] = updater(data.attributes[name]);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'set',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute remover method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributeRemover(Class, method, type) {
+  /**
+   * Remove the desired attribute for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element - Target element.
+   * @param  {string} name    - Attribute's name.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source - Source element.
+   * @param  {any}     target - Target element.
+   * @param  {string}  name   - Attribute's name.
+   *
+   * @return {Graph}          - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, name) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element;
+      const target = '' + name;
+
+      name = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    delete data.attributes[name];
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'remove',
+      attributes: data.attributes,
+      name
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute replacer method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesReplacer(Class, method, type) {
+  /**
+   * Replace the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - New attributes.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - New attributes.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + attributes;
+
+      attributes = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    data.attributes = attributes;
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'replace',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute merger method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesMerger(Class, method, type) {
+  /**
+   * Merge the attributes for the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}    element    - Target element.
+   * @param  {object} attributes - Attributes to merge.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}     source     - Source element.
+   * @param  {any}     target     - Target element.
+   * @param  {object}  attributes - Attributes to merge.
+   *
+   * @return {Graph}              - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, attributes) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + attributes;
+
+      attributes = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided attributes are not a plain object.`
+      );
+
+    assign(data.attributes, attributes);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'merge',
+      attributes: data.attributes,
+      data: attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * Attach an attribute updater method onto the provided class.
+ *
+ * @param {function} Class         - Target class.
+ * @param {string}   method        - Method name.
+ * @param {string}   type          - Type of the edge to find.
+ */
+function attachEdgeAttributesUpdater(Class, method, type) {
+  /**
+   * Update the attributes of the given element (node or edge).
+   *
+   * Arity 2:
+   * @param  {any}      element - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * Arity 3 (only for edges):
+   * @param  {any}      source  - Source element.
+   * @param  {any}      target  - Target element.
+   * @param  {function} updater - Updater function.
+   *
+   * @return {Graph}            - Returns itself for chaining.
+   *
+   * @throws {Error} - Will throw if too many arguments are provided.
+   * @throws {Error} - Will throw if any of the elements is not found.
+   */
+  Class.prototype[method] = function (element, updater) {
+    let data;
+
+    if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type)
+      throw new UsageGraphError(
+        `Graph.${method}: cannot find this type of edges in your ${this.type} graph.`
+      );
+
+    if (arguments.length > 2) {
+      if (this.multi)
+        throw new UsageGraphError(
+          `Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`
+        );
+
+      const source = '' + element,
+        target = '' + updater;
+
+      updater = arguments[2];
+
+      data = getMatchingEdge(this, source, target, type);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`
+        );
+    } else {
+      if (type !== 'mixed')
+        throw new UsageGraphError(
+          `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.`
+        );
+
+      element = '' + element;
+      data = this._edges.get(element);
+
+      if (!data)
+        throw new NotFoundGraphError(
+          `Graph.${method}: could not find the "${element}" edge in the graph.`
+        );
+    }
+
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.${method}: provided updater is not a function.`
+      );
+
+    data.attributes = updater(data.attributes);
+
+    // Emitting
+    this.emit('edgeAttributesUpdated', {
+      key: data.key,
+      type: 'update',
+      attributes: data.attributes
+    });
+
+    return this;
+  };
+}
+
+/**
+ * List of methods to attach.
+ */
+const EDGE_ATTRIBUTES_METHODS = [
+  {
+    name: element => `get${element}Attribute`,
+    attacher: attachEdgeAttributeGetter
+  },
+  {
+    name: element => `get${element}Attributes`,
+    attacher: attachEdgeAttributesGetter
+  },
+  {
+    name: element => `has${element}Attribute`,
+    attacher: attachEdgeAttributeChecker
+  },
+  {
+    name: element => `set${element}Attribute`,
+    attacher: attachEdgeAttributeSetter
+  },
+  {
+    name: element => `update${element}Attribute`,
+    attacher: attachEdgeAttributeUpdater
+  },
+  {
+    name: element => `remove${element}Attribute`,
+    attacher: attachEdgeAttributeRemover
+  },
+  {
+    name: element => `replace${element}Attributes`,
+    attacher: attachEdgeAttributesReplacer
+  },
+  {
+    name: element => `merge${element}Attributes`,
+    attacher: attachEdgeAttributesMerger
+  },
+  {
+    name: element => `update${element}Attributes`,
+    attacher: attachEdgeAttributesUpdater
+  }
+];
+
+/**
+ * Attach every attributes-related methods to a Graph class.
+ *
+ * @param {function} Graph - Target class.
+ */
+function attachEdgeAttributesMethods(Graph) {
+  EDGE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) {
+    // For edges
+    attacher(Graph, name('Edge'), 'mixed');
+
+    // For directed edges
+    attacher(Graph, name('DirectedEdge'), 'directed');
+
+    // For undirected edges
+    attacher(Graph, name('UndirectedEdge'), 'undirected');
+  });
+}
+
+/**
+ * Graphology Edge Iteration
+ * ==========================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's edges.
+ */
+
+/**
+ * Definitions.
+ */
+const EDGES_ITERATION = [
+  {
+    name: 'edges',
+    type: 'mixed'
+  },
+  {
+    name: 'inEdges',
+    type: 'directed',
+    direction: 'in'
+  },
+  {
+    name: 'outEdges',
+    type: 'directed',
+    direction: 'out'
+  },
+  {
+    name: 'inboundEdges',
+    type: 'mixed',
+    direction: 'in'
+  },
+  {
+    name: 'outboundEdges',
+    type: 'mixed',
+    direction: 'out'
+  },
+  {
+    name: 'directedEdges',
+    type: 'directed'
+  },
+  {
+    name: 'undirectedEdges',
+    type: 'undirected'
+  }
+];
+
+/**
+ * Function iterating over edges from the given object to match one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {function} callback - Function to call.
+ */
+function forEachSimple(breakable, object, callback, avoid) {
+  let shouldBreak = false;
+
+  for (const k in object) {
+    if (k === avoid) continue;
+
+    const edgeData = object[k];
+
+    shouldBreak = callback(
+      edgeData.key,
+      edgeData.attributes,
+      edgeData.source.key,
+      edgeData.target.key,
+      edgeData.source.attributes,
+      edgeData.target.attributes,
+      edgeData.undirected
+    );
+
+    if (breakable && shouldBreak) return edgeData.key;
+  }
+
+  return;
+}
+
+function forEachMulti(breakable, object, callback, avoid) {
+  let edgeData, source, target;
+
+  let shouldBreak = false;
+
+  for (const k in object) {
+    if (k === avoid) continue;
+
+    edgeData = object[k];
+
+    do {
+      source = edgeData.source;
+      target = edgeData.target;
+
+      shouldBreak = callback(
+        edgeData.key,
+        edgeData.attributes,
+        source.key,
+        target.key,
+        source.attributes,
+        target.attributes,
+        edgeData.undirected
+      );
+
+      if (breakable && shouldBreak) return edgeData.key;
+
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+  }
+
+  return;
+}
+
+/**
+ * Function returning an iterator over edges from the given object.
+ *
+ * @param  {object}   object - Target object.
+ * @return {Iterator}
+ */
+function createIterator(object, avoid) {
+  const keys = Object.keys(object);
+  const l = keys.length;
+
+  let edgeData;
+  let i = 0;
+
+  return new Iterator(function next() {
+    do {
+      if (!edgeData) {
+        if (i >= l) return {done: true};
+
+        const k = keys[i++];
+
+        if (k === avoid) {
+          edgeData = undefined;
+          continue;
+        }
+
+        edgeData = object[k];
+      } else {
+        edgeData = edgeData.next;
+      }
+    } while (!edgeData);
+
+    return {
+      done: false,
+      value: {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      }
+    };
+  });
+}
+
+/**
+ * Function iterating over the egdes from the object at given key to match
+ * one of them.
+ *
+ * @param {object}   object   - Target object.
+ * @param {mixed}    k        - Neighbor key.
+ * @param {function} callback - Callback to use.
+ */
+function forEachForKeySimple(breakable, object, k, callback) {
+  const edgeData = object[k];
+
+  if (!edgeData) return;
+
+  const sourceData = edgeData.source;
+  const targetData = edgeData.target;
+
+  if (
+    callback(
+      edgeData.key,
+      edgeData.attributes,
+      sourceData.key,
+      targetData.key,
+      sourceData.attributes,
+      targetData.attributes,
+      edgeData.undirected
+    ) &&
+    breakable
+  )
+    return edgeData.key;
+}
+
+function forEachForKeyMulti(breakable, object, k, callback) {
+  let edgeData = object[k];
+
+  if (!edgeData) return;
+
+  let shouldBreak = false;
+
+  do {
+    shouldBreak = callback(
+      edgeData.key,
+      edgeData.attributes,
+      edgeData.source.key,
+      edgeData.target.key,
+      edgeData.source.attributes,
+      edgeData.target.attributes,
+      edgeData.undirected
+    );
+
+    if (breakable && shouldBreak) return edgeData.key;
+
+    edgeData = edgeData.next;
+  } while (edgeData !== undefined);
+
+  return;
+}
+
+/**
+ * Function returning an iterator over the egdes from the object at given key.
+ *
+ * @param  {object}   object   - Target object.
+ * @param  {mixed}    k        - Neighbor key.
+ * @return {Iterator}
+ */
+function createIteratorForKey(object, k) {
+  let edgeData = object[k];
+
+  if (edgeData.next !== undefined) {
+    return new Iterator(function () {
+      if (!edgeData) return {done: true};
+
+      const value = {
+        edge: edgeData.key,
+        attributes: edgeData.attributes,
+        source: edgeData.source.key,
+        target: edgeData.target.key,
+        sourceAttributes: edgeData.source.attributes,
+        targetAttributes: edgeData.target.attributes,
+        undirected: edgeData.undirected
+      };
+
+      edgeData = edgeData.next;
+
+      return {
+        done: false,
+        value
+      };
+    });
+  }
+
+  return Iterator.of({
+    edge: edgeData.key,
+    attributes: edgeData.attributes,
+    source: edgeData.source.key,
+    target: edgeData.target.key,
+    sourceAttributes: edgeData.source.attributes,
+    targetAttributes: edgeData.target.attributes,
+    undirected: edgeData.undirected
+  });
+}
+
+/**
+ * Function creating an array of edges for the given type.
+ *
+ * @param  {Graph}   graph - Target Graph instance.
+ * @param  {string}  type  - Type of edges to retrieve.
+ * @return {array}         - Array of edges.
+ */
+function createEdgeArray(graph, type) {
+  if (graph.size === 0) return [];
+
+  if (type === 'mixed' || type === graph.type) {
+    if (typeof Array.from === 'function')
+      return Array.from(graph._edges.keys());
+
+    return take(graph._edges.keys(), graph._edges.size);
+  }
+
+  const size =
+    type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+
+  const list = new Array(size),
+    mask = type === 'undirected';
+
+  const iterator = graph._edges.values();
+
+  let i = 0;
+  let step, data;
+
+  while (((step = iterator.next()), step.done !== true)) {
+    data = step.value;
+
+    if (data.undirected === mask) list[i++] = data.key;
+  }
+
+  return list;
+}
+
+/**
+ * Function iterating over a graph's edges using a callback to match one of
+ * them.
+ *
+ * @param  {Graph}    graph    - Target Graph instance.
+ * @param  {string}   type     - Type of edges to retrieve.
+ * @param  {function} callback - Function to call.
+ */
+function forEachEdge(breakable, graph, type, callback) {
+  if (graph.size === 0) return;
+
+  const shouldFilter = type !== 'mixed' && type !== graph.type;
+  const mask = type === 'undirected';
+
+  let step, data;
+  let shouldBreak = false;
+  const iterator = graph._edges.values();
+
+  while (((step = iterator.next()), step.done !== true)) {
+    data = step.value;
+
+    if (shouldFilter && data.undirected !== mask) continue;
+
+    const {key, attributes, source, target} = data;
+
+    shouldBreak = callback(
+      key,
+      attributes,
+      source.key,
+      target.key,
+      source.attributes,
+      target.attributes,
+      data.undirected
+    );
+
+    if (breakable && shouldBreak) return key;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an iterator of edges for the given type.
+ *
+ * @param  {Graph}    graph - Target Graph instance.
+ * @param  {string}   type  - Type of edges to retrieve.
+ * @return {Iterator}
+ */
+function createEdgeIterator(graph, type) {
+  if (graph.size === 0) return Iterator.empty();
+
+  const shouldFilter = type !== 'mixed' && type !== graph.type;
+  const mask = type === 'undirected';
+
+  const iterator = graph._edges.values();
+
+  return new Iterator(function next() {
+    let step, data;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      step = iterator.next();
+
+      if (step.done) return step;
+
+      data = step.value;
+
+      if (shouldFilter && data.undirected !== mask) continue;
+
+      break;
+    }
+
+    const value = {
+      edge: data.key,
+      attributes: data.attributes,
+      source: data.source.key,
+      target: data.target.key,
+      sourceAttributes: data.source.attributes,
+      targetAttributes: data.target.attributes,
+      undirected: data.undirected
+    };
+
+    return {value, done: false};
+  });
+}
+
+/**
+ * Function iterating over a node's edges using a callback to match one of them.
+ *
+ * @param  {boolean}  multi     - Whether the graph is multi or not.
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Function to call.
+ */
+function forEachEdgeForNode(
+  breakable,
+  multi,
+  type,
+  direction,
+  nodeData,
+  callback
+) {
+  const fn = multi ? forEachMulti : forEachSimple;
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = fn(breakable, nodeData.in, callback);
+
+      if (breakable && found) return found;
+    }
+    if (direction !== 'in') {
+      found = fn(
+        breakable,
+        nodeData.out,
+        callback,
+        !direction ? nodeData.key : undefined
+      );
+
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    found = fn(breakable, nodeData.undirected, callback);
+
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of edges for the given type & the given node.
+ *
+ * @param  {boolean} multi     - Whether the graph is multi or not.
+ * @param  {string}  type      - Type of edges to retrieve.
+ * @param  {string}  direction - In or out?
+ * @param  {any}     nodeData  - Target node's data.
+ * @return {array}             - Array of edges.
+ */
+function createEdgeArrayForNode(multi, type, direction, nodeData) {
+  const edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+    edges.push(key);
+  });
+
+  return edges;
+}
+
+/**
+ * Function iterating over a node's edges using a callback.
+ *
+ * @param  {string}   type      - Type of edges to retrieve.
+ * @param  {string}   direction - In or out?
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+function createEdgeIteratorForNode(type, direction, nodeData) {
+  let iterator = Iterator.empty();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out' && typeof nodeData.in !== 'undefined')
+      iterator = chain(iterator, createIterator(nodeData.in));
+    if (direction !== 'in' && typeof nodeData.out !== 'undefined')
+      iterator = chain(
+        iterator,
+        createIterator(nodeData.out, !direction ? nodeData.key : undefined)
+      );
+  }
+
+  if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+    iterator = chain(iterator, createIterator(nodeData.undirected));
+  }
+
+  return iterator;
+}
+
+/**
+ * Function iterating over edges for the given path using a callback to match
+ * one of them.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+function forEachEdgeForPath(
+  breakable,
+  type,
+  multi,
+  direction,
+  sourceData,
+  target,
+  callback
+) {
+  const fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (typeof sourceData.in !== 'undefined' && direction !== 'out') {
+      found = fn(breakable, sourceData.in, target, callback);
+
+      if (breakable && found) return found;
+    }
+
+    if (
+      typeof sourceData.out !== 'undefined' &&
+      direction !== 'in' &&
+      (direction || sourceData.key !== target)
+    ) {
+      found = fn(breakable, sourceData.out, target, callback);
+
+      if (breakable && found) return found;
+    }
+  }
+
+  if (type !== 'directed') {
+    if (typeof sourceData.undirected !== 'undefined') {
+      found = fn(breakable, sourceData.undirected, target, callback);
+
+      if (breakable && found) return found;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {boolean}  multi      - Whether the graph is multi.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {any}      target     - Target node.
+ * @return {array}               - Array of edges.
+ */
+function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+  const edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+  forEachEdgeForPath(
+    false,
+    type,
+    multi,
+    direction,
+    sourceData,
+    target,
+    function (key) {
+      edges.push(key);
+    }
+  );
+
+  return edges;
+}
+
+/**
+ * Function returning an iterator over edges for the given path.
+ *
+ * @param  {string}   type       - Type of edges to retrieve.
+ * @param  {string}   direction  - In or out?
+ * @param  {NodeData} sourceData - Source node's data.
+ * @param  {string}   target     - Target node.
+ * @param  {function} callback   - Function to call.
+ */
+function createEdgeIteratorForPath(type, direction, sourceData, target) {
+  let iterator = Iterator.empty();
+
+  if (type !== 'undirected') {
+    if (
+      typeof sourceData.in !== 'undefined' &&
+      direction !== 'out' &&
+      target in sourceData.in
+    )
+      iterator = chain(iterator, createIteratorForKey(sourceData.in, target));
+
+    if (
+      typeof sourceData.out !== 'undefined' &&
+      direction !== 'in' &&
+      target in sourceData.out &&
+      (direction || sourceData.key !== target)
+    )
+      iterator = chain(iterator, createIteratorForKey(sourceData.out, target));
+  }
+
+  if (type !== 'directed') {
+    if (
+      typeof sourceData.undirected !== 'undefined' &&
+      target in sourceData.undirected
+    )
+      iterator = chain(
+        iterator,
+        createIteratorForKey(sourceData.undirected, target)
+      );
+  }
+
+  return iterator;
+}
+
+/**
+ * Function attaching an edge array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachEdgeArrayCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  /**
+   * Function returning an array of certain edges.
+   *
+   * Arity 0: Return all the relevant edges.
+   *
+   * Arity 1: Return all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Return the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return [];
+
+    if (!arguments.length) return createEdgeArray(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${name}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      return createEdgeArrayForNode(
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData
+      );
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return createEdgeArrayForPath(
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).`
+    );
+  };
+}
+
+/**
+ * Function attaching a edge callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachForEachEdge(Class, description) {
+  const {name, type, direction} = description;
+
+  const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[forEachName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(false, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+      return forEachEdgeForNode(
+        false,
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData,
+        callback
+      );
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${forEachName}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return forEachEdgeForPath(
+        false,
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target,
+        callback
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${forEachName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).`
+    );
+  };
+
+  /**
+   * Function mapping the graph's relevant edges by applying the given
+   * callback.
+   *
+   * Arity 1: Map all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Map all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Map the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    let result;
+
+    // We know the result length beforehand
+    if (args.length === 0) {
+      let length = 0;
+
+      if (type !== 'directed') length += this.undirectedSize;
+      if (type !== 'undirected') length += this.directedSize;
+
+      result = new Array(length);
+
+      let i = 0;
+
+      args.push((e, ea, s, t, sa, ta, u) => {
+        result[i++] = callback(e, ea, s, t, sa, ta, u);
+      });
+    }
+
+    // We don't know the result length beforehand
+    // TODO: we can in some instances of simple graphs, knowing degree
+    else {
+      result = [];
+
+      args.push((e, ea, s, t, sa, ta, u) => {
+        result.push(callback(e, ea, s, t, sa, ta, u));
+      });
+    }
+
+    this[forEachName].apply(this, args);
+
+    return result;
+  };
+
+  /**
+   * Function filtering the graph's relevant edges using the provided predicate
+   * function.
+   *
+   * Arity 1: Filter all the relevant edges.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 2: Filter all of a node's relevant edges.
+   * @param  {any}      node      - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * Arity 3: Filter the relevant edges across the given path.
+   * @param  {any}      source    - Source node.
+   * @param  {any}      target    - Target node.
+   * @param  {function} predicate - Predicate to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    const result = [];
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+    });
+
+    this[forEachName].apply(this, args);
+
+    return result;
+  };
+
+  /**
+   * Function reducing the graph's relevant edges using the provided accumulator
+   * function.
+   *
+   * Arity 1: Reduce all the relevant edges.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 2: Reduce all of a node's relevant edges.
+   * @param  {any}      node         - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * Arity 3: Reduce the relevant edges across the given path.
+   * @param  {any}      source       - Source node.
+   * @param  {any}      target       - Target node.
+   * @param  {function} accumulator  - Accumulator to use.
+   * @param  {any}      initialValue - Initial value.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function () {
+    let args = Array.prototype.slice.call(arguments);
+
+    if (args.length < 2 || args.length > 4) {
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: invalid number of arguments (expecting 2, 3 or 4 and got ${args.length}).`
+      );
+    }
+
+    if (
+      typeof args[args.length - 1] === 'function' &&
+      typeof args[args.length - 2] !== 'function'
+    ) {
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.`
+      );
+    }
+
+    let callback;
+    let initialValue;
+
+    if (args.length === 2) {
+      callback = args[0];
+      initialValue = args[1];
+      args = [];
+    } else if (args.length === 3) {
+      callback = args[1];
+      initialValue = args[2];
+      args = [args[0]];
+    } else if (args.length === 4) {
+      callback = args[2];
+      initialValue = args[3];
+      args = [args[0], args[1]];
+    }
+
+    let accumulator = initialValue;
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+    });
+
+    this[forEachName].apply(this, args);
+
+    return accumulator;
+  };
+}
+
+/**
+ * Function attaching a breakable edge callback iterator method to the Graph
+ * prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachFindEdge(Class, description) {
+  const {name, type, direction} = description;
+
+  const findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over the graph's relevant edges in order to match
+   * one of them using the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[findEdgeName] = function (source, target, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return false;
+
+    if (arguments.length === 1) {
+      callback = source;
+      return forEachEdge(true, this, type, callback);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      callback = target;
+
+      const nodeData = this._nodes.get(source);
+
+      if (typeof nodeData === 'undefined')
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      // TODO: maybe attach the sub method to the instance dynamically?
+      return forEachEdgeForNode(
+        true,
+        this.multi,
+        type === 'mixed' ? this.type : type,
+        direction,
+        nodeData,
+        callback
+      );
+    }
+
+    if (arguments.length === 3) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${findEdgeName}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return forEachEdgeForPath(
+        true,
+        type,
+        this.multi,
+        direction,
+        sourceData,
+        target,
+        callback
+      );
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${findEdgeName}: too many arguments (expecting 1, 2 or 3 and got ${arguments.length}).`
+    );
+  };
+
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether any one of them matches the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[someName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      return callback(e, ea, s, t, sa, ta, u);
+    });
+
+    const found = this[findEdgeName].apply(this, args);
+
+    if (found) return true;
+
+    return false;
+  };
+
+  /**
+   * Function iterating over the graph's relevant edges in order to assert
+   * whether all of them matche the provided predicate function.
+   *
+   * Arity 1: Iterate over all the relevant edges.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 2: Iterate over all of a node's relevant edges.
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * Arity 3: Iterate over the relevant edges across the given path.
+   * @param  {any}      source   - Source node.
+   * @param  {any}      target   - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+  Class.prototype[everyName] = function () {
+    const args = Array.prototype.slice.call(arguments);
+    const callback = args.pop();
+
+    args.push((e, ea, s, t, sa, ta, u) => {
+      return !callback(e, ea, s, t, sa, ta, u);
+    });
+
+    const found = this[findEdgeName].apply(this, args);
+
+    if (found) return false;
+
+    return true;
+  };
+}
+
+/**
+ * Function attaching an edge iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachEdgeIteratorCreator(Class, description) {
+  const {name: originalName, type, direction} = description;
+
+  const name = originalName.slice(0, -1) + 'Entries';
+
+  /**
+   * Function returning an iterator over the graph's edges.
+   *
+   * Arity 0: Iterate over all the relevant edges.
+   *
+   * Arity 1: Iterate over all of a node's relevant edges.
+   * @param  {any}   node   - Target node.
+   *
+   * Arity 2: Iterate over the relevant edges across the given path.
+   * @param  {any}   source - Source node.
+   * @param  {any}   target - Target node.
+   *
+   * @return {array|number} - The edges or the number of edges.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[name] = function (source, target) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return Iterator.empty();
+
+    if (!arguments.length) return createEdgeIterator(this, type);
+
+    if (arguments.length === 1) {
+      source = '' + source;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}: could not find the "${source}" node in the graph.`
+        );
+
+      // Iterating over a node's edges
+      return createEdgeIteratorForNode(type, direction, sourceData);
+    }
+
+    if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      const sourceData = this._nodes.get(source);
+
+      if (!sourceData)
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${source}" source node in the graph.`
+        );
+
+      if (!this._nodes.has(target))
+        throw new NotFoundGraphError(
+          `Graph.${name}:  could not find the "${target}" target node in the graph.`
+        );
+
+      // Iterating over the edges between source & target
+      return createEdgeIteratorForPath(type, direction, sourceData, target);
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: too many arguments (expecting 0, 1 or 2 and got ${arguments.length}).`
+    );
+  };
+}
+
+/**
+ * Function attaching every edge iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+function attachEdgeIterationMethods(Graph) {
+  EDGES_ITERATION.forEach(description => {
+    attachEdgeArrayCreator(Graph, description);
+    attachForEachEdge(Graph, description);
+    attachFindEdge(Graph, description);
+    attachEdgeIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Neighbor Iteration
+ * ==============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over
+ * neighbors.
+ */
+
+/**
+ * Definitions.
+ */
+const NEIGHBORS_ITERATION = [
+  {
+    name: 'neighbors',
+    type: 'mixed'
+  },
+  {
+    name: 'inNeighbors',
+    type: 'directed',
+    direction: 'in'
+  },
+  {
+    name: 'outNeighbors',
+    type: 'directed',
+    direction: 'out'
+  },
+  {
+    name: 'inboundNeighbors',
+    type: 'mixed',
+    direction: 'in'
+  },
+  {
+    name: 'outboundNeighbors',
+    type: 'mixed',
+    direction: 'out'
+  },
+  {
+    name: 'directedNeighbors',
+    type: 'directed'
+  },
+  {
+    name: 'undirectedNeighbors',
+    type: 'undirected'
+  }
+];
+
+/**
+ * Helpers.
+ */
+function CompositeSetWrapper() {
+  this.A = null;
+  this.B = null;
+}
+
+CompositeSetWrapper.prototype.wrap = function (set) {
+  if (this.A === null) this.A = set;
+  else if (this.B === null) this.B = set;
+};
+
+CompositeSetWrapper.prototype.has = function (key) {
+  if (this.A !== null && key in this.A) return true;
+  if (this.B !== null && key in this.B) return true;
+  return false;
+};
+
+/**
+ * Function iterating over the given node's relevant neighbors to match
+ * one of them using a predicated function.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @param  {function} callback  - Callback to use.
+ */
+function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+  for (const k in object) {
+    const edgeData = object[k];
+
+    const sourceData = edgeData.source;
+    const targetData = edgeData.target;
+
+    const neighborData = sourceData === nodeData ? targetData : sourceData;
+
+    if (visited && visited.has(neighborData.key)) continue;
+
+    const shouldBreak = callback(neighborData.key, neighborData.attributes);
+
+    if (breakable && shouldBreak) return neighborData.key;
+  }
+
+  return;
+}
+
+function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected')
+      return forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData.undirected,
+        callback
+      );
+
+    if (typeof direction === 'string')
+      return forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData[direction],
+        callback
+      );
+  }
+
+  // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+  const visited = new CompositeSetWrapper();
+
+  let found;
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      found = forEachInObjectOnce(
+        breakable,
+        null,
+        nodeData,
+        nodeData.in,
+        callback
+      );
+
+      if (breakable && found) return found;
+
+      visited.wrap(nodeData.in);
+    }
+    if (direction !== 'in') {
+      found = forEachInObjectOnce(
+        breakable,
+        visited,
+        nodeData,
+        nodeData.out,
+        callback
+      );
+
+      if (breakable && found) return found;
+
+      visited.wrap(nodeData.out);
+    }
+  }
+
+  if (type !== 'directed') {
+    found = forEachInObjectOnce(
+      breakable,
+      visited,
+      nodeData,
+      nodeData.undirected,
+      callback
+    );
+
+    if (breakable && found) return found;
+  }
+
+  return;
+}
+
+/**
+ * Function creating an array of relevant neighbors for the given node.
+ *
+ * @param  {string}       type      - Type of neighbors.
+ * @param  {string}       direction - Direction.
+ * @param  {any}          nodeData  - Target node's data.
+ * @return {Array}                  - The list of neighbors.
+ */
+function createNeighborArrayForNode(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected') return Object.keys(nodeData.undirected);
+
+    if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+  }
+
+  const neighbors = [];
+
+  forEachNeighbor(false, type, direction, nodeData, function (key) {
+    neighbors.push(key);
+  });
+
+  return neighbors;
+}
+
+/**
+ * Function returning an iterator over the given node's relevant neighbors.
+ *
+ * @param  {string}   type      - Type of neighbors.
+ * @param  {string}   direction - Direction.
+ * @param  {any}      nodeData  - Target node's data.
+ * @return {Iterator}
+ */
+function createDedupedObjectIterator(visited, nodeData, object) {
+  const keys = Object.keys(object);
+  const l = keys.length;
+
+  let i = 0;
+
+  return new Iterator(function next() {
+    let neighborData = null;
+
+    do {
+      if (i >= l) {
+        if (visited) visited.wrap(object);
+        return {done: true};
+      }
+
+      const edgeData = object[keys[i++]];
+
+      const sourceData = edgeData.source;
+      const targetData = edgeData.target;
+
+      neighborData = sourceData === nodeData ? targetData : sourceData;
+
+      if (visited && visited.has(neighborData.key)) {
+        neighborData = null;
+        continue;
+      }
+    } while (neighborData === null);
+
+    return {
+      done: false,
+      value: {neighbor: neighborData.key, attributes: neighborData.attributes}
+    };
+  });
+}
+
+function createNeighborIterator(type, direction, nodeData) {
+  // If we want only undirected or in or out, we can roll some optimizations
+  if (type !== 'mixed') {
+    if (type === 'undirected')
+      return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+
+    if (typeof direction === 'string')
+      return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+  }
+
+  let iterator = Iterator.empty();
+
+  // Else we need to keep a set of neighbors not to return duplicates
+  // We cheat by querying the other adjacencies
+  const visited = new CompositeSetWrapper();
+
+  if (type !== 'undirected') {
+    if (direction !== 'out') {
+      iterator = chain(
+        iterator,
+        createDedupedObjectIterator(visited, nodeData, nodeData.in)
+      );
+    }
+    if (direction !== 'in') {
+      iterator = chain(
+        iterator,
+        createDedupedObjectIterator(visited, nodeData, nodeData.out)
+      );
+    }
+  }
+
+  if (type !== 'directed') {
+    iterator = chain(
+      iterator,
+      createDedupedObjectIterator(visited, nodeData, nodeData.undirected)
+    );
+  }
+
+  return iterator;
+}
+
+/**
+ * Function attaching a neighbors array creator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachNeighborArrayCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  /**
+   * Function returning an array of certain neighbors.
+   *
+   * @param  {any}   node   - Target node.
+   * @return {array} - The neighbors of neighbors.
+   *
+   * @throws {Error} - Will throw if node is not found in the graph.
+   */
+  Class.prototype[name] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return [];
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${name}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return createNeighborArrayForNode(
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData
+    );
+  };
+}
+
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachForEachNeighbor(Class, description) {
+  const {name, type, direction} = description;
+
+  const forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[forEachName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${forEachName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    forEachNeighbor(
+      false,
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData,
+      callback
+    );
+  };
+
+  /**
+   * Function mapping the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[mapName] = function (node, callback) {
+    // TODO: optimize when size is known beforehand
+    const result = [];
+
+    this[forEachName](node, (n, a) => {
+      result.push(callback(n, a));
+    });
+
+    return result;
+  };
+
+  /**
+   * Function filtering the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[filterName] = function (node, callback) {
+    const result = [];
+
+    this[forEachName](node, (n, a) => {
+      if (callback(n, a)) result.push(n);
+    });
+
+    return result;
+  };
+
+  /**
+   * Function reducing the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+  Class.prototype[reduceName] = function (node, callback, initialValue) {
+    if (arguments.length < 3)
+      throw new InvalidArgumentsGraphError(
+        `Graph.${reduceName}: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.`
+      );
+
+    let accumulator = initialValue;
+
+    this[forEachName](node, (n, a) => {
+      accumulator = callback(accumulator, n, a);
+    });
+
+    return accumulator;
+  };
+}
+
+/**
+ * Function attaching a breakable neighbors callback iterator method to the
+ * Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachFindNeighbor(Class, description) {
+  const {name, type, direction} = description;
+
+  const capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+
+  const findName = 'find' + capitalizedSingular;
+
+  /**
+   * Function iterating over all the relevant neighbors using a callback.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {undefined}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[findName] = function (node, callback) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${findName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return forEachNeighbor(
+      true,
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData,
+      callback
+    );
+  };
+
+  /**
+   * Function iterating over all the relevant neighbors to find if any of them
+   * matches the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const someName = 'some' + capitalizedSingular;
+
+  Class.prototype[someName] = function (node, callback) {
+    const found = this[findName](node, callback);
+
+    if (found) return true;
+
+    return false;
+  };
+
+  /**
+   * Function iterating over all the relevant neighbors to find if all of them
+   * matche the given predicate.
+   *
+   * @param  {any}      node     - Target node.
+   * @param  {function} callback - Callback to use.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  const everyName = 'every' + capitalizedSingular;
+
+  Class.prototype[everyName] = function (node, callback) {
+    const found = this[findName](node, (n, a) => {
+      return !callback(n, a);
+    });
+
+    if (found) return false;
+
+    return true;
+  };
+}
+
+/**
+ * Function attaching a neighbors callback iterator method to the Graph prototype.
+ *
+ * @param {function} Class       - Target class.
+ * @param {object}   description - Method description.
+ */
+function attachNeighborIteratorCreator(Class, description) {
+  const {name, type, direction} = description;
+
+  const iteratorName = name.slice(0, -1) + 'Entries';
+
+  /**
+   * Function returning an iterator over all the relevant neighbors.
+   *
+   * @param  {any}      node     - Target node.
+   * @return {Iterator}
+   *
+   * @throws {Error} - Will throw if there are too many arguments.
+   */
+  Class.prototype[iteratorName] = function (node) {
+    // Early termination
+    if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type)
+      return Iterator.empty();
+
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (typeof nodeData === 'undefined')
+      throw new NotFoundGraphError(
+        `Graph.${iteratorName}: could not find the "${node}" node in the graph.`
+      );
+
+    // Here, we want to iterate over a node's relevant neighbors
+    return createNeighborIterator(
+      type === 'mixed' ? this.type : type,
+      direction,
+      nodeData
+    );
+  };
+}
+
+/**
+ * Function attaching every neighbor iteration method to the Graph class.
+ *
+ * @param {function} Graph - Graph class.
+ */
+function attachNeighborIterationMethods(Graph) {
+  NEIGHBORS_ITERATION.forEach(description => {
+    attachNeighborArrayCreator(Graph, description);
+    attachForEachNeighbor(Graph, description);
+    attachFindNeighbor(Graph, description);
+    attachNeighborIteratorCreator(Graph, description);
+  });
+}
+
+/**
+ * Graphology Adjacency Iteration
+ * ===============================
+ *
+ * Attaching some methods to the Graph class to be able to iterate over a
+ * graph's adjacency.
+ */
+
+/**
+ * Function iterating over a simple graph's adjacency using a callback.
+ *
+ * @param {boolean}  breakable         - Can we break?
+ * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+ * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+ * @param {Graph}    graph             - Target Graph instance.
+ * @param {callback} function          - Iteration callback.
+ */
+function forEachAdjacency(
+  breakable,
+  assymetric,
+  disconnectedNodes,
+  graph,
+  callback
+) {
+  const iterator = graph._nodes.values();
+
+  const type = graph.type;
+
+  let step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+  while (((step = iterator.next()), step.done !== true)) {
+    let hasEdges = false;
+
+    sourceData = step.value;
+
+    if (type !== 'undirected') {
+      adj = sourceData.out;
+
+      for (neighbor in adj) {
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+
+          hasEdges = true;
+          shouldBreak = callback(
+            sourceData.key,
+            targetData.key,
+            sourceData.attributes,
+            targetData.attributes,
+            edgeData.key,
+            edgeData.attributes,
+            edgeData.undirected
+          );
+
+          if (breakable && shouldBreak) return edgeData;
+
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (type !== 'directed') {
+      adj = sourceData.undirected;
+
+      for (neighbor in adj) {
+        if (assymetric && sourceData.key > neighbor) continue;
+
+        edgeData = adj[neighbor];
+
+        do {
+          targetData = edgeData.target;
+
+          if (targetData.key !== neighbor) targetData = edgeData.source;
+
+          hasEdges = true;
+          shouldBreak = callback(
+            sourceData.key,
+            targetData.key,
+            sourceData.attributes,
+            targetData.attributes,
+            edgeData.key,
+            edgeData.attributes,
+            edgeData.undirected
+          );
+
+          if (breakable && shouldBreak) return edgeData;
+
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (disconnectedNodes && !hasEdges) {
+      shouldBreak = callback(
+        sourceData.key,
+        null,
+        sourceData.attributes,
+        null,
+        null,
+        null,
+        null
+      );
+
+      if (breakable && shouldBreak) return null;
+    }
+  }
+
+  return;
+}
+
+/**
+ * Graphology Serialization Utilities
+ * ===================================
+ *
+ * Collection of functions used by the graph serialization schemes.
+ */
+
+/**
+ * Formats internal node data into a serialized node.
+ *
+ * @param  {any}    key  - The node's key.
+ * @param  {object} data - Internal node's data.
+ * @return {array}       - The serialized node.
+ */
+function serializeNode(key, data) {
+  const serialized = {key};
+
+  if (!isEmpty(data.attributes))
+    serialized.attributes = assign({}, data.attributes);
+
+  return serialized;
+}
+
+/**
+ * Formats internal edge data into a serialized edge.
+ *
+ * @param  {any}    key  - The edge's key.
+ * @param  {object} data - Internal edge's data.
+ * @return {array}       - The serialized edge.
+ */
+function serializeEdge(key, data) {
+  const serialized = {
+    key,
+    source: data.source.key,
+    target: data.target.key
+  };
+
+  if (!isEmpty(data.attributes))
+    serialized.attributes = assign({}, data.attributes);
+
+  if (data.undirected) serialized.undirected = true;
+
+  return serialized;
+}
+
+/**
+ * Checks whether the given value is a serialized node.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+function validateSerializedNode(value) {
+  if (!isPlainObject(value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.'
+    );
+
+  if (!('key' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized node is missing its key.'
+    );
+
+  if (
+    'attributes' in value &&
+    (!isPlainObject(value.attributes) || value.attributes === null)
+  )
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.'
+    );
+}
+
+/**
+ * Checks whether the given value is a serialized edge.
+ *
+ * @param  {mixed} value - Target value.
+ * @return {string|null}
+ */
+function validateSerializedEdge(value) {
+  if (!isPlainObject(value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.'
+    );
+
+  if (!('source' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized edge is missing its source.'
+    );
+
+  if (!('target' in value))
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: serialized edge is missing its target.'
+    );
+
+  if (
+    'attributes' in value &&
+    (!isPlainObject(value.attributes) || value.attributes === null)
+  )
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.'
+    );
+
+  if ('undirected' in value && typeof value.undirected !== 'boolean')
+    throw new InvalidArgumentsGraphError(
+      'Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.'
+    );
+}
+
+/* eslint no-nested-ternary: 0 */
+
+/**
+ * Constants.
+ */
+const INSTANCE_ID = incrementalIdStartingFromRandomByte();
+
+/**
+ * Enums.
+ */
+const TYPES = new Set(['directed', 'undirected', 'mixed']);
+
+const EMITTER_PROPS = new Set([
+  'domain',
+  '_events',
+  '_eventsCount',
+  '_maxListeners'
+]);
+
+const EDGE_ADD_METHODS = [
+  {
+    name: verb => `${verb}Edge`,
+    generateKey: true
+  },
+  {
+    name: verb => `${verb}DirectedEdge`,
+    generateKey: true,
+    type: 'directed'
+  },
+  {
+    name: verb => `${verb}UndirectedEdge`,
+    generateKey: true,
+    type: 'undirected'
+  },
+  {
+    name: verb => `${verb}EdgeWithKey`
+  },
+  {
+    name: verb => `${verb}DirectedEdgeWithKey`,
+    type: 'directed'
+  },
+  {
+    name: verb => `${verb}UndirectedEdgeWithKey`,
+    type: 'undirected'
+  }
+];
+
+/**
+ * Default options.
+ */
+const DEFAULTS = {
+  allowSelfLoops: true,
+  multi: false,
+  type: 'mixed'
+};
+
+/**
+ * Abstract functions used by the Graph class for various methods.
+ */
+
+/**
+ * Internal method used to add a node to the given graph
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {any}     node            - The node's key.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {NodeData}                - Created node data.
+ */
+function addNode(graph, node, attributes) {
+  if (attributes && !isPlainObject(attributes))
+    throw new InvalidArgumentsGraphError(
+      `Graph.addNode: invalid attributes. Expecting an object but got "${attributes}"`
+    );
+
+  // String coercion
+  node = '' + node;
+  attributes = attributes || {};
+
+  if (graph._nodes.has(node))
+    throw new UsageGraphError(
+      `Graph.addNode: the "${node}" node already exist in the graph.`
+    );
+
+  const data = new graph.NodeDataClass(node, attributes);
+
+  // Adding the node to internal register
+  graph._nodes.set(node, data);
+
+  // Emitting
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes
+  });
+
+  return data;
+}
+
+/**
+ * Same as the above but without sanity checks because we call this in contexts
+ * where necessary checks were already done.
+ */
+function unsafeAddNode(graph, node, attributes) {
+  const data = new graph.NodeDataClass(node, attributes);
+
+  graph._nodes.set(node, data);
+
+  graph.emit('nodeAdded', {
+    key: node,
+    attributes
+  });
+
+  return data;
+}
+
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+function addEdge(
+  graph,
+  name,
+  mustGenerateKey,
+  undirected,
+  edge,
+  source,
+  target,
+  attributes
+) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead.`
+    );
+
+  if (undirected && graph.type === 'directed')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead.`
+    );
+
+  if (attributes && !isPlainObject(attributes))
+    throw new InvalidArgumentsGraphError(
+      `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"`
+    );
+
+  // Coercion of source & target:
+  source = '' + source;
+  target = '' + target;
+  attributes = attributes || {};
+
+  if (!graph.allowSelfLoops && source === target)
+    throw new UsageGraphError(
+      `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.`
+    );
+
+  const sourceData = graph._nodes.get(source),
+    targetData = graph._nodes.get(target);
+
+  if (!sourceData)
+    throw new NotFoundGraphError(
+      `Graph.${name}: source node "${source}" not found.`
+    );
+
+  if (!targetData)
+    throw new NotFoundGraphError(
+      `Graph.${name}: target node "${target}" not found.`
+    );
+
+  // Must the graph generate an id for this edge?
+  const eventData = {
+    key: null,
+    undirected,
+    source,
+    target,
+    attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge;
+
+    // Here, we have a key collision
+    if (graph._edges.has(edge))
+      throw new UsageGraphError(
+        `Graph.${name}: the "${edge}" edge already exists in the graph.`
+      );
+  }
+
+  // Here, we might have a source / target collision
+  if (
+    !graph.multi &&
+    (undirected
+      ? typeof sourceData.undirected[target] !== 'undefined'
+      : typeof sourceData.out[target] !== 'undefined')
+  ) {
+    throw new UsageGraphError(
+      `Graph.${name}: an edge linking "${source}" to "${target}" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option.`
+    );
+  }
+
+  // Storing some data
+  const edgeData = new EdgeData(
+    undirected,
+    edge,
+    sourceData,
+    targetData,
+    attributes
+  );
+
+  // Adding the edge to the internal register
+  graph._edges.set(edge, edgeData);
+
+  // Incrementing node degree counters
+  const isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  }
+
+  // Updating relevant index
+  if (graph.multi) edgeData.attachMulti();
+  else edgeData.attach();
+
+  if (undirected) graph._undirectedSize++;
+  else graph._directedSize++;
+
+  // Emitting
+  eventData.key = edge;
+
+  graph.emit('edgeAdded', eventData);
+
+  return edge;
+}
+
+/**
+ * Internal method used to add an arbitrary edge to the given graph.
+ *
+ * @param  {Graph}   graph           - Target graph.
+ * @param  {string}  name            - Name of the child method for errors.
+ * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+ * @param  {boolean} undirected      - Whether the edge is undirected.
+ * @param  {any}     edge            - The edge's key.
+ * @param  {any}     source          - The source node.
+ * @param  {any}     target          - The target node.
+ * @param  {object}  [attributes]    - Optional attributes.
+ * @param  {boolean} [asUpdater]       - Are we updating or merging?
+ * @return {any}                     - The edge.
+ *
+ * @throws {Error} - Will throw if the graph is of the wrong type.
+ * @throws {Error} - Will throw if the given attributes are not an object.
+ * @throws {Error} - Will throw if source or target doesn't exist.
+ * @throws {Error} - Will throw if the edge already exist.
+ */
+function mergeEdge(
+  graph,
+  name,
+  mustGenerateKey,
+  undirected,
+  edge,
+  source,
+  target,
+  attributes,
+  asUpdater
+) {
+  // Checking validity of operation
+  if (!undirected && graph.type === 'undirected')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead.`
+    );
+
+  if (undirected && graph.type === 'directed')
+    throw new UsageGraphError(
+      `Graph.${name}: you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead.`
+    );
+
+  if (attributes) {
+    if (asUpdater) {
+      if (typeof attributes !== 'function')
+        throw new InvalidArgumentsGraphError(
+          `Graph.${name}: invalid updater function. Expecting a function but got "${attributes}"`
+        );
+    } else {
+      if (!isPlainObject(attributes))
+        throw new InvalidArgumentsGraphError(
+          `Graph.${name}: invalid attributes. Expecting an object but got "${attributes}"`
+        );
+    }
+  }
+
+  // Coercion of source & target:
+  source = '' + source;
+  target = '' + target;
+
+  let updater;
+
+  if (asUpdater) {
+    updater = attributes;
+    attributes = undefined;
+  }
+
+  if (!graph.allowSelfLoops && source === target)
+    throw new UsageGraphError(
+      `Graph.${name}: source & target are the same ("${source}"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false.`
+    );
+
+  let sourceData = graph._nodes.get(source);
+  let targetData = graph._nodes.get(target);
+  let edgeData;
+
+  // Do we need to handle duplicate?
+  let alreadyExistingEdgeData;
+
+  if (!mustGenerateKey) {
+    edgeData = graph._edges.get(edge);
+
+    if (edgeData) {
+      // Here, we need to ensure, if the user gave a key, that source & target
+      // are consistent
+      if (edgeData.source.key !== source || edgeData.target.key !== target) {
+        // If source or target inconsistent
+        if (
+          !undirected ||
+          edgeData.source.key !== target ||
+          edgeData.target.key !== source
+        ) {
+          // If directed, or source/target aren't flipped
+          throw new UsageGraphError(
+            `Graph.${name}: inconsistency detected when attempting to merge the "${edge}" edge with "${source}" source & "${target}" target vs. ("${edgeData.source.key}", "${edgeData.target.key}").`
+          );
+        }
+      }
+
+      alreadyExistingEdgeData = edgeData;
+    }
+  }
+
+  // Here, we might have a source / target collision
+  if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+    alreadyExistingEdgeData = undirected
+      ? sourceData.undirected[target]
+      : sourceData.out[target];
+  }
+
+  // Handling duplicates
+  if (alreadyExistingEdgeData) {
+    const info = [alreadyExistingEdgeData.key, false, false, false];
+
+    // We can skip the attribute merging part if the user did not provide them
+    if (asUpdater ? !updater : !attributes) return info;
+
+    // Updating the attributes
+    if (asUpdater) {
+      const oldAttributes = alreadyExistingEdgeData.attributes;
+      alreadyExistingEdgeData.attributes = updater(oldAttributes);
+
+      graph.emit('edgeAttributesUpdated', {
+        type: 'replace',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes
+      });
+    }
+
+    // Merging the attributes
+    else {
+      assign(alreadyExistingEdgeData.attributes, attributes);
+
+      graph.emit('edgeAttributesUpdated', {
+        type: 'merge',
+        key: alreadyExistingEdgeData.key,
+        attributes: alreadyExistingEdgeData.attributes,
+        data: attributes
+      });
+    }
+
+    return info;
+  }
+
+  attributes = attributes || {};
+
+  if (asUpdater && updater) attributes = updater(attributes);
+
+  // Must the graph generate an id for this edge?
+  const eventData = {
+    key: null,
+    undirected,
+    source,
+    target,
+    attributes
+  };
+
+  if (mustGenerateKey) {
+    // NOTE: in this case we can guarantee that the key does not already
+    // exist and is already correctly casted as a string
+    edge = graph._edgeKeyGenerator();
+  } else {
+    // Coercion of edge key
+    edge = '' + edge;
+
+    // Here, we have a key collision
+    if (graph._edges.has(edge))
+      throw new UsageGraphError(
+        `Graph.${name}: the "${edge}" edge already exists in the graph.`
+      );
+  }
+
+  let sourceWasAdded = false;
+  let targetWasAdded = false;
+
+  if (!sourceData) {
+    sourceData = unsafeAddNode(graph, source, {});
+    sourceWasAdded = true;
+
+    if (source === target) {
+      targetData = sourceData;
+      targetWasAdded = true;
+    }
+  }
+  if (!targetData) {
+    targetData = unsafeAddNode(graph, target, {});
+    targetWasAdded = true;
+  }
+
+  // Storing some data
+  edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes);
+
+  // Adding the edge to the internal register
+  graph._edges.set(edge, edgeData);
+
+  // Incrementing node degree counters
+  const isSelfLoop = source === target;
+
+  if (undirected) {
+    sourceData.undirectedDegree++;
+    targetData.undirectedDegree++;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount++;
+  } else {
+    sourceData.outDegree++;
+    targetData.inDegree++;
+
+    if (isSelfLoop) graph._directedSelfLoopCount++;
+  }
+
+  // Updating relevant index
+  if (graph.multi) edgeData.attachMulti();
+  else edgeData.attach();
+
+  if (undirected) graph._undirectedSize++;
+  else graph._directedSize++;
+
+  // Emitting
+  eventData.key = edge;
+
+  graph.emit('edgeAdded', eventData);
+
+  return [edge, true, sourceWasAdded, targetWasAdded];
+}
+
+/**
+ * Internal method used to drop an edge.
+ *
+ * @param  {Graph}    graph    - Target graph.
+ * @param  {EdgeData} edgeData - Data of the edge to drop.
+ */
+function dropEdgeFromData(graph, edgeData) {
+  // Dropping the edge from the register
+  graph._edges.delete(edgeData.key);
+
+  // Updating related degrees
+  const {source: sourceData, target: targetData, attributes} = edgeData;
+
+  const undirected = edgeData.undirected;
+
+  const isSelfLoop = sourceData === targetData;
+
+  if (undirected) {
+    sourceData.undirectedDegree--;
+    targetData.undirectedDegree--;
+
+    if (isSelfLoop) graph._undirectedSelfLoopCount--;
+  } else {
+    sourceData.outDegree--;
+    targetData.inDegree--;
+
+    if (isSelfLoop) graph._directedSelfLoopCount--;
+  }
+
+  // Clearing index
+  if (graph.multi) edgeData.detachMulti();
+  else edgeData.detach();
+
+  if (undirected) graph._undirectedSize--;
+  else graph._directedSize--;
+
+  // Emitting
+  graph.emit('edgeDropped', {
+    key: edgeData.key,
+    attributes,
+    source: sourceData.key,
+    target: targetData.key,
+    undirected
+  });
+}
+
+/**
+ * Graph class
+ *
+ * @constructor
+ * @param  {object}  [options] - Options:
+ * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+ * @param  {string}    [type]           - Type of the graph.
+ * @param  {boolean}   [map]            - Allow references as keys?
+ * @param  {boolean}   [multi]          - Allow parallel edges?
+ *
+ * @throws {Error} - Will throw if the arguments are not valid.
+ */
+class Graph extends EventEmitter {
+  constructor(options) {
+    super();
+
+    //-- Solving options
+    options = assign({}, DEFAULTS, options);
+
+    // Enforcing options validity
+    if (typeof options.multi !== 'boolean')
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'multi' option. Expecting a boolean but got "${options.multi}".`
+      );
+
+    if (!TYPES.has(options.type))
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'type' option. Should be one of "mixed", "directed" or "undirected" but got "${options.type}".`
+      );
+
+    if (typeof options.allowSelfLoops !== 'boolean')
+      throw new InvalidArgumentsGraphError(
+        `Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got "${options.allowSelfLoops}".`
+      );
+
+    //-- Private properties
+
+    // Utilities
+    const NodeDataClass =
+      options.type === 'mixed'
+        ? MixedNodeData
+        : options.type === 'directed'
+        ? DirectedNodeData
+        : UndirectedNodeData;
+
+    privateProperty(this, 'NodeDataClass', NodeDataClass);
+
+    // Internal edge key generator
+
+    // NOTE: this internal generator produce keys that are strings
+    // composed of a weird prefix, an incremental instance id starting from
+    // a random byte and finally an internal instance incremental id.
+    // All this to avoid intra-frame and cross-frame adversarial inputs
+    // that can force a single #.addEdge call to degenerate into a O(n)
+    // available key search loop.
+
+    // It also ensures that automatically generated edge keys are unlikely
+    // to produce collisions with arbitrary keys given by users.
+    const instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+    let edgeId = 0;
+
+    const edgeKeyGenerator = () => {
+      let availableEdgeKey;
+
+      do {
+        availableEdgeKey = instancePrefix + edgeId++;
+      } while (this._edges.has(availableEdgeKey));
+
+      return availableEdgeKey;
+    };
+
+    // Indexes
+    privateProperty(this, '_attributes', {});
+    privateProperty(this, '_nodes', new Map());
+    privateProperty(this, '_edges', new Map());
+    privateProperty(this, '_directedSize', 0);
+    privateProperty(this, '_undirectedSize', 0);
+    privateProperty(this, '_directedSelfLoopCount', 0);
+    privateProperty(this, '_undirectedSelfLoopCount', 0);
+    privateProperty(this, '_edgeKeyGenerator', edgeKeyGenerator);
+
+    // Options
+    privateProperty(this, '_options', options);
+
+    // Emitter properties
+    EMITTER_PROPS.forEach(prop => privateProperty(this, prop, this[prop]));
+
+    //-- Properties readers
+    readOnlyProperty(this, 'order', () => this._nodes.size);
+    readOnlyProperty(this, 'size', () => this._edges.size);
+    readOnlyProperty(this, 'directedSize', () => this._directedSize);
+    readOnlyProperty(this, 'undirectedSize', () => this._undirectedSize);
+    readOnlyProperty(
+      this,
+      'selfLoopCount',
+      () => this._directedSelfLoopCount + this._undirectedSelfLoopCount
+    );
+    readOnlyProperty(
+      this,
+      'directedSelfLoopCount',
+      () => this._directedSelfLoopCount
+    );
+    readOnlyProperty(
+      this,
+      'undirectedSelfLoopCount',
+      () => this._undirectedSelfLoopCount
+    );
+    readOnlyProperty(this, 'multi', this._options.multi);
+    readOnlyProperty(this, 'type', this._options.type);
+    readOnlyProperty(this, 'allowSelfLoops', this._options.allowSelfLoops);
+    readOnlyProperty(this, 'implementation', () => 'graphology');
+  }
+
+  _resetInstanceCounters() {
+    this._directedSize = 0;
+    this._undirectedSize = 0;
+    this._directedSelfLoopCount = 0;
+    this._undirectedSelfLoopCount = 0;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Read
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning whether the given node is found in the graph.
+   *
+   * @param  {any}     node - The node.
+   * @return {boolean}
+   */
+  hasNode(node) {
+    return this._nodes.has('' + node);
+  }
+
+  /**
+   * Method returning whether the given directed edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasDirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'undirected') return false;
+
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      const edgeData = this._edges.get(edge);
+
+      return !!edgeData && !edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      const edges = nodeData.out[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning whether the given undirected edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasUndirectedEdge(source, target) {
+    // Early termination
+    if (this.type === 'directed') return false;
+
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      const edgeData = this._edges.get(edge);
+
+      return !!edgeData && edgeData.undirected;
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      const edges = nodeData.undirected[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasDirectedEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning whether the given edge is found in the graph.
+   *
+   * Arity 1:
+   * @param  {any}     edge - The edge's key.
+   *
+   * Arity 2:
+   * @param  {any}     source - The edge's source.
+   * @param  {any}     target - The edge's target.
+   *
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the arguments are invalid.
+   */
+  hasEdge(source, target) {
+    if (arguments.length === 1) {
+      const edge = '' + source;
+
+      return this._edges.has(edge);
+    } else if (arguments.length === 2) {
+      source = '' + source;
+      target = '' + target;
+
+      // If the node source or the target is not in the graph we break
+      const nodeData = this._nodes.get(source);
+
+      if (!nodeData) return false;
+
+      // Is there a directed edge pointing toward target?
+      let edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+
+      if (!edges)
+        edges =
+          typeof nodeData.undirected !== 'undefined' &&
+          nodeData.undirected[target];
+
+      if (!edges) return false;
+
+      return this.multi ? !!edges.size : true;
+    }
+
+    throw new InvalidArgumentsGraphError(
+      `Graph.hasEdge: invalid arity (${arguments.length}, instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target.`
+    );
+  }
+
+  /**
+   * Method returning the edge matching source & target in a directed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  directedEdge(source, target) {
+    if (this.type === 'undirected') return;
+
+    source = '' + source;
+    target = '' + target;
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.'
+      );
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.directedEdge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.directedEdge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData = (sourceData.out && sourceData.out[target]) || undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning the edge matching source & target in a undirected fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  undirectedEdge(source, target) {
+    if (this.type === 'directed') return;
+
+    source = '' + source;
+    target = '' + target;
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.'
+      );
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedEdge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.undirectedEdge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData =
+      (sourceData.undirected && sourceData.undirected[target]) || undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning the edge matching source & target in a mixed fashion.
+   *
+   * @param  {any} source - The edge's source.
+   * @param  {any} target - The edge's target.
+   *
+   * @return {any|undefined}
+   *
+   * @throws {Error} - Will throw if the graph is multi.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   */
+  edge(source, target) {
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.'
+      );
+
+    source = '' + source;
+    target = '' + target;
+
+    const sourceData = this._nodes.get(source);
+
+    if (!sourceData)
+      throw new NotFoundGraphError(
+        `Graph.edge: could not find the "${source}" source node in the graph.`
+      );
+
+    if (!this._nodes.has(target))
+      throw new NotFoundGraphError(
+        `Graph.edge: could not find the "${target}" target node in the graph.`
+      );
+
+    const edgeData =
+      (sourceData.out && sourceData.out[target]) ||
+      (sourceData.undirected && sourceData.undirected[target]) ||
+      undefined;
+
+    if (edgeData) return edgeData.key;
+  }
+
+  /**
+   * Method returning whether two nodes are directed neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areDirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areDirectedNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.in || neighbor in nodeData.out;
+  }
+
+  /**
+   * Method returning whether two nodes are out neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areOutNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areOutNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.out;
+  }
+
+  /**
+   * Method returning whether two nodes are in neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areInNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areInNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return false;
+
+    return neighbor in nodeData.in;
+  }
+
+  /**
+   * Method returning whether two nodes are undirected neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areUndirectedNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areUndirectedNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return false;
+
+    return neighbor in nodeData.undirected;
+  }
+
+  /**
+   * Method returning whether two nodes are neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.in || neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether two nodes are inbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areInboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areInboundNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.in) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether two nodes are outbound neighbors.
+   *
+   * @param  {any}     node     - The node's key.
+   * @param  {any}     neighbor - The neighbor's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  areOutboundNeighbors(node, neighbor) {
+    node = '' + node;
+    neighbor = '' + neighbor;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.areOutboundNeighbors: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type !== 'undirected') {
+      if (neighbor in nodeData.out) return true;
+    }
+
+    if (this.type !== 'directed') {
+      if (neighbor in nodeData.undirected) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning the given node's in degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.inDegree;
+  }
+
+  /**
+   * Method returning the given node's out degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.outDegree;
+  }
+
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  directedDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.directedDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    return nodeData.inDegree + nodeData.outDegree;
+  }
+
+  /**
+   * Method returning the given node's undirected degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  undirectedDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedDegree: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return 0;
+
+    return nodeData.undirectedDegree;
+  }
+
+  /**
+   * Method returning the given node's inbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inboundDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inboundDegree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's outbound degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outboundDegree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outboundDegree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's directed degree.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  degree(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.degree: could not find the "${node}" node in the graph.`
+      );
+
+    let degree = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+    }
+
+    return degree;
+  }
+
+  /**
+   * Method returning the given node's in degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.in[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.inDegree - loops;
+  }
+
+  /**
+   * Method returning the given node's out degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.out[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.outDegree - loops;
+  }
+
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  directedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.directedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'undirected') return 0;
+
+    const self = nodeData.out[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.inDegree + nodeData.outDegree - loops * 2;
+  }
+
+  /**
+   * Method returning the given node's undirected degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's in degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  undirectedDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.undirectedDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    if (this.type === 'directed') return 0;
+
+    const self = nodeData.undirected[node];
+    const loops = self ? (this.multi ? self.size : 1) : 0;
+
+    return nodeData.undirectedDegree - loops * 2;
+  }
+
+  /**
+   * Method returning the given node's inbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's inbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  inboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.inboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree;
+
+      self = nodeData.out[node];
+      loops += self ? (this.multi ? self.size : 1) : 0;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given node's outbound degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's outbound degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  outboundDegreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.outboundDegreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.outDegree;
+
+      self = nodeData.in[node];
+      loops += self ? (this.multi ? self.size : 1) : 0;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given node's directed degree without considering self loops.
+   *
+   * @param  {any}     node - The node's key.
+   * @return {number}       - The node's degree.
+   *
+   * @throws {Error} - Will throw if the node isn't in the graph.
+   */
+  degreeWithoutSelfLoops(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.degreeWithoutSelfLoops: could not find the "${node}" node in the graph.`
+      );
+
+    let self;
+    let degree = 0;
+    let loops = 0;
+
+    if (this.type !== 'directed') {
+      degree += nodeData.undirectedDegree;
+
+      self = nodeData.undirected[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    if (this.type !== 'undirected') {
+      degree += nodeData.inDegree + nodeData.outDegree;
+
+      self = nodeData.out[node];
+      loops += (self ? (this.multi ? self.size : 1) : 0) * 2;
+    }
+
+    return degree - loops;
+  }
+
+  /**
+   * Method returning the given edge's source.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's source.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  source(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.source: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source.key;
+  }
+
+  /**
+   * Method returning the given edge's target.
+   *
+   * @param  {any} edge - The edge's key.
+   * @return {any}      - The edge's target.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  target(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.target: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.target.key;
+  }
+
+  /**
+   * Method returning the given edge's extremities.
+   *
+   * @param  {any}   edge - The edge's key.
+   * @return {array}      - The edge's extremities.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  extremities(edge) {
+    edge = '' + edge;
+
+    const edgeData = this._edges.get(edge);
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.extremities: could not find the "${edge}" edge in the graph.`
+      );
+
+    return [edgeData.source.key, edgeData.target.key];
+  }
+
+  /**
+   * Given a node & an edge, returns the other extremity of the edge.
+   *
+   * @param  {any}   node - The node's key.
+   * @param  {any}   edge - The edge's key.
+   * @return {any}        - The related node.
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph or if the
+   *                   edge & node are not related.
+   */
+  opposite(node, edge) {
+    node = '' + node;
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.opposite: could not find the "${edge}" edge in the graph.`
+      );
+
+    const source = data.source.key;
+    const target = data.target.key;
+
+    if (node === source) return target;
+    if (node === target) return source;
+
+    throw new NotFoundGraphError(
+      `Graph.opposite: the "${node}" node is not attached to the "${edge}" edge (${source}, ${target}).`
+    );
+  }
+
+  /**
+   * Returns whether the given edge has the given node as extremity.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @param  {any}     node - The node's key.
+   * @return {boolean}      - The related node.
+   *
+   * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+   */
+  hasExtremity(edge, node) {
+    edge = '' + edge;
+    node = '' + node;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.hasExtremity: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source.key === node || data.target.key === node;
+  }
+
+  /**
+   * Method returning whether the given edge is undirected.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isUndirected(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isUndirected: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.undirected;
+  }
+
+  /**
+   * Method returning whether the given edge is directed.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isDirected(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isDirected: could not find the "${edge}" edge in the graph.`
+      );
+
+    return !data.undirected;
+  }
+
+  /**
+   * Method returning whether the given edge is a self loop.
+   *
+   * @param  {any}     edge - The edge's key.
+   * @return {boolean}
+   *
+   * @throws {Error} - Will throw if the edge isn't in the graph.
+   */
+  isSelfLoop(edge) {
+    edge = '' + edge;
+
+    const data = this._edges.get(edge);
+
+    if (!data)
+      throw new NotFoundGraphError(
+        `Graph.isSelfLoop: could not find the "${edge}" edge in the graph.`
+      );
+
+    return data.source === data.target;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Mutation
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to add a node to the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   *
+   * @throws {Error} - Will throw if the given node already exist.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   */
+  addNode(node, attributes) {
+    const nodeData = addNode(this, node, attributes);
+
+    return nodeData.key;
+  }
+
+  /**
+   * Method used to merge a node into the graph.
+   *
+   * @param  {any}    node         - The node.
+   * @param  {object} [attributes] - Optional attributes.
+   * @return {any}                 - The node.
+   */
+  mergeNode(node, attributes) {
+    if (attributes && !isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        `Graph.mergeNode: invalid attributes. Expecting an object but got "${attributes}"`
+      );
+
+    // String coercion
+    node = '' + node;
+    attributes = attributes || {};
+
+    // If the node already exists, we merge the attributes
+    let data = this._nodes.get(node);
+
+    if (data) {
+      if (attributes) {
+        assign(data.attributes, attributes);
+
+        this.emit('nodeAttributesUpdated', {
+          type: 'merge',
+          key: node,
+          attributes: data.attributes,
+          data: attributes
+        });
+      }
+      return [node, false];
+    }
+
+    data = new this.NodeDataClass(node, attributes);
+
+    // Adding the node to internal register
+    this._nodes.set(node, data);
+
+    // Emitting
+    this.emit('nodeAdded', {
+      key: node,
+      attributes
+    });
+
+    return [node, true];
+  }
+
+  /**
+   * Method used to add a node if it does not exist in the graph or else to
+   * update its attributes using a function.
+   *
+   * @param  {any}      node      - The node.
+   * @param  {function} [updater] - Optional updater function.
+   * @return {any}                - The node.
+   */
+  updateNode(node, updater) {
+    if (updater && typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        `Graph.updateNode: invalid updater function. Expecting a function but got "${updater}"`
+      );
+
+    // String coercion
+    node = '' + node;
+
+    // If the node already exists, we update the attributes
+    let data = this._nodes.get(node);
+
+    if (data) {
+      if (updater) {
+        const oldAttributes = data.attributes;
+        data.attributes = updater(oldAttributes);
+
+        this.emit('nodeAttributesUpdated', {
+          type: 'replace',
+          key: node,
+          attributes: data.attributes
+        });
+      }
+      return [node, false];
+    }
+
+    const attributes = updater ? updater({}) : {};
+
+    data = new this.NodeDataClass(node, attributes);
+
+    // Adding the node to internal register
+    this._nodes.set(node, data);
+
+    // Emitting
+    this.emit('nodeAdded', {
+      key: node,
+      attributes
+    });
+
+    return [node, true];
+  }
+
+  /**
+   * Method used to drop a single node & all its attached edges from the graph.
+   *
+   * @param  {any}    node - The node.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the node doesn't exist.
+   */
+  dropNode(node) {
+    node = '' + node;
+
+    const nodeData = this._nodes.get(node);
+
+    if (!nodeData)
+      throw new NotFoundGraphError(
+        `Graph.dropNode: could not find the "${node}" node in the graph.`
+      );
+
+    let edgeData;
+
+    // Removing attached edges
+    // NOTE: we could be faster here, but this is such a pain to maintain
+    if (this.type !== 'undirected') {
+      for (const neighbor in nodeData.out) {
+        edgeData = nodeData.out[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+
+      for (const neighbor in nodeData.in) {
+        edgeData = nodeData.in[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    if (this.type !== 'directed') {
+      for (const neighbor in nodeData.undirected) {
+        edgeData = nodeData.undirected[neighbor];
+
+        do {
+          dropEdgeFromData(this, edgeData);
+          edgeData = edgeData.next;
+        } while (edgeData);
+      }
+    }
+
+    // Dropping the node from the register
+    this._nodes.delete(node);
+
+    // Emitting
+    this.emit('nodeDropped', {
+      key: node,
+      attributes: nodeData.attributes
+    });
+  }
+
+  /**
+   * Method used to drop a single edge from the graph.
+   *
+   * Arity 1:
+   * @param  {any}    edge - The edge.
+   *
+   * Arity 2:
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropEdge(edge) {
+    let edgeData;
+
+    if (arguments.length > 1) {
+      const source = '' + arguments[0];
+      const target = '' + arguments[1];
+
+      edgeData = getMatchingEdge(this, source, target, this.type);
+
+      if (!edgeData)
+        throw new NotFoundGraphError(
+          `Graph.dropEdge: could not find the "${source}" -> "${target}" edge in the graph.`
+        );
+    } else {
+      edge = '' + edge;
+
+      edgeData = this._edges.get(edge);
+
+      if (!edgeData)
+        throw new NotFoundGraphError(
+          `Graph.dropEdge: could not find the "${edge}" edge in the graph.`
+        );
+    }
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to drop a single directed edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropDirectedEdge(source, target) {
+    if (arguments.length < 2)
+      throw new UsageGraphError(
+        'Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.'
+      );
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.'
+      );
+
+    source = '' + source;
+    target = '' + target;
+
+    const edgeData = getMatchingEdge(this, source, target, 'directed');
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.dropDirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.`
+      );
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to drop a single undirected edge from the graph.
+   *
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   *
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if the edge doesn't exist.
+   */
+  dropUndirectedEdge(source, target) {
+    if (arguments.length < 2)
+      throw new UsageGraphError(
+        'Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.'
+      );
+
+    if (this.multi)
+      throw new UsageGraphError(
+        'Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.'
+      );
+
+    const edgeData = getMatchingEdge(this, source, target, 'undirected');
+
+    if (!edgeData)
+      throw new NotFoundGraphError(
+        `Graph.dropUndirectedEdge: could not find a "${source}" -> "${target}" edge in the graph.`
+      );
+
+    dropEdgeFromData(this, edgeData);
+
+    return this;
+  }
+
+  /**
+   * Method used to remove every edge & every node from the graph.
+   *
+   * @return {Graph}
+   */
+  clear() {
+    // Clearing edges
+    this._edges.clear();
+
+    // Clearing nodes
+    this._nodes.clear();
+
+    // Reset counters
+    this._resetInstanceCounters();
+
+    // Emitting
+    this.emit('cleared');
+  }
+
+  /**
+   * Method used to remove every edge from the graph.
+   *
+   * @return {Graph}
+   */
+  clearEdges() {
+    // Clearing structure index
+    const iterator = this._nodes.values();
+
+    let step;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      step.value.clear();
+    }
+
+    // Clearing edges
+    this._edges.clear();
+
+    // Reset counters
+    this._resetInstanceCounters();
+
+    // Emitting
+    this.emit('edgesCleared');
+  }
+
+  /**---------------------------------------------------------------------------
+   * Attributes-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning the desired graph's attribute.
+   *
+   * @param  {string} name - Name of the attribute.
+   * @return {any}
+   */
+  getAttribute(name) {
+    return this._attributes[name];
+  }
+
+  /**
+   * Method returning the graph's attributes.
+   *
+   * @return {object}
+   */
+  getAttributes() {
+    return this._attributes;
+  }
+
+  /**
+   * Method returning whether the graph has the desired attribute.
+   *
+   * @param  {string}  name - Name of the attribute.
+   * @return {boolean}
+   */
+  hasAttribute(name) {
+    return this._attributes.hasOwnProperty(name);
+  }
+
+  /**
+   * Method setting a value for the desired graph's attribute.
+   *
+   * @param  {string}  name  - Name of the attribute.
+   * @param  {any}     value - Value for the attribute.
+   * @return {Graph}
+   */
+  setAttribute(name, value) {
+    this._attributes[name] = value;
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method using a function to update the desired graph's attribute's value.
+   *
+   * @param  {string}   name    - Name of the attribute.
+   * @param  {function} updater - Function use to update the attribute's value.
+   * @return {Graph}
+   */
+  updateAttribute(name, updater) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateAttribute: updater should be a function.'
+      );
+
+    const value = this._attributes[name];
+
+    this._attributes[name] = updater(value);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'set',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method removing the desired graph's attribute.
+   *
+   * @param  {string} name  - Name of the attribute.
+   * @return {Graph}
+   */
+  removeAttribute(name) {
+    delete this._attributes[name];
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'remove',
+      attributes: this._attributes,
+      name
+    });
+
+    return this;
+  }
+
+  /**
+   * Method replacing the graph's attributes.
+   *
+   * @param  {object} attributes - New attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  replaceAttributes(attributes) {
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        'Graph.replaceAttributes: provided attributes are not a plain object.'
+      );
+
+    this._attributes = attributes;
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'replace',
+      attributes: this._attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method merging the graph's attributes.
+   *
+   * @param  {object} attributes - Attributes to merge.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given attributes are not a plain object.
+   */
+  mergeAttributes(attributes) {
+    if (!isPlainObject(attributes))
+      throw new InvalidArgumentsGraphError(
+        'Graph.mergeAttributes: provided attributes are not a plain object.'
+      );
+
+    assign(this._attributes, attributes);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'merge',
+      attributes: this._attributes,
+      data: attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method updating the graph's attributes.
+   *
+   * @param  {function} updater - Function used to update the attributes.
+   * @return {Graph}
+   *
+   * @throws {Error} - Will throw if given updater is not a function.
+   */
+  updateAttributes(updater) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateAttributes: provided updater is not a function.'
+      );
+
+    this._attributes = updater(this._attributes);
+
+    // Emitting
+    this.emit('attributesUpdated', {
+      type: 'update',
+      attributes: this._attributes
+    });
+
+    return this;
+  }
+
+  /**
+   * Method used to update each node's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  updateEachNodeAttributes(updater, hints) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachNodeAttributes: expecting an updater function.'
+      );
+
+    if (hints && !validateHints(hints))
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+    }
+
+    this.emit('eachNodeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+
+  /**
+   * Method used to update each edge's attributes using the given function.
+   *
+   * @param {function}  updater - Updater function to use.
+   * @param {object}    [hints] - Optional hints.
+   */
+  updateEachEdgeAttributes(updater, hints) {
+    if (typeof updater !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachEdgeAttributes: expecting an updater function.'
+      );
+
+    if (hints && !validateHints(hints))
+      throw new InvalidArgumentsGraphError(
+        'Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}'
+      );
+
+    const iterator = this._edges.values();
+
+    let step, edgeData, sourceData, targetData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      edgeData = step.value;
+      sourceData = edgeData.source;
+      targetData = edgeData.target;
+
+      edgeData.attributes = updater(
+        edgeData.key,
+        edgeData.attributes,
+        sourceData.key,
+        targetData.key,
+        sourceData.attributes,
+        targetData.attributes,
+        edgeData.undirected
+      );
+    }
+
+    this.emit('eachEdgeAttributesUpdated', {
+      hints: hints ? hints : null
+    });
+  }
+
+  /**---------------------------------------------------------------------------
+   * Iteration-related methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method iterating over the graph's adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  forEachAdjacencyEntry(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAdjacencyEntry: expecting a callback.'
+      );
+
+    forEachAdjacency(false, false, false, this, callback);
+  }
+  forEachAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.'
+      );
+
+    forEachAdjacency(false, false, true, this, callback);
+  }
+
+  /**
+   * Method iterating over the graph's assymetric adjacency using the given callback.
+   *
+   * @param  {function}  callback - Callback to use.
+   */
+  forEachAssymetricAdjacencyEntry(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAssymetricAdjacencyEntry: expecting a callback.'
+      );
+
+    forEachAdjacency(false, true, false, this, callback);
+  }
+  forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.'
+      );
+
+    forEachAdjacency(false, true, true, this, callback);
+  }
+
+  /**
+   * Method returning the list of the graph's nodes.
+   *
+   * @return {array} - The nodes.
+   */
+  nodes() {
+    if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+
+    return take(this._nodes.keys(), this._nodes.size);
+  }
+
+  /**
+   * Method iterating over the graph's nodes using the given callback.
+   *
+   * @param  {function}  callback - Callback (key, attributes, index).
+   */
+  forEachNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.forEachNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      callback(nodeData.key, nodeData.attributes);
+    }
+  }
+
+  /**
+   * Method iterating attempting to find a node matching the given predicate
+   * function.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  findNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.findNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+    }
+
+    return;
+  }
+
+  /**
+   * Method mapping nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  mapNodes(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.mapNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    const result = new Array(this.order);
+    let i = 0;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      result[i++] = callback(nodeData.key, nodeData.attributes);
+    }
+
+    return result;
+  }
+
+  /**
+   * Method returning whether some node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  someNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.someNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes)) return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Method returning whether all node verify the given predicate.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  everyNode(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.everyNode: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (!callback(nodeData.key, nodeData.attributes)) return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Method filtering nodes.
+   *
+   * @param  {function}  callback - Callback (key, attributes).
+   */
+  filterNodes(callback) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.filterNodes: expecting a callback.'
+      );
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    const result = [];
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+
+      if (callback(nodeData.key, nodeData.attributes))
+        result.push(nodeData.key);
+    }
+
+    return result;
+  }
+
+  /**
+   * Method reducing nodes.
+   *
+   * @param  {function}  callback - Callback (accumulator, key, attributes).
+   */
+  reduceNodes(callback, initialValue) {
+    if (typeof callback !== 'function')
+      throw new InvalidArgumentsGraphError(
+        'Graph.reduceNodes: expecting a callback.'
+      );
+
+    if (arguments.length < 2)
+      throw new InvalidArgumentsGraphError(
+        'Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.'
+      );
+
+    let accumulator = initialValue;
+
+    const iterator = this._nodes.values();
+
+    let step, nodeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      nodeData = step.value;
+      accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+    }
+
+    return accumulator;
+  }
+
+  /**
+   * Method returning an iterator over the graph's node entries.
+   *
+   * @return {Iterator}
+   */
+  nodeEntries() {
+    const iterator = this._nodes.values();
+
+    return new Iterator(() => {
+      const step = iterator.next();
+
+      if (step.done) return step;
+
+      const data = step.value;
+
+      return {
+        value: {node: data.key, attributes: data.attributes},
+        done: false
+      };
+    });
+  }
+
+  /**---------------------------------------------------------------------------
+   * Serialization
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used to export the whole graph.
+   *
+   * @return {object} - The serialized graph.
+   */
+  export() {
+    const nodes = new Array(this._nodes.size);
+
+    let i = 0;
+
+    this._nodes.forEach((data, key) => {
+      nodes[i++] = serializeNode(key, data);
+    });
+
+    const edges = new Array(this._edges.size);
+
+    i = 0;
+
+    this._edges.forEach((data, key) => {
+      edges[i++] = serializeEdge(key, data);
+    });
+
+    return {
+      options: {
+        type: this.type,
+        multi: this.multi,
+        allowSelfLoops: this.allowSelfLoops
+      },
+      attributes: this.getAttributes(),
+      nodes,
+      edges
+    };
+  }
+
+  /**
+   * Method used to import a serialized graph.
+   *
+   * @param  {object|Graph} data  - The serialized graph.
+   * @param  {boolean}      merge - Whether to merge data.
+   * @return {Graph}              - Returns itself for chaining.
+   */
+  import(data, merge = false) {
+    // Importing a Graph instance directly
+    if (isGraph(data)) {
+      // Nodes
+      data.forEachNode((n, a) => {
+        if (merge) this.mergeNode(n, a);
+        else this.addNode(n, a);
+      });
+
+      // Edges
+      data.forEachEdge((e, a, s, t, _sa, _ta, u) => {
+        if (merge) {
+          if (u) this.mergeUndirectedEdgeWithKey(e, s, t, a);
+          else this.mergeDirectedEdgeWithKey(e, s, t, a);
+        } else {
+          if (u) this.addUndirectedEdgeWithKey(e, s, t, a);
+          else this.addDirectedEdgeWithKey(e, s, t, a);
+        }
+      });
+
+      return this;
+    }
+
+    // Importing a serialized graph
+    if (!isPlainObject(data))
+      throw new InvalidArgumentsGraphError(
+        'Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.'
+      );
+
+    if (data.attributes) {
+      if (!isPlainObject(data.attributes))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid attributes. Expecting a plain object.'
+        );
+
+      if (merge) this.mergeAttributes(data.attributes);
+      else this.replaceAttributes(data.attributes);
+    }
+
+    let i, l, list, node, edge;
+
+    if (data.nodes) {
+      list = data.nodes;
+
+      if (!Array.isArray(list))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid nodes. Expecting an array.'
+        );
+
+      for (i = 0, l = list.length; i < l; i++) {
+        node = list[i];
+
+        // Validating
+        validateSerializedNode(node);
+
+        // Adding the node
+        const {key, attributes} = node;
+
+        if (merge) this.mergeNode(key, attributes);
+        else this.addNode(key, attributes);
+      }
+    }
+
+    if (data.edges) {
+      list = data.edges;
+
+      if (!Array.isArray(list))
+        throw new InvalidArgumentsGraphError(
+          'Graph.import: invalid edges. Expecting an array.'
+        );
+
+      for (i = 0, l = list.length; i < l; i++) {
+        edge = list[i];
+
+        // Validating
+        validateSerializedEdge(edge);
+
+        // Adding the edge
+        const {source, target, attributes, undirected = false} = edge;
+
+        let method;
+
+        if ('key' in edge) {
+          method = merge
+            ? undirected
+              ? this.mergeUndirectedEdgeWithKey
+              : this.mergeDirectedEdgeWithKey
+            : undirected
+            ? this.addUndirectedEdgeWithKey
+            : this.addDirectedEdgeWithKey;
+
+          method.call(this, edge.key, source, target, attributes);
+        } else {
+          method = merge
+            ? undirected
+              ? this.mergeUndirectedEdge
+              : this.mergeDirectedEdge
+            : undirected
+            ? this.addUndirectedEdge
+            : this.addDirectedEdge;
+
+          method.call(this, source, target, attributes);
+        }
+      }
+    }
+
+    return this;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Utils
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method returning a null copy of the graph, i.e. a graph without nodes
+   * & edges but with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The null copy.
+   */
+  nullCopy(options) {
+    const graph = new Graph(assign({}, this._options, options));
+    graph.replaceAttributes(assign({}, this.getAttributes()));
+    return graph;
+  }
+
+  /**
+   * Method returning an empty copy of the graph, i.e. a graph without edges but
+   * with the exact same options.
+   *
+   * @param  {object} options - Options to merge with the current ones.
+   * @return {Graph}          - The empty copy.
+   */
+  emptyCopy(options) {
+    const graph = this.nullCopy(options);
+
+    this._nodes.forEach((nodeData, key) => {
+      const attributes = assign({}, nodeData.attributes);
+
+      // NOTE: no need to emit events since user cannot access the instance yet
+      nodeData = new graph.NodeDataClass(key, attributes);
+      graph._nodes.set(key, nodeData);
+    });
+
+    return graph;
+  }
+
+  /**
+   * Method returning an exact copy of the graph.
+   *
+   * @param  {object} options - Upgrade options.
+   * @return {Graph}          - The copy.
+   */
+  copy(options) {
+    options = options || {};
+
+    if (
+      typeof options.type === 'string' &&
+      options.type !== this.type &&
+      options.type !== 'mixed'
+    )
+      throw new UsageGraphError(
+        `Graph.copy: cannot create an incompatible copy from "${this.type}" type to "${options.type}" because this would mean losing information about the current graph.`
+      );
+
+    if (
+      typeof options.multi === 'boolean' &&
+      options.multi !== this.multi &&
+      options.multi !== true
+    )
+      throw new UsageGraphError(
+        'Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.'
+      );
+
+    if (
+      typeof options.allowSelfLoops === 'boolean' &&
+      options.allowSelfLoops !== this.allowSelfLoops &&
+      options.allowSelfLoops !== true
+    )
+      throw new UsageGraphError(
+        'Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.'
+      );
+
+    const graph = this.emptyCopy(options);
+
+    const iterator = this._edges.values();
+
+    let step, edgeData;
+
+    while (((step = iterator.next()), step.done !== true)) {
+      edgeData = step.value;
+
+      // NOTE: no need to emit events since user cannot access the instance yet
+      addEdge(
+        graph,
+        'copy',
+        false,
+        edgeData.undirected,
+        edgeData.key,
+        edgeData.source.key,
+        edgeData.target.key,
+        assign({}, edgeData.attributes)
+      );
+    }
+
+    return graph;
+  }
+
+  /**---------------------------------------------------------------------------
+   * Known methods
+   **---------------------------------------------------------------------------
+   */
+
+  /**
+   * Method used by JavaScript to perform JSON serialization.
+   *
+   * @return {object} - The serialized graph.
+   */
+  toJSON() {
+    return this.export();
+  }
+
+  /**
+   * Method returning [object Graph].
+   */
+  toString() {
+    return '[object Graph]';
+  }
+
+  /**
+   * Method used internally by node's console to display a custom object.
+   *
+   * @return {object} - Formatted object representation of the graph.
+   */
+  inspect() {
+    const nodes = {};
+    this._nodes.forEach((data, key) => {
+      nodes[key] = data.attributes;
+    });
+
+    const edges = {},
+      multiIndex = {};
+
+    this._edges.forEach((data, key) => {
+      const direction = data.undirected ? '--' : '->';
+
+      let label = '';
+
+      let source = data.source.key;
+      let target = data.target.key;
+      let tmp;
+
+      if (data.undirected && source > target) {
+        tmp = source;
+        source = target;
+        target = tmp;
+      }
+
+      const desc = `(${source})${direction}(${target})`;
+
+      if (!key.startsWith('geid_')) {
+        label += `[${key}]: `;
+      } else if (this.multi) {
+        if (typeof multiIndex[desc] === 'undefined') {
+          multiIndex[desc] = 0;
+        } else {
+          multiIndex[desc]++;
+        }
+
+        label += `${multiIndex[desc]}. `;
+      }
+
+      label += desc;
+
+      edges[label] = data.attributes;
+    });
+
+    const dummy = {};
+
+    for (const k in this) {
+      if (
+        this.hasOwnProperty(k) &&
+        !EMITTER_PROPS.has(k) &&
+        typeof this[k] !== 'function' &&
+        typeof k !== 'symbol'
+      )
+        dummy[k] = this[k];
+    }
+
+    dummy.attributes = this._attributes;
+    dummy.nodes = nodes;
+    dummy.edges = edges;
+
+    privateProperty(dummy, 'constructor', this.constructor);
+
+    return dummy;
+  }
+}
+
+/**
+ * Attaching methods to the prototype.
+ *
+ * Here, we are attaching a wide variety of methods to the Graph class'
+ * prototype when those are very numerous and when their creation is
+ * abstracted.
+ */
+
+/**
+ * Attaching custom inspect method for node >= 10.
+ */
+if (typeof Symbol !== 'undefined')
+  Graph.prototype[Symbol.for('nodejs.util.inspect.custom')] =
+    Graph.prototype.inspect;
+
+/**
+ * Related to edge addition.
+ */
+EDGE_ADD_METHODS.forEach(method => {
+  ['add', 'merge', 'update'].forEach(verb => {
+    const name = method.name(verb);
+    const fn = verb === 'add' ? addEdge : mergeEdge;
+
+    if (method.generateKey) {
+      Graph.prototype[name] = function (source, target, attributes) {
+        return fn(
+          this,
+          name,
+          true,
+          (method.type || this.type) === 'undirected',
+          null,
+          source,
+          target,
+          attributes,
+          verb === 'update'
+        );
+      };
+    } else {
+      Graph.prototype[name] = function (edge, source, target, attributes) {
+        return fn(
+          this,
+          name,
+          false,
+          (method.type || this.type) === 'undirected',
+          edge,
+          source,
+          target,
+          attributes,
+          verb === 'update'
+        );
+      };
+    }
+  });
+});
+
+/**
+ * Attributes-related.
+ */
+attachNodeAttributesMethods(Graph);
+attachEdgeAttributesMethods(Graph);
+
+/**
+ * Edge iteration-related.
+ */
+attachEdgeIterationMethods(Graph);
+
+/**
+ * Neighbor iteration-related.
+ */
+attachNeighborIterationMethods(Graph);
+
+/**
+ * Graphology Helper Classes
+ * ==========================
+ *
+ * Building some higher-order classes instantiating the graph with
+ * predefinite options.
+ */
+
+/**
+ * Alternative constructors.
+ */
+class DirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'directed'}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== false)
+      throw new InvalidArgumentsGraphError(
+        'DirectedGraph.from: inconsistent indication that the graph should be multi in given options!'
+      );
+
+    if (finalOptions.type !== 'directed')
+      throw new InvalidArgumentsGraphError(
+        'DirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class UndirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'undirected'}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== false)
+      throw new InvalidArgumentsGraphError(
+        'UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!'
+      );
+
+    if (finalOptions.type !== 'undirected')
+      throw new InvalidArgumentsGraphError(
+        'UndirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiDirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'directed', multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    if (finalOptions.type !== 'directed')
+      throw new InvalidArgumentsGraphError(
+        'MultiDirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+class MultiUndirectedGraph extends Graph {
+  constructor(options) {
+    const finalOptions = assign({type: 'undirected', multi: true}, options);
+
+    if ('multi' in finalOptions && finalOptions.multi !== true)
+      throw new InvalidArgumentsGraphError(
+        'MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!'
+      );
+
+    if (finalOptions.type !== 'undirected')
+      throw new InvalidArgumentsGraphError(
+        'MultiUndirectedGraph.from: inconsistent "' +
+          finalOptions.type +
+          '" type in given options!'
+      );
+
+    super(finalOptions);
+  }
+}
+
+/**
+ * Attaching static #.from method to each of the constructors.
+ */
+function attachStaticFromMethod(Class) {
+  /**
+   * Builds a graph from serialized data or another graph's data.
+   *
+   * @param  {Graph|SerializedGraph} data      - Hydratation data.
+   * @param  {object}                [options] - Options.
+   * @return {Class}
+   */
+  Class.from = function (data, options) {
+    // Merging given options with serialized ones
+    const finalOptions = assign({}, data.options, options);
+
+    const instance = new Class(finalOptions);
+    instance.import(data);
+
+    return instance;
+  };
+}
+
+attachStaticFromMethod(Graph);
+attachStaticFromMethod(DirectedGraph);
+attachStaticFromMethod(UndirectedGraph);
+attachStaticFromMethod(MultiGraph);
+attachStaticFromMethod(MultiDirectedGraph);
+attachStaticFromMethod(MultiUndirectedGraph);
+
+Graph.Graph = Graph;
+Graph.DirectedGraph = DirectedGraph;
+Graph.UndirectedGraph = UndirectedGraph;
+Graph.MultiGraph = MultiGraph;
+Graph.MultiDirectedGraph = MultiDirectedGraph;
+Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+
+Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+Graph.NotFoundGraphError = NotFoundGraphError;
+Graph.UsageGraphError = UsageGraphError;
+
+/**
+ * Graphology ESM Endoint
+ * =======================
+ *
+ * Endpoint for ESM modules consumers.
+ */
+
+export { DirectedGraph, Graph, InvalidArgumentsGraphError, MultiDirectedGraph, MultiGraph, MultiUndirectedGraph, NotFoundGraphError, UndirectedGraph, UsageGraphError, Graph as default };
+//# sourceMappingURL=graphology.esm.js.map
diff --git a/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.js b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3d07b30a7f3c952015811f39d73c699d8c064d5
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.js
@@ -0,0 +1,6125 @@
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.graphology = factory());
+})(this, (function () { 'use strict';
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+      return typeof obj;
+    } : function (obj) {
+      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+    }, _typeof(obj);
+  }
+
+  function _inheritsLoose(subClass, superClass) {
+    subClass.prototype = Object.create(superClass.prototype);
+    subClass.prototype.constructor = subClass;
+
+    _setPrototypeOf(subClass, superClass);
+  }
+
+  function _getPrototypeOf(o) {
+    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
+      return o.__proto__ || Object.getPrototypeOf(o);
+    };
+    return _getPrototypeOf(o);
+  }
+
+  function _setPrototypeOf(o, p) {
+    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
+      o.__proto__ = p;
+      return o;
+    };
+
+    return _setPrototypeOf(o, p);
+  }
+
+  function _isNativeReflectConstruct() {
+    if (typeof Reflect === "undefined" || !Reflect.construct) return false;
+    if (Reflect.construct.sham) return false;
+    if (typeof Proxy === "function") return true;
+
+    try {
+      Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  function _construct(Parent, args, Class) {
+    if (_isNativeReflectConstruct()) {
+      _construct = Reflect.construct;
+    } else {
+      _construct = function _construct(Parent, args, Class) {
+        var a = [null];
+        a.push.apply(a, args);
+        var Constructor = Function.bind.apply(Parent, a);
+        var instance = new Constructor();
+        if (Class) _setPrototypeOf(instance, Class.prototype);
+        return instance;
+      };
+    }
+
+    return _construct.apply(null, arguments);
+  }
+
+  function _isNativeFunction(fn) {
+    return Function.toString.call(fn).indexOf("[native code]") !== -1;
+  }
+
+  function _wrapNativeSuper(Class) {
+    var _cache = typeof Map === "function" ? new Map() : undefined;
+
+    _wrapNativeSuper = function _wrapNativeSuper(Class) {
+      if (Class === null || !_isNativeFunction(Class)) return Class;
+
+      if (typeof Class !== "function") {
+        throw new TypeError("Super expression must either be null or a function");
+      }
+
+      if (typeof _cache !== "undefined") {
+        if (_cache.has(Class)) return _cache.get(Class);
+
+        _cache.set(Class, Wrapper);
+      }
+
+      function Wrapper() {
+        return _construct(Class, arguments, _getPrototypeOf(this).constructor);
+      }
+
+      Wrapper.prototype = Object.create(Class.prototype, {
+        constructor: {
+          value: Wrapper,
+          enumerable: false,
+          writable: true,
+          configurable: true
+        }
+      });
+      return _setPrototypeOf(Wrapper, Class);
+    };
+
+    return _wrapNativeSuper(Class);
+  }
+
+  function _assertThisInitialized(self) {
+    if (self === void 0) {
+      throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+    }
+
+    return self;
+  }
+
+  /**
+   * Graphology Utilities
+   * =====================
+   *
+   * Collection of helpful functions used by the implementation.
+   */
+
+  /**
+   * Object.assign-like polyfill.
+   *
+   * @param  {object} target       - First object.
+   * @param  {object} [...objects] - Objects to merge.
+   * @return {object}
+   */
+  function assignPolyfill() {
+    var target = arguments[0];
+
+    for (var i = 1, l = arguments.length; i < l; i++) {
+      if (!arguments[i]) continue;
+
+      for (var k in arguments[i]) {
+        target[k] = arguments[i][k];
+      }
+    }
+
+    return target;
+  }
+
+  var assign = assignPolyfill;
+  if (typeof Object.assign === 'function') assign = Object.assign;
+  /**
+   * Function returning the first matching edge for given path.
+   * Note: this function does not check the existence of source & target. This
+   * must be performed by the caller.
+   *
+   * @param  {Graph}  graph  - Target graph.
+   * @param  {any}    source - Source node.
+   * @param  {any}    target - Target node.
+   * @param  {string} type   - Type of the edge (mixed, directed or undirected).
+   * @return {string|null}
+   */
+
+  function getMatchingEdge(graph, source, target, type) {
+    var sourceData = graph._nodes.get(source);
+
+    var edge = null;
+    if (!sourceData) return edge;
+
+    if (type === 'mixed') {
+      edge = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target];
+    } else if (type === 'directed') {
+      edge = sourceData.out && sourceData.out[target];
+    } else {
+      edge = sourceData.undirected && sourceData.undirected[target];
+    }
+
+    return edge;
+  }
+  /**
+   * Checks whether the given value is a Graph implementation instance.
+   *
+   * @param  {mixed}   value - Target value.
+   * @return {boolean}
+   */
+
+  function isGraph(value) {
+    return value !== null && _typeof(value) === 'object' && typeof value.addUndirectedEdgeWithKey === 'function' && typeof value.dropNode === 'function';
+  }
+  /**
+   * Checks whether the given value is a plain object.
+   *
+   * @param  {mixed}   value - Target value.
+   * @return {boolean}
+   */
+
+  function isPlainObject(value) {
+    return _typeof(value) === 'object' && value !== null && value.constructor === Object;
+  }
+  /**
+   * Checks whether the given object is empty.
+   *
+   * @param  {object}  o - Target Object.
+   * @return {boolean}
+   */
+
+  function isEmpty(o) {
+    var k;
+
+    for (k in o) {
+      return false;
+    }
+
+    return true;
+  }
+  /**
+   * Creates a "private" property for the given member name by concealing it
+   * using the `enumerable` option.
+   *
+   * @param {object} target - Target object.
+   * @param {string} name   - Member name.
+   */
+
+  function privateProperty(target, name, value) {
+    Object.defineProperty(target, name, {
+      enumerable: false,
+      configurable: false,
+      writable: true,
+      value: value
+    });
+  }
+  /**
+   * Creates a read-only property for the given member name & the given getter.
+   *
+   * @param {object}   target - Target object.
+   * @param {string}   name   - Member name.
+   * @param {mixed}    value  - The attached getter or fixed value.
+   */
+
+  function readOnlyProperty(target, name, value) {
+    var descriptor = {
+      enumerable: true,
+      configurable: true
+    };
+
+    if (typeof value === 'function') {
+      descriptor.get = value;
+    } else {
+      descriptor.value = value;
+      descriptor.writable = false;
+    }
+
+    Object.defineProperty(target, name, descriptor);
+  }
+  /**
+   * Returns whether the given object constitute valid hints.
+   *
+   * @param {object} hints - Target object.
+   */
+
+  function validateHints(hints) {
+    if (!isPlainObject(hints)) return false;
+    if (hints.attributes && !Array.isArray(hints.attributes)) return false;
+    return true;
+  }
+  /**
+   * Creates a function generating incremental ids for edges.
+   *
+   * @return {function}
+   */
+
+  function incrementalIdStartingFromRandomByte() {
+    var i = Math.floor(Math.random() * 256) & 0xff;
+    return function () {
+      return i++;
+    };
+  }
+
+  var events = {exports: {}};
+
+  var R = typeof Reflect === 'object' ? Reflect : null;
+  var ReflectApply = R && typeof R.apply === 'function' ? R.apply : function ReflectApply(target, receiver, args) {
+    return Function.prototype.apply.call(target, receiver, args);
+  };
+  var ReflectOwnKeys;
+
+  if (R && typeof R.ownKeys === 'function') {
+    ReflectOwnKeys = R.ownKeys;
+  } else if (Object.getOwnPropertySymbols) {
+    ReflectOwnKeys = function ReflectOwnKeys(target) {
+      return Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));
+    };
+  } else {
+    ReflectOwnKeys = function ReflectOwnKeys(target) {
+      return Object.getOwnPropertyNames(target);
+    };
+  }
+
+  function ProcessEmitWarning(warning) {
+    if (console && console.warn) console.warn(warning);
+  }
+
+  var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
+    return value !== value;
+  };
+
+  function EventEmitter() {
+    EventEmitter.init.call(this);
+  }
+
+  events.exports = EventEmitter;
+  events.exports.once = once; // Backwards-compat with node 0.10.x
+
+  EventEmitter.EventEmitter = EventEmitter;
+  EventEmitter.prototype._events = undefined;
+  EventEmitter.prototype._eventsCount = 0;
+  EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are
+  // added to it. This is a useful default which helps finding memory leaks.
+
+  var defaultMaxListeners = 10;
+
+  function checkListener(listener) {
+    if (typeof listener !== 'function') {
+      throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
+    }
+  }
+
+  Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
+    enumerable: true,
+    get: function () {
+      return defaultMaxListeners;
+    },
+    set: function (arg) {
+      if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
+        throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
+      }
+
+      defaultMaxListeners = arg;
+    }
+  });
+
+  EventEmitter.init = function () {
+    if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
+      this._events = Object.create(null);
+      this._eventsCount = 0;
+    }
+
+    this._maxListeners = this._maxListeners || undefined;
+  }; // Obviously not all Emitters should be limited to 10. This function allows
+  // that to be increased. Set to zero for unlimited.
+
+
+  EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+    if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+      throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
+    }
+
+    this._maxListeners = n;
+    return this;
+  };
+
+  function _getMaxListeners(that) {
+    if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners;
+    return that._maxListeners;
+  }
+
+  EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
+    return _getMaxListeners(this);
+  };
+
+  EventEmitter.prototype.emit = function emit(type) {
+    var args = [];
+
+    for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
+
+    var doError = type === 'error';
+    var events = this._events;
+    if (events !== undefined) doError = doError && events.error === undefined;else if (!doError) return false; // If there is no 'error' event listener then throw.
+
+    if (doError) {
+      var er;
+      if (args.length > 0) er = args[0];
+
+      if (er instanceof Error) {
+        // Note: The comments on the `throw` lines are intentional, they show
+        // up in Node's output if this results in an unhandled exception.
+        throw er; // Unhandled 'error' event
+      } // At least give some kind of context to the user
+
+
+      var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
+      err.context = er;
+      throw err; // Unhandled 'error' event
+    }
+
+    var handler = events[type];
+    if (handler === undefined) return false;
+
+    if (typeof handler === 'function') {
+      ReflectApply(handler, this, args);
+    } else {
+      var len = handler.length;
+      var listeners = arrayClone(handler, len);
+
+      for (var i = 0; i < len; ++i) ReflectApply(listeners[i], this, args);
+    }
+
+    return true;
+  };
+
+  function _addListener(target, type, listener, prepend) {
+    var m;
+    var events;
+    var existing;
+    checkListener(listener);
+    events = target._events;
+
+    if (events === undefined) {
+      events = target._events = Object.create(null);
+      target._eventsCount = 0;
+    } else {
+      // To avoid recursion in the case that type === "newListener"! Before
+      // adding it to the listeners, first emit "newListener".
+      if (events.newListener !== undefined) {
+        target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the
+        // this._events to be assigned to a new object
+
+        events = target._events;
+      }
+
+      existing = events[type];
+    }
+
+    if (existing === undefined) {
+      // Optimize the case of one listener. Don't need the extra array object.
+      existing = events[type] = listener;
+      ++target._eventsCount;
+    } else {
+      if (typeof existing === 'function') {
+        // Adding the second element, need to change to array.
+        existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append.
+      } else if (prepend) {
+        existing.unshift(listener);
+      } else {
+        existing.push(listener);
+      } // Check for listener leak
+
+
+      m = _getMaxListeners(target);
+
+      if (m > 0 && existing.length > m && !existing.warned) {
+        existing.warned = true; // No error code for this since it is a Warning
+        // eslint-disable-next-line no-restricted-syntax
+
+        var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + String(type) + ' listeners ' + 'added. Use emitter.setMaxListeners() to ' + 'increase limit');
+        w.name = 'MaxListenersExceededWarning';
+        w.emitter = target;
+        w.type = type;
+        w.count = existing.length;
+        ProcessEmitWarning(w);
+      }
+    }
+
+    return target;
+  }
+
+  EventEmitter.prototype.addListener = function addListener(type, listener) {
+    return _addListener(this, type, listener, false);
+  };
+
+  EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+  EventEmitter.prototype.prependListener = function prependListener(type, listener) {
+    return _addListener(this, type, listener, true);
+  };
+
+  function onceWrapper() {
+    if (!this.fired) {
+      this.target.removeListener(this.type, this.wrapFn);
+      this.fired = true;
+      if (arguments.length === 0) return this.listener.call(this.target);
+      return this.listener.apply(this.target, arguments);
+    }
+  }
+
+  function _onceWrap(target, type, listener) {
+    var state = {
+      fired: false,
+      wrapFn: undefined,
+      target: target,
+      type: type,
+      listener: listener
+    };
+    var wrapped = onceWrapper.bind(state);
+    wrapped.listener = listener;
+    state.wrapFn = wrapped;
+    return wrapped;
+  }
+
+  EventEmitter.prototype.once = function once(type, listener) {
+    checkListener(listener);
+    this.on(type, _onceWrap(this, type, listener));
+    return this;
+  };
+
+  EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) {
+    checkListener(listener);
+    this.prependListener(type, _onceWrap(this, type, listener));
+    return this;
+  }; // Emits a 'removeListener' event if and only if the listener was removed.
+
+
+  EventEmitter.prototype.removeListener = function removeListener(type, listener) {
+    var list, events, position, i, originalListener;
+    checkListener(listener);
+    events = this._events;
+    if (events === undefined) return this;
+    list = events[type];
+    if (list === undefined) return this;
+
+    if (list === listener || list.listener === listener) {
+      if (--this._eventsCount === 0) this._events = Object.create(null);else {
+        delete events[type];
+        if (events.removeListener) this.emit('removeListener', type, list.listener || listener);
+      }
+    } else if (typeof list !== 'function') {
+      position = -1;
+
+      for (i = list.length - 1; i >= 0; i--) {
+        if (list[i] === listener || list[i].listener === listener) {
+          originalListener = list[i].listener;
+          position = i;
+          break;
+        }
+      }
+
+      if (position < 0) return this;
+      if (position === 0) list.shift();else {
+        spliceOne(list, position);
+      }
+      if (list.length === 1) events[type] = list[0];
+      if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener);
+    }
+
+    return this;
+  };
+
+  EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+  EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
+    var listeners, events, i;
+    events = this._events;
+    if (events === undefined) return this; // not listening for removeListener, no need to emit
+
+    if (events.removeListener === undefined) {
+      if (arguments.length === 0) {
+        this._events = Object.create(null);
+        this._eventsCount = 0;
+      } else if (events[type] !== undefined) {
+        if (--this._eventsCount === 0) this._events = Object.create(null);else delete events[type];
+      }
+
+      return this;
+    } // emit removeListener for all listeners on all events
+
+
+    if (arguments.length === 0) {
+      var keys = Object.keys(events);
+      var key;
+
+      for (i = 0; i < keys.length; ++i) {
+        key = keys[i];
+        if (key === 'removeListener') continue;
+        this.removeAllListeners(key);
+      }
+
+      this.removeAllListeners('removeListener');
+      this._events = Object.create(null);
+      this._eventsCount = 0;
+      return this;
+    }
+
+    listeners = events[type];
+
+    if (typeof listeners === 'function') {
+      this.removeListener(type, listeners);
+    } else if (listeners !== undefined) {
+      // LIFO order
+      for (i = listeners.length - 1; i >= 0; i--) {
+        this.removeListener(type, listeners[i]);
+      }
+    }
+
+    return this;
+  };
+
+  function _listeners(target, type, unwrap) {
+    var events = target._events;
+    if (events === undefined) return [];
+    var evlistener = events[type];
+    if (evlistener === undefined) return [];
+    if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener];
+    return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
+  }
+
+  EventEmitter.prototype.listeners = function listeners(type) {
+    return _listeners(this, type, true);
+  };
+
+  EventEmitter.prototype.rawListeners = function rawListeners(type) {
+    return _listeners(this, type, false);
+  };
+
+  EventEmitter.listenerCount = function (emitter, type) {
+    if (typeof emitter.listenerCount === 'function') {
+      return emitter.listenerCount(type);
+    } else {
+      return listenerCount.call(emitter, type);
+    }
+  };
+
+  EventEmitter.prototype.listenerCount = listenerCount;
+
+  function listenerCount(type) {
+    var events = this._events;
+
+    if (events !== undefined) {
+      var evlistener = events[type];
+
+      if (typeof evlistener === 'function') {
+        return 1;
+      } else if (evlistener !== undefined) {
+        return evlistener.length;
+      }
+    }
+
+    return 0;
+  }
+
+  EventEmitter.prototype.eventNames = function eventNames() {
+    return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
+  };
+
+  function arrayClone(arr, n) {
+    var copy = new Array(n);
+
+    for (var i = 0; i < n; ++i) copy[i] = arr[i];
+
+    return copy;
+  }
+
+  function spliceOne(list, index) {
+    for (; index + 1 < list.length; index++) list[index] = list[index + 1];
+
+    list.pop();
+  }
+
+  function unwrapListeners(arr) {
+    var ret = new Array(arr.length);
+
+    for (var i = 0; i < ret.length; ++i) {
+      ret[i] = arr[i].listener || arr[i];
+    }
+
+    return ret;
+  }
+
+  function once(emitter, name) {
+    return new Promise(function (resolve, reject) {
+      function errorListener(err) {
+        emitter.removeListener(name, resolver);
+        reject(err);
+      }
+
+      function resolver() {
+        if (typeof emitter.removeListener === 'function') {
+          emitter.removeListener('error', errorListener);
+        }
+
+        resolve([].slice.call(arguments));
+      }
+      eventTargetAgnosticAddListener(emitter, name, resolver, {
+        once: true
+      });
+
+      if (name !== 'error') {
+        addErrorHandlerIfEventEmitter(emitter, errorListener, {
+          once: true
+        });
+      }
+    });
+  }
+
+  function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
+    if (typeof emitter.on === 'function') {
+      eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
+    }
+  }
+
+  function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
+    if (typeof emitter.on === 'function') {
+      if (flags.once) {
+        emitter.once(name, listener);
+      } else {
+        emitter.on(name, listener);
+      }
+    } else if (typeof emitter.addEventListener === 'function') {
+      // EventTarget does not have `error` event semantics like Node
+      // EventEmitters, we do not listen for `error` events here.
+      emitter.addEventListener(name, function wrapListener(arg) {
+        // IE does not have builtin `{ once: true }` support so we
+        // have to do it manually.
+        if (flags.once) {
+          emitter.removeEventListener(name, wrapListener);
+        }
+
+        listener(arg);
+      });
+    } else {
+      throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
+    }
+  }
+
+  /**
+   * Obliterator Iterator Class
+   * ===========================
+   *
+   * Simple class representing the library's iterators.
+   */
+  /**
+   * Iterator class.
+   *
+   * @constructor
+   * @param {function} next - Next function.
+   */
+
+  function Iterator$2(next) {
+    if (typeof next !== 'function') throw new Error('obliterator/iterator: expecting a function!');
+    this.next = next;
+  }
+  /**
+   * If symbols are supported, we add `next` to `Symbol.iterator`.
+   */
+
+
+  if (typeof Symbol !== 'undefined') Iterator$2.prototype[Symbol.iterator] = function () {
+    return this;
+  };
+  /**
+   * Returning an iterator of the given values.
+   *
+   * @param  {any...} values - Values.
+   * @return {Iterator}
+   */
+
+  Iterator$2.of = function () {
+    var args = arguments,
+        l = args.length,
+        i = 0;
+    return new Iterator$2(function () {
+      if (i >= l) return {
+        done: true
+      };
+      return {
+        done: false,
+        value: args[i++]
+      };
+    });
+  };
+  /**
+   * Returning an empty iterator.
+   *
+   * @return {Iterator}
+   */
+
+
+  Iterator$2.empty = function () {
+    var iterator = new Iterator$2(function () {
+      return {
+        done: true
+      };
+    });
+    return iterator;
+  };
+  /**
+   * Returning an iterator over the given indexed sequence.
+   *
+   * @param  {string|Array} sequence - Target sequence.
+   * @return {Iterator}
+   */
+
+
+  Iterator$2.fromSequence = function (sequence) {
+    var i = 0,
+        l = sequence.length;
+    return new Iterator$2(function () {
+      if (i >= l) return {
+        done: true
+      };
+      return {
+        done: false,
+        value: sequence[i++]
+      };
+    });
+  };
+  /**
+   * Returning whether the given value is an iterator.
+   *
+   * @param  {any} value - Value.
+   * @return {boolean}
+   */
+
+
+  Iterator$2.is = function (value) {
+    if (value instanceof Iterator$2) return true;
+    return typeof value === 'object' && value !== null && typeof value.next === 'function';
+  };
+  /**
+   * Exporting.
+   */
+
+
+  var iterator = Iterator$2;
+
+  var support$1 = {};
+
+  support$1.ARRAY_BUFFER_SUPPORT = typeof ArrayBuffer !== 'undefined';
+  support$1.SYMBOL_SUPPORT = typeof Symbol !== 'undefined';
+
+  /**
+   * Obliterator Iter Function
+   * ==========================
+   *
+   * Function coercing values to an iterator. It can be quite useful when needing
+   * to handle iterables and iterators the same way.
+   */
+  var Iterator$1 = iterator;
+  var support = support$1;
+  var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+  var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+  function iterOrNull(target) {
+    // Indexed sequence
+    if (typeof target === 'string' || Array.isArray(target) || ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(target)) return Iterator$1.fromSequence(target); // Invalid value
+
+    if (typeof target !== 'object' || target === null) return null; // Iterable
+
+    if (SYMBOL_SUPPORT && typeof target[Symbol.iterator] === 'function') return target[Symbol.iterator](); // Iterator duck-typing
+
+    if (typeof target.next === 'function') return target; // Invalid object
+
+    return null;
+  }
+
+  var iter$2 = function iter(target) {
+    var iterator = iterOrNull(target);
+    if (!iterator) throw new Error('obliterator: target is not iterable nor a valid iterator.');
+    return iterator;
+  };
+
+  /* eslint no-constant-condition: 0 */
+  /**
+   * Obliterator Take Function
+   * ==========================
+   *
+   * Function taking n or every value of the given iterator and returns them
+   * into an array.
+   */
+
+  var iter$1 = iter$2;
+  /**
+   * Take.
+   *
+   * @param  {Iterable} iterable - Target iterable.
+   * @param  {number}   [n]      - Optional number of items to take.
+   * @return {array}
+   */
+
+  var take = function take(iterable, n) {
+    var l = arguments.length > 1 ? n : Infinity,
+        array = l !== Infinity ? new Array(l) : [],
+        step,
+        i = 0;
+    var iterator = iter$1(iterable);
+
+    while (true) {
+      if (i === l) return array;
+      step = iterator.next();
+
+      if (step.done) {
+        if (i !== n) array.length = i;
+        return array;
+      }
+
+      array[i++] = step.value;
+    }
+  };
+
+  /**
+   * Graphology Custom Errors
+   * =========================
+   *
+   * Defining custom errors for ease of use & easy unit tests across
+   * implementations (normalized typology rather than relying on error
+   * messages to check whether the correct error was found).
+   */
+  var GraphError = /*#__PURE__*/function (_Error) {
+    _inheritsLoose(GraphError, _Error);
+
+    function GraphError(message) {
+      var _this;
+
+      _this = _Error.call(this) || this;
+      _this.name = 'GraphError';
+      _this.message = message;
+      return _this;
+    }
+
+    return GraphError;
+  }( /*#__PURE__*/_wrapNativeSuper(Error));
+  var InvalidArgumentsGraphError = /*#__PURE__*/function (_GraphError) {
+    _inheritsLoose(InvalidArgumentsGraphError, _GraphError);
+
+    function InvalidArgumentsGraphError(message) {
+      var _this2;
+
+      _this2 = _GraphError.call(this, message) || this;
+      _this2.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this2), InvalidArgumentsGraphError.prototype.constructor);
+      return _this2;
+    }
+
+    return InvalidArgumentsGraphError;
+  }(GraphError);
+  var NotFoundGraphError = /*#__PURE__*/function (_GraphError2) {
+    _inheritsLoose(NotFoundGraphError, _GraphError2);
+
+    function NotFoundGraphError(message) {
+      var _this3;
+
+      _this3 = _GraphError2.call(this, message) || this;
+      _this3.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this3), NotFoundGraphError.prototype.constructor);
+      return _this3;
+    }
+
+    return NotFoundGraphError;
+  }(GraphError);
+  var UsageGraphError = /*#__PURE__*/function (_GraphError3) {
+    _inheritsLoose(UsageGraphError, _GraphError3);
+
+    function UsageGraphError(message) {
+      var _this4;
+
+      _this4 = _GraphError3.call(this, message) || this;
+      _this4.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability
+
+      if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(_assertThisInitialized(_this4), UsageGraphError.prototype.constructor);
+      return _this4;
+    }
+
+    return UsageGraphError;
+  }(GraphError);
+
+  /**
+   * Graphology Internal Data Classes
+   * =================================
+   *
+   * Internal classes hopefully reduced to structs by engines & storing
+   * necessary information for nodes & edges.
+   *
+   * Note that those classes don't rely on the `class` keyword to avoid some
+   * cruft introduced by most of ES2015 transpilers.
+   */
+
+  /**
+   * MixedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+  function MixedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  MixedNodeData.prototype.clear = function () {
+    // Degrees
+    this.inDegree = 0;
+    this.outDegree = 0;
+    this.undirectedDegree = 0; // Indices
+
+    this["in"] = {};
+    this.out = {};
+    this.undirected = {};
+  };
+  /**
+   * DirectedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+
+
+  function DirectedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  DirectedNodeData.prototype.clear = function () {
+    // Degrees
+    this.inDegree = 0;
+    this.outDegree = 0; // Indices
+
+    this["in"] = {};
+    this.out = {};
+  };
+  /**
+   * UndirectedNodeData class.
+   *
+   * @constructor
+   * @param {string} string     - The node's key.
+   * @param {object} attributes - Node's attributes.
+   */
+
+
+  function UndirectedNodeData(key, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.clear();
+  }
+
+  UndirectedNodeData.prototype.clear = function () {
+    // Degrees
+    this.undirectedDegree = 0; // Indices
+
+    this.undirected = {};
+  };
+  /**
+   * EdgeData class.
+   *
+   * @constructor
+   * @param {boolean} undirected   - Whether the edge is undirected.
+   * @param {string}  string       - The edge's key.
+   * @param {string}  source       - Source of the edge.
+   * @param {string}  target       - Target of the edge.
+   * @param {object}  attributes   - Edge's attributes.
+   */
+
+
+  function EdgeData(undirected, key, source, target, attributes) {
+    // Attributes
+    this.key = key;
+    this.attributes = attributes;
+    this.undirected = undirected; // Extremities
+
+    this.source = source;
+    this.target = target;
+  }
+
+  EdgeData.prototype.attach = function () {
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected';
+    var source = this.source.key;
+    var target = this.target.key; // Handling source
+
+    this.source[outKey][target] = this;
+    if (this.undirected && source === target) return; // Handling target
+
+    this.target[inKey][source] = this;
+  };
+
+  EdgeData.prototype.attachMulti = function () {
+    var outKey = 'out';
+    var inKey = 'in';
+    var source = this.source.key;
+    var target = this.target.key;
+    if (this.undirected) outKey = inKey = 'undirected'; // Handling source
+
+    var adj = this.source[outKey];
+    var head = adj[target];
+
+    if (typeof head === 'undefined') {
+      adj[target] = this; // Self-loop optimization
+
+      if (!(this.undirected && source === target)) {
+        // Handling target
+        this.target[inKey][source] = this;
+      }
+
+      return;
+    } // Prepending to doubly-linked list
+
+
+    head.previous = this;
+    this.next = head; // Pointing to new head
+    // NOTE: use mutating swap later to avoid lookup?
+
+    adj[target] = this;
+    this.target[inKey][source] = this;
+  };
+
+  EdgeData.prototype.detach = function () {
+    var source = this.source.key;
+    var target = this.target.key;
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected';
+    delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+    delete this.target[inKey][source];
+  };
+
+  EdgeData.prototype.detachMulti = function () {
+    var source = this.source.key;
+    var target = this.target.key;
+    var outKey = 'out';
+    var inKey = 'in';
+    if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list
+
+    if (this.previous === undefined) {
+      // We are dealing with the head
+      // Should we delete the adjacency entry because it is now empty?
+      if (this.next === undefined) {
+        delete this.source[outKey][target]; // No-op delete in case of undirected self-loop
+
+        delete this.target[inKey][source];
+      } else {
+        // Detaching
+        this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method
+
+        this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop
+
+        this.target[inKey][source] = this.next;
+      }
+    } else {
+      // We are dealing with another list node
+      this.previous.next = this.next; // If not last
+
+      if (this.next !== undefined) {
+        this.next.previous = this.previous;
+      }
+    }
+  };
+
+  /**
+   * Graphology Node Attributes methods
+   * ===================================
+   */
+  var NODE = 0;
+  var SOURCE = 1;
+  var TARGET = 2;
+  var OPPOSITE = 3;
+
+  function findRelevantNodeData(graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2) {
+    var nodeData, edgeData, arg1, arg2;
+    nodeOrEdge = '' + nodeOrEdge;
+
+    if (mode === NODE) {
+      nodeData = graph._nodes.get(nodeOrEdge);
+      if (!nodeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" node in the graph."));
+      arg1 = nameOrEdge;
+      arg2 = add1;
+    } else if (mode === OPPOSITE) {
+      nameOrEdge = '' + nameOrEdge;
+      edgeData = graph._edges.get(nameOrEdge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nameOrEdge, "\" edge in the graph."));
+      var source = edgeData.source.key;
+      var target = edgeData.target.key;
+
+      if (nodeOrEdge === source) {
+        nodeData = edgeData.target;
+      } else if (nodeOrEdge === target) {
+        nodeData = edgeData.source;
+      } else {
+        throw new NotFoundGraphError("Graph.".concat(method, ": the \"").concat(nodeOrEdge, "\" node is not attached to the \"").concat(nameOrEdge, "\" edge (").concat(source, ", ").concat(target, ")."));
+      }
+
+      arg1 = add1;
+      arg2 = add2;
+    } else {
+      edgeData = graph._edges.get(nodeOrEdge);
+      if (!edgeData) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(nodeOrEdge, "\" edge in the graph."));
+
+      if (mode === SOURCE) {
+        nodeData = edgeData.source;
+      } else {
+        nodeData = edgeData.target;
+      }
+
+      arg1 = nameOrEdge;
+      arg2 = add1;
+    }
+
+    return [nodeData, arg1, arg2];
+  }
+
+  function attachNodeAttributeGetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData[0],
+          name = _findRelevantNodeData[1];
+
+      return data.attributes[name];
+    };
+  }
+
+  function attachNodeAttributesGetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge) {
+      var _findRelevantNodeData2 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge),
+          data = _findRelevantNodeData2[0];
+
+      return data.attributes;
+    };
+  }
+
+  function attachNodeAttributeChecker(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData3 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData3[0],
+          name = _findRelevantNodeData3[1];
+
+      return data.attributes.hasOwnProperty(name);
+    };
+  }
+
+  function attachNodeAttributeSetter(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+      var _findRelevantNodeData4 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+          data = _findRelevantNodeData4[0],
+          name = _findRelevantNodeData4[1],
+          value = _findRelevantNodeData4[2];
+
+      data.attributes[name] = value; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributeUpdater(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) {
+      var _findRelevantNodeData5 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1, add2),
+          data = _findRelevantNodeData5[0],
+          name = _findRelevantNodeData5[1],
+          updater = _findRelevantNodeData5[2];
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+      var attributes = data.attributes;
+      var value = updater(attributes[name]);
+      attributes[name] = value; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributeRemover(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData6 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData6[0],
+          name = _findRelevantNodeData6[1];
+
+      delete data.attributes[name]; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'remove',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesReplacer(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData7 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData7[0],
+          attributes = _findRelevantNodeData7[1];
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      data.attributes = attributes; // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'replace',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesMerger(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData8 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData8[0],
+          attributes = _findRelevantNodeData8[1];
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      assign(data.attributes, attributes); // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'merge',
+        attributes: data.attributes,
+        data: attributes
+      });
+      return this;
+    };
+  }
+
+  function attachNodeAttributesUpdater(Class, method, mode) {
+    Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) {
+      var _findRelevantNodeData9 = findRelevantNodeData(this, method, mode, nodeOrEdge, nameOrEdge, add1),
+          data = _findRelevantNodeData9[0],
+          updater = _findRelevantNodeData9[1];
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+      data.attributes = updater(data.attributes); // Emitting
+
+      this.emit('nodeAttributesUpdated', {
+        key: data.key,
+        type: 'update',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * List of methods to attach.
+   */
+
+
+  var NODE_ATTRIBUTES_METHODS = [{
+    name: function name(element) {
+      return "get".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeGetter
+  }, {
+    name: function name(element) {
+      return "get".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesGetter
+  }, {
+    name: function name(element) {
+      return "has".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeChecker
+  }, {
+    name: function name(element) {
+      return "set".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeSetter
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeUpdater
+  }, {
+    name: function name(element) {
+      return "remove".concat(element, "Attribute");
+    },
+    attacher: attachNodeAttributeRemover
+  }, {
+    name: function name(element) {
+      return "replace".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesReplacer
+  }, {
+    name: function name(element) {
+      return "merge".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesMerger
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attributes");
+    },
+    attacher: attachNodeAttributesUpdater
+  }];
+  /**
+   * Attach every attributes-related methods to a Graph class.
+   *
+   * @param {function} Graph - Target class.
+   */
+
+  function attachNodeAttributesMethods(Graph) {
+    NODE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+      var name = _ref.name,
+          attacher = _ref.attacher;
+      // For nodes
+      attacher(Graph, name('Node'), NODE); // For sources
+
+      attacher(Graph, name('Source'), SOURCE); // For targets
+
+      attacher(Graph, name('Target'), TARGET); // For opposites
+
+      attacher(Graph, name('Opposite'), OPPOSITE);
+    });
+  }
+
+  /**
+   * Graphology Edge Attributes methods
+   * ===================================
+   */
+  /**
+   * Attach an attribute getter method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+  function attachEdgeAttributeGetter(Class, method, type) {
+    /**
+     * Get the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {mixed}          - The attribute's value.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes[name];
+    };
+  }
+  /**
+   * Attach an attributes getter method onto the provided class.
+   *
+   * @param {function} Class       - Target class.
+   * @param {string}   method      - Method name.
+   * @param {string}   type        - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesGetter(Class, method, type) {
+    /**
+     * Retrieves all the target element's attributes.
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     *
+     * @return {object}          - The element's attributes.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 1) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + arguments[1];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes;
+    };
+  }
+  /**
+   * Attach an attribute checker method onto the provided class.
+   *
+   * @param {function} Class       - Target class.
+   * @param {string}   method      - Method name.
+   * @param {string}   type        - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeChecker(Class, method, type) {
+    /**
+     * Checks whether the desired attribute is set for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      return data.attributes.hasOwnProperty(name);
+    };
+  }
+  /**
+   * Attach an attribute setter method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeSetter(Class, method, type) {
+    /**
+     * Set the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     * @param  {mixed}  value   - New attribute value.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     * @param  {mixed}  value   - New attribute value.
+     *
+     * @return {Graph}          - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name, value) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 3) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        value = arguments[3];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      data.attributes[name] = value; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute updater method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeUpdater(Class, method, type) {
+    /**
+     * Update the desired attribute for the given element (node or edge) using
+     * the provided function.
+     *
+     * Arity 2:
+     * @param  {any}      element - Target element.
+     * @param  {string}   name    - Attribute's name.
+     * @param  {function} updater - Updater function.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}      source  - Source element.
+     * @param  {any}      target  - Target element.
+     * @param  {string}   name    - Attribute's name.
+     * @param  {function} updater - Updater function.
+     *
+     * @return {Graph}            - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name, updater) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 3) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        updater = arguments[3];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": updater should be a function."));
+      data.attributes[name] = updater(data.attributes[name]); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'set',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute remover method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributeRemover(Class, method, type) {
+    /**
+     * Remove the desired attribute for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element - Target element.
+     * @param  {string} name    - Attribute's name.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source - Source element.
+     * @param  {any}     target - Target element.
+     * @param  {string}  name   - Attribute's name.
+     *
+     * @return {Graph}          - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, name) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element;
+        var target = '' + name;
+        name = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      delete data.attributes[name]; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'remove',
+        attributes: data.attributes,
+        name: name
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute replacer method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesReplacer(Class, method, type) {
+    /**
+     * Replace the attributes for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element    - Target element.
+     * @param  {object} attributes - New attributes.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source     - Source element.
+     * @param  {any}     target     - Target element.
+     * @param  {object}  attributes - New attributes.
+     *
+     * @return {Graph}              - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, attributes) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + attributes;
+        attributes = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      data.attributes = attributes; // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'replace',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute merger method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesMerger(Class, method, type) {
+    /**
+     * Merge the attributes for the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}    element    - Target element.
+     * @param  {object} attributes - Attributes to merge.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}     source     - Source element.
+     * @param  {any}     target     - Target element.
+     * @param  {object}  attributes - Attributes to merge.
+     *
+     * @return {Graph}              - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, attributes) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + attributes;
+        attributes = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided attributes are not a plain object."));
+      assign(data.attributes, attributes); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'merge',
+        attributes: data.attributes,
+        data: attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * Attach an attribute updater method onto the provided class.
+   *
+   * @param {function} Class         - Target class.
+   * @param {string}   method        - Method name.
+   * @param {string}   type          - Type of the edge to find.
+   */
+
+
+  function attachEdgeAttributesUpdater(Class, method, type) {
+    /**
+     * Update the attributes of the given element (node or edge).
+     *
+     * Arity 2:
+     * @param  {any}      element - Target element.
+     * @param  {function} updater - Updater function.
+     *
+     * Arity 3 (only for edges):
+     * @param  {any}      source  - Source element.
+     * @param  {any}      target  - Target element.
+     * @param  {function} updater - Updater function.
+     *
+     * @return {Graph}            - Returns itself for chaining.
+     *
+     * @throws {Error} - Will throw if too many arguments are provided.
+     * @throws {Error} - Will throw if any of the elements is not found.
+     */
+    Class.prototype[method] = function (element, updater) {
+      var data;
+      if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError("Graph.".concat(method, ": cannot find this type of edges in your ").concat(this.type, " graph."));
+
+      if (arguments.length > 2) {
+        if (this.multi) throw new UsageGraphError("Graph.".concat(method, ": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));
+        var source = '' + element,
+            target = '' + updater;
+        updater = arguments[2];
+        data = getMatchingEdge(this, source, target, type);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find an edge for the given path (\"").concat(source, "\" - \"").concat(target, "\")."));
+      } else {
+        if (type !== 'mixed') throw new UsageGraphError("Graph.".concat(method, ": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));
+        element = '' + element;
+        data = this._edges.get(element);
+        if (!data) throw new NotFoundGraphError("Graph.".concat(method, ": could not find the \"").concat(element, "\" edge in the graph."));
+      }
+
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(method, ": provided updater is not a function."));
+      data.attributes = updater(data.attributes); // Emitting
+
+      this.emit('edgeAttributesUpdated', {
+        key: data.key,
+        type: 'update',
+        attributes: data.attributes
+      });
+      return this;
+    };
+  }
+  /**
+   * List of methods to attach.
+   */
+
+
+  var EDGE_ATTRIBUTES_METHODS = [{
+    name: function name(element) {
+      return "get".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeGetter
+  }, {
+    name: function name(element) {
+      return "get".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesGetter
+  }, {
+    name: function name(element) {
+      return "has".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeChecker
+  }, {
+    name: function name(element) {
+      return "set".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeSetter
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeUpdater
+  }, {
+    name: function name(element) {
+      return "remove".concat(element, "Attribute");
+    },
+    attacher: attachEdgeAttributeRemover
+  }, {
+    name: function name(element) {
+      return "replace".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesReplacer
+  }, {
+    name: function name(element) {
+      return "merge".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesMerger
+  }, {
+    name: function name(element) {
+      return "update".concat(element, "Attributes");
+    },
+    attacher: attachEdgeAttributesUpdater
+  }];
+  /**
+   * Attach every attributes-related methods to a Graph class.
+   *
+   * @param {function} Graph - Target class.
+   */
+
+  function attachEdgeAttributesMethods(Graph) {
+    EDGE_ATTRIBUTES_METHODS.forEach(function (_ref) {
+      var name = _ref.name,
+          attacher = _ref.attacher;
+      // For edges
+      attacher(Graph, name('Edge'), 'mixed'); // For directed edges
+
+      attacher(Graph, name('DirectedEdge'), 'directed'); // For undirected edges
+
+      attacher(Graph, name('UndirectedEdge'), 'undirected');
+    });
+  }
+
+  /**
+   * Obliterator Chain Function
+   * ===========================
+   *
+   * Variadic function combining the given iterables.
+   */
+  var Iterator = iterator;
+  var iter = iter$2;
+  /**
+   * Chain.
+   *
+   * @param  {...Iterator} iterables - Target iterables.
+   * @return {Iterator}
+   */
+
+  var chain = function chain() {
+    var iterables = arguments;
+    var current = null;
+    var i = -1;
+    /* eslint-disable no-constant-condition */
+
+    return new Iterator(function next() {
+      var step = null;
+
+      do {
+        if (current === null) {
+          i++;
+          if (i >= iterables.length) return {
+            done: true
+          };
+          current = iter(iterables[i]);
+        }
+
+        step = current.next();
+
+        if (step.done === true) {
+          current = null;
+          continue;
+        }
+
+        break;
+      } while (true);
+
+      return step;
+    });
+  };
+
+  /**
+   * Graphology Edge Iteration
+   * ==========================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over a
+   * graph's edges.
+   */
+  /**
+   * Definitions.
+   */
+
+  var EDGES_ITERATION = [{
+    name: 'edges',
+    type: 'mixed'
+  }, {
+    name: 'inEdges',
+    type: 'directed',
+    direction: 'in'
+  }, {
+    name: 'outEdges',
+    type: 'directed',
+    direction: 'out'
+  }, {
+    name: 'inboundEdges',
+    type: 'mixed',
+    direction: 'in'
+  }, {
+    name: 'outboundEdges',
+    type: 'mixed',
+    direction: 'out'
+  }, {
+    name: 'directedEdges',
+    type: 'directed'
+  }, {
+    name: 'undirectedEdges',
+    type: 'undirected'
+  }];
+  /**
+   * Function iterating over edges from the given object to match one of them.
+   *
+   * @param {object}   object   - Target object.
+   * @param {function} callback - Function to call.
+   */
+
+  function forEachSimple(breakable, object, callback, avoid) {
+    var shouldBreak = false;
+
+    for (var k in object) {
+      if (k === avoid) continue;
+      var edgeData = object[k];
+      shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+    }
+
+    return;
+  }
+
+  function forEachMulti(breakable, object, callback, avoid) {
+    var edgeData, source, target;
+    var shouldBreak = false;
+
+    for (var k in object) {
+      if (k === avoid) continue;
+      edgeData = object[k];
+
+      do {
+        source = edgeData.source;
+        target = edgeData.target;
+        shouldBreak = callback(edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected);
+        if (breakable && shouldBreak) return edgeData.key;
+        edgeData = edgeData.next;
+      } while (edgeData !== undefined);
+    }
+
+    return;
+  }
+  /**
+   * Function returning an iterator over edges from the given object.
+   *
+   * @param  {object}   object - Target object.
+   * @return {Iterator}
+   */
+
+
+  function createIterator(object, avoid) {
+    var keys = Object.keys(object);
+    var l = keys.length;
+    var edgeData;
+    var i = 0;
+    return new iterator(function next() {
+      do {
+        if (!edgeData) {
+          if (i >= l) return {
+            done: true
+          };
+          var k = keys[i++];
+
+          if (k === avoid) {
+            edgeData = undefined;
+            continue;
+          }
+
+          edgeData = object[k];
+        } else {
+          edgeData = edgeData.next;
+        }
+      } while (!edgeData);
+
+      return {
+        done: false,
+        value: {
+          edge: edgeData.key,
+          attributes: edgeData.attributes,
+          source: edgeData.source.key,
+          target: edgeData.target.key,
+          sourceAttributes: edgeData.source.attributes,
+          targetAttributes: edgeData.target.attributes,
+          undirected: edgeData.undirected
+        }
+      };
+    });
+  }
+  /**
+   * Function iterating over the egdes from the object at given key to match
+   * one of them.
+   *
+   * @param {object}   object   - Target object.
+   * @param {mixed}    k        - Neighbor key.
+   * @param {function} callback - Callback to use.
+   */
+
+
+  function forEachForKeySimple(breakable, object, k, callback) {
+    var edgeData = object[k];
+    if (!edgeData) return;
+    var sourceData = edgeData.source;
+    var targetData = edgeData.target;
+    if (callback(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected) && breakable) return edgeData.key;
+  }
+
+  function forEachForKeyMulti(breakable, object, k, callback) {
+    var edgeData = object[k];
+    if (!edgeData) return;
+    var shouldBreak = false;
+
+    do {
+      shouldBreak = callback(edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected);
+      if (breakable && shouldBreak) return edgeData.key;
+      edgeData = edgeData.next;
+    } while (edgeData !== undefined);
+
+    return;
+  }
+  /**
+   * Function returning an iterator over the egdes from the object at given key.
+   *
+   * @param  {object}   object   - Target object.
+   * @param  {mixed}    k        - Neighbor key.
+   * @return {Iterator}
+   */
+
+
+  function createIteratorForKey(object, k) {
+    var edgeData = object[k];
+
+    if (edgeData.next !== undefined) {
+      return new iterator(function () {
+        if (!edgeData) return {
+          done: true
+        };
+        var value = {
+          edge: edgeData.key,
+          attributes: edgeData.attributes,
+          source: edgeData.source.key,
+          target: edgeData.target.key,
+          sourceAttributes: edgeData.source.attributes,
+          targetAttributes: edgeData.target.attributes,
+          undirected: edgeData.undirected
+        };
+        edgeData = edgeData.next;
+        return {
+          done: false,
+          value: value
+        };
+      });
+    }
+
+    return iterator.of({
+      edge: edgeData.key,
+      attributes: edgeData.attributes,
+      source: edgeData.source.key,
+      target: edgeData.target.key,
+      sourceAttributes: edgeData.source.attributes,
+      targetAttributes: edgeData.target.attributes,
+      undirected: edgeData.undirected
+    });
+  }
+  /**
+   * Function creating an array of edges for the given type.
+   *
+   * @param  {Graph}   graph - Target Graph instance.
+   * @param  {string}  type  - Type of edges to retrieve.
+   * @return {array}         - Array of edges.
+   */
+
+
+  function createEdgeArray(graph, type) {
+    if (graph.size === 0) return [];
+
+    if (type === 'mixed' || type === graph.type) {
+      if (typeof Array.from === 'function') return Array.from(graph._edges.keys());
+      return take(graph._edges.keys(), graph._edges.size);
+    }
+
+    var size = type === 'undirected' ? graph.undirectedSize : graph.directedSize;
+    var list = new Array(size),
+        mask = type === 'undirected';
+
+    var iterator = graph._edges.values();
+
+    var i = 0;
+    var step, data;
+
+    while (step = iterator.next(), step.done !== true) {
+      data = step.value;
+      if (data.undirected === mask) list[i++] = data.key;
+    }
+
+    return list;
+  }
+  /**
+   * Function iterating over a graph's edges using a callback to match one of
+   * them.
+   *
+   * @param  {Graph}    graph    - Target Graph instance.
+   * @param  {string}   type     - Type of edges to retrieve.
+   * @param  {function} callback - Function to call.
+   */
+
+
+  function forEachEdge(breakable, graph, type, callback) {
+    if (graph.size === 0) return;
+    var shouldFilter = type !== 'mixed' && type !== graph.type;
+    var mask = type === 'undirected';
+    var step, data;
+    var shouldBreak = false;
+
+    var iterator = graph._edges.values();
+
+    while (step = iterator.next(), step.done !== true) {
+      data = step.value;
+      if (shouldFilter && data.undirected !== mask) continue;
+      var _data = data,
+          key = _data.key,
+          attributes = _data.attributes,
+          source = _data.source,
+          target = _data.target;
+      shouldBreak = callback(key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected);
+      if (breakable && shouldBreak) return key;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an iterator of edges for the given type.
+   *
+   * @param  {Graph}    graph - Target Graph instance.
+   * @param  {string}   type  - Type of edges to retrieve.
+   * @return {Iterator}
+   */
+
+
+  function createEdgeIterator(graph, type) {
+    if (graph.size === 0) return iterator.empty();
+    var shouldFilter = type !== 'mixed' && type !== graph.type;
+    var mask = type === 'undirected';
+
+    var iterator$1 = graph._edges.values();
+
+    return new iterator(function next() {
+      var step, data; // eslint-disable-next-line no-constant-condition
+
+      while (true) {
+        step = iterator$1.next();
+        if (step.done) return step;
+        data = step.value;
+        if (shouldFilter && data.undirected !== mask) continue;
+        break;
+      }
+
+      var value = {
+        edge: data.key,
+        attributes: data.attributes,
+        source: data.source.key,
+        target: data.target.key,
+        sourceAttributes: data.source.attributes,
+        targetAttributes: data.target.attributes,
+        undirected: data.undirected
+      };
+      return {
+        value: value,
+        done: false
+      };
+    });
+  }
+  /**
+   * Function iterating over a node's edges using a callback to match one of them.
+   *
+   * @param  {boolean}  multi     - Whether the graph is multi or not.
+   * @param  {string}   type      - Type of edges to retrieve.
+   * @param  {string}   direction - In or out?
+   * @param  {any}      nodeData  - Target node's data.
+   * @param  {function} callback  - Function to call.
+   */
+
+
+  function forEachEdgeForNode(breakable, multi, type, direction, nodeData, callback) {
+    var fn = multi ? forEachMulti : forEachSimple;
+    var found;
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        found = fn(breakable, nodeData["in"], callback);
+        if (breakable && found) return found;
+      }
+
+      if (direction !== 'in') {
+        found = fn(breakable, nodeData.out, callback, !direction ? nodeData.key : undefined);
+        if (breakable && found) return found;
+      }
+    }
+
+    if (type !== 'directed') {
+      found = fn(breakable, nodeData.undirected, callback);
+      if (breakable && found) return found;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of edges for the given type & the given node.
+   *
+   * @param  {boolean} multi     - Whether the graph is multi or not.
+   * @param  {string}  type      - Type of edges to retrieve.
+   * @param  {string}  direction - In or out?
+   * @param  {any}     nodeData  - Target node's data.
+   * @return {array}             - Array of edges.
+   */
+
+
+  function createEdgeArrayForNode(multi, type, direction, nodeData) {
+    var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+    forEachEdgeForNode(false, multi, type, direction, nodeData, function (key) {
+      edges.push(key);
+    });
+    return edges;
+  }
+  /**
+   * Function iterating over a node's edges using a callback.
+   *
+   * @param  {string}   type      - Type of edges to retrieve.
+   * @param  {string}   direction - In or out?
+   * @param  {any}      nodeData  - Target node's data.
+   * @return {Iterator}
+   */
+
+
+  function createEdgeIteratorForNode(type, direction, nodeData) {
+    var iterator$1 = iterator.empty();
+
+    if (type !== 'undirected') {
+      if (direction !== 'out' && typeof nodeData["in"] !== 'undefined') iterator$1 = chain(iterator$1, createIterator(nodeData["in"]));
+      if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator$1 = chain(iterator$1, createIterator(nodeData.out, !direction ? nodeData.key : undefined));
+    }
+
+    if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') {
+      iterator$1 = chain(iterator$1, createIterator(nodeData.undirected));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function iterating over edges for the given path using a callback to match
+   * one of them.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {boolean}  multi      - Whether the graph is multi.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {string}   target     - Target node.
+   * @param  {function} callback   - Function to call.
+   */
+
+
+  function forEachEdgeForPath(breakable, type, multi, direction, sourceData, target, callback) {
+    var fn = multi ? forEachForKeyMulti : forEachForKeySimple;
+    var found;
+
+    if (type !== 'undirected') {
+      if (typeof sourceData["in"] !== 'undefined' && direction !== 'out') {
+        found = fn(breakable, sourceData["in"], target, callback);
+        if (breakable && found) return found;
+      }
+
+      if (typeof sourceData.out !== 'undefined' && direction !== 'in' && (direction || sourceData.key !== target)) {
+        found = fn(breakable, sourceData.out, target, callback);
+        if (breakable && found) return found;
+      }
+    }
+
+    if (type !== 'directed') {
+      if (typeof sourceData.undirected !== 'undefined') {
+        found = fn(breakable, sourceData.undirected, target, callback);
+        if (breakable && found) return found;
+      }
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of edges for the given path.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {boolean}  multi      - Whether the graph is multi.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {any}      target     - Target node.
+   * @return {array}               - Array of edges.
+   */
+
+
+  function createEdgeArrayForPath(type, multi, direction, sourceData, target) {
+    var edges = []; // TODO: possibility to know size beforehand or factorize with map
+
+    forEachEdgeForPath(false, type, multi, direction, sourceData, target, function (key) {
+      edges.push(key);
+    });
+    return edges;
+  }
+  /**
+   * Function returning an iterator over edges for the given path.
+   *
+   * @param  {string}   type       - Type of edges to retrieve.
+   * @param  {string}   direction  - In or out?
+   * @param  {NodeData} sourceData - Source node's data.
+   * @param  {string}   target     - Target node.
+   * @param  {function} callback   - Function to call.
+   */
+
+
+  function createEdgeIteratorForPath(type, direction, sourceData, target) {
+    var iterator$1 = iterator.empty();
+
+    if (type !== 'undirected') {
+      if (typeof sourceData["in"] !== 'undefined' && direction !== 'out' && target in sourceData["in"]) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData["in"], target));
+      if (typeof sourceData.out !== 'undefined' && direction !== 'in' && target in sourceData.out && (direction || sourceData.key !== target)) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData.out, target));
+    }
+
+    if (type !== 'directed') {
+      if (typeof sourceData.undirected !== 'undefined' && target in sourceData.undirected) iterator$1 = chain(iterator$1, createIteratorForKey(sourceData.undirected, target));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function attaching an edge array creator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachEdgeArrayCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    /**
+     * Function returning an array of certain edges.
+     *
+     * Arity 0: Return all the relevant edges.
+     *
+     * Arity 1: Return all of a node's relevant edges.
+     * @param  {any}   node   - Target node.
+     *
+     * Arity 2: Return the relevant edges across the given path.
+     * @param  {any}   source - Source node.
+     * @param  {any}   target - Target node.
+     *
+     * @return {array|number} - The edges or the number of edges.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[name] = function (source, target) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+      if (!arguments.length) return createEdgeArray(this, type);
+
+      if (arguments.length === 1) {
+        source = '' + source;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+        return createEdgeArrayForNode(this.multi, type === 'mixed' ? this.type : type, direction, nodeData);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return createEdgeArrayForPath(type, this.multi, direction, sourceData, target);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+    };
+  }
+  /**
+   * Function attaching a edge callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachForEachEdge(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over the graph's relevant edges by applying the given
+     * callback.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[forEachName] = function (source, target, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+
+      if (arguments.length === 1) {
+        callback = source;
+        return forEachEdge(false, this, type, callback);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        callback = target;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+        // TODO: maybe attach the sub method to the instance dynamically?
+
+        return forEachEdgeForNode(false, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+      }
+
+      if (arguments.length === 3) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(forEachName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return forEachEdgeForPath(false, type, this.multi, direction, sourceData, target, callback);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(forEachName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+    };
+    /**
+     * Function mapping the graph's relevant edges by applying the given
+     * callback.
+     *
+     * Arity 1: Map all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Map all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Map the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[mapName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      var result; // We know the result length beforehand
+
+      if (args.length === 0) {
+        var length = 0;
+        if (type !== 'directed') length += this.undirectedSize;
+        if (type !== 'undirected') length += this.directedSize;
+        result = new Array(length);
+        var i = 0;
+        args.push(function (e, ea, s, t, sa, ta, u) {
+          result[i++] = callback(e, ea, s, t, sa, ta, u);
+        });
+      } // We don't know the result length beforehand
+      // TODO: we can in some instances of simple graphs, knowing degree
+      else {
+        result = [];
+        args.push(function (e, ea, s, t, sa, ta, u) {
+          result.push(callback(e, ea, s, t, sa, ta, u));
+        });
+      }
+
+      this[forEachName].apply(this, args);
+      return result;
+    };
+    /**
+     * Function filtering the graph's relevant edges using the provided predicate
+     * function.
+     *
+     * Arity 1: Filter all the relevant edges.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * Arity 2: Filter all of a node's relevant edges.
+     * @param  {any}      node      - Target node.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * Arity 3: Filter the relevant edges across the given path.
+     * @param  {any}      source    - Source node.
+     * @param  {any}      target    - Target node.
+     * @param  {function} predicate - Predicate to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[filterName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      var result = [];
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        if (callback(e, ea, s, t, sa, ta, u)) result.push(e);
+      });
+      this[forEachName].apply(this, args);
+      return result;
+    };
+    /**
+     * Function reducing the graph's relevant edges using the provided accumulator
+     * function.
+     *
+     * Arity 1: Reduce all the relevant edges.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * Arity 2: Reduce all of a node's relevant edges.
+     * @param  {any}      node         - Target node.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * Arity 3: Reduce the relevant edges across the given path.
+     * @param  {any}      source       - Source node.
+     * @param  {any}      target       - Target node.
+     * @param  {function} accumulator  - Accumulator to use.
+     * @param  {any}      initialValue - Initial value.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[reduceName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+
+      if (args.length < 2 || args.length > 4) {
+        throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(args.length, ")."));
+      }
+
+      if (typeof args[args.length - 1] === 'function' && typeof args[args.length - 2] !== 'function') {
+        throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+      }
+
+      var callback;
+      var initialValue;
+
+      if (args.length === 2) {
+        callback = args[0];
+        initialValue = args[1];
+        args = [];
+      } else if (args.length === 3) {
+        callback = args[1];
+        initialValue = args[2];
+        args = [args[0]];
+      } else if (args.length === 4) {
+        callback = args[2];
+        initialValue = args[3];
+        args = [args[0], args[1]];
+      }
+
+      var accumulator = initialValue;
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        accumulator = callback(accumulator, e, ea, s, t, sa, ta, u);
+      });
+      this[forEachName].apply(this, args);
+      return accumulator;
+    };
+  }
+  /**
+   * Function attaching a breakable edge callback iterator method to the Graph
+   * prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachFindEdge(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var findEdgeName = 'find' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over the graph's relevant edges in order to match
+     * one of them using the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[findEdgeName] = function (source, target, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return false;
+
+      if (arguments.length === 1) {
+        callback = source;
+        return forEachEdge(true, this, type, callback);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        callback = target;
+
+        var nodeData = this._nodes.get(source);
+
+        if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findEdgeName, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+        // TODO: maybe attach the sub method to the instance dynamically?
+
+        return forEachEdgeForNode(true, this.multi, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+      }
+
+      if (arguments.length === 3) {
+        source = '' + source;
+        target = '' + target;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(findEdgeName, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return forEachEdgeForPath(true, type, this.multi, direction, sourceData, target, callback);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(findEdgeName, ": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length, ")."));
+    };
+    /**
+     * Function iterating over the graph's relevant edges in order to assert
+     * whether any one of them matches the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var someName = 'some' + name[0].toUpperCase() + name.slice(1, -1);
+
+    Class.prototype[someName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        return callback(e, ea, s, t, sa, ta, u);
+      });
+      var found = this[findEdgeName].apply(this, args);
+      if (found) return true;
+      return false;
+    };
+    /**
+     * Function iterating over the graph's relevant edges in order to assert
+     * whether all of them matche the provided predicate function.
+     *
+     * Arity 1: Iterate over all the relevant edges.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 2: Iterate over all of a node's relevant edges.
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * Arity 3: Iterate over the relevant edges across the given path.
+     * @param  {any}      source   - Source node.
+     * @param  {any}      target   - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var everyName = 'every' + name[0].toUpperCase() + name.slice(1, -1);
+
+    Class.prototype[everyName] = function () {
+      var args = Array.prototype.slice.call(arguments);
+      var callback = args.pop();
+      args.push(function (e, ea, s, t, sa, ta, u) {
+        return !callback(e, ea, s, t, sa, ta, u);
+      });
+      var found = this[findEdgeName].apply(this, args);
+      if (found) return false;
+      return true;
+    };
+  }
+  /**
+   * Function attaching an edge iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachEdgeIteratorCreator(Class, description) {
+    var originalName = description.name,
+        type = description.type,
+        direction = description.direction;
+    var name = originalName.slice(0, -1) + 'Entries';
+    /**
+     * Function returning an iterator over the graph's edges.
+     *
+     * Arity 0: Iterate over all the relevant edges.
+     *
+     * Arity 1: Iterate over all of a node's relevant edges.
+     * @param  {any}   node   - Target node.
+     *
+     * Arity 2: Iterate over the relevant edges across the given path.
+     * @param  {any}   source - Source node.
+     * @param  {any}   target - Target node.
+     *
+     * @return {array|number} - The edges or the number of edges.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[name] = function (source, target) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return iterator.empty();
+      if (!arguments.length) return createEdgeIterator(this, type);
+
+      if (arguments.length === 1) {
+        source = '' + source;
+
+        var sourceData = this._nodes.get(source);
+
+        if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(source, "\" node in the graph.")); // Iterating over a node's edges
+
+        return createEdgeIteratorForNode(type, direction, sourceData);
+      }
+
+      if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target;
+
+        var _sourceData = this._nodes.get(source);
+
+        if (!_sourceData) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(source, "\" source node in the graph."));
+        if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.".concat(name, ":  could not find the \"").concat(target, "\" target node in the graph.")); // Iterating over the edges between source & target
+
+        return createEdgeIteratorForPath(type, direction, _sourceData, target);
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.".concat(name, ": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length, ")."));
+    };
+  }
+  /**
+   * Function attaching every edge iteration method to the Graph class.
+   *
+   * @param {function} Graph - Graph class.
+   */
+
+
+  function attachEdgeIterationMethods(Graph) {
+    EDGES_ITERATION.forEach(function (description) {
+      attachEdgeArrayCreator(Graph, description);
+      attachForEachEdge(Graph, description);
+      attachFindEdge(Graph, description);
+      attachEdgeIteratorCreator(Graph, description);
+    });
+  }
+
+  /**
+   * Graphology Neighbor Iteration
+   * ==============================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over
+   * neighbors.
+   */
+  /**
+   * Definitions.
+   */
+
+  var NEIGHBORS_ITERATION = [{
+    name: 'neighbors',
+    type: 'mixed'
+  }, {
+    name: 'inNeighbors',
+    type: 'directed',
+    direction: 'in'
+  }, {
+    name: 'outNeighbors',
+    type: 'directed',
+    direction: 'out'
+  }, {
+    name: 'inboundNeighbors',
+    type: 'mixed',
+    direction: 'in'
+  }, {
+    name: 'outboundNeighbors',
+    type: 'mixed',
+    direction: 'out'
+  }, {
+    name: 'directedNeighbors',
+    type: 'directed'
+  }, {
+    name: 'undirectedNeighbors',
+    type: 'undirected'
+  }];
+  /**
+   * Helpers.
+   */
+
+  function CompositeSetWrapper() {
+    this.A = null;
+    this.B = null;
+  }
+
+  CompositeSetWrapper.prototype.wrap = function (set) {
+    if (this.A === null) this.A = set;else if (this.B === null) this.B = set;
+  };
+
+  CompositeSetWrapper.prototype.has = function (key) {
+    if (this.A !== null && key in this.A) return true;
+    if (this.B !== null && key in this.B) return true;
+    return false;
+  };
+  /**
+   * Function iterating over the given node's relevant neighbors to match
+   * one of them using a predicated function.
+   *
+   * @param  {string}   type      - Type of neighbors.
+   * @param  {string}   direction - Direction.
+   * @param  {any}      nodeData  - Target node's data.
+   * @param  {function} callback  - Callback to use.
+   */
+
+
+  function forEachInObjectOnce(breakable, visited, nodeData, object, callback) {
+    for (var k in object) {
+      var edgeData = object[k];
+      var sourceData = edgeData.source;
+      var targetData = edgeData.target;
+      var neighborData = sourceData === nodeData ? targetData : sourceData;
+      if (visited && visited.has(neighborData.key)) continue;
+      var shouldBreak = callback(neighborData.key, neighborData.attributes);
+      if (breakable && shouldBreak) return neighborData.key;
+    }
+
+    return;
+  }
+
+  function forEachNeighbor(breakable, type, direction, nodeData, callback) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return forEachInObjectOnce(breakable, null, nodeData, nodeData.undirected, callback);
+      if (typeof direction === 'string') return forEachInObjectOnce(breakable, null, nodeData, nodeData[direction], callback);
+    } // Else we need to keep a set of neighbors not to return duplicates
+    // We cheat by querying the other adjacencies
+
+
+    var visited = new CompositeSetWrapper();
+    var found;
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        found = forEachInObjectOnce(breakable, null, nodeData, nodeData["in"], callback);
+        if (breakable && found) return found;
+        visited.wrap(nodeData["in"]);
+      }
+
+      if (direction !== 'in') {
+        found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.out, callback);
+        if (breakable && found) return found;
+        visited.wrap(nodeData.out);
+      }
+    }
+
+    if (type !== 'directed') {
+      found = forEachInObjectOnce(breakable, visited, nodeData, nodeData.undirected, callback);
+      if (breakable && found) return found;
+    }
+
+    return;
+  }
+  /**
+   * Function creating an array of relevant neighbors for the given node.
+   *
+   * @param  {string}       type      - Type of neighbors.
+   * @param  {string}       direction - Direction.
+   * @param  {any}          nodeData  - Target node's data.
+   * @return {Array}                  - The list of neighbors.
+   */
+
+
+  function createNeighborArrayForNode(type, direction, nodeData) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return Object.keys(nodeData.undirected);
+      if (typeof direction === 'string') return Object.keys(nodeData[direction]);
+    }
+
+    var neighbors = [];
+    forEachNeighbor(false, type, direction, nodeData, function (key) {
+      neighbors.push(key);
+    });
+    return neighbors;
+  }
+  /**
+   * Function returning an iterator over the given node's relevant neighbors.
+   *
+   * @param  {string}   type      - Type of neighbors.
+   * @param  {string}   direction - Direction.
+   * @param  {any}      nodeData  - Target node's data.
+   * @return {Iterator}
+   */
+
+
+  function createDedupedObjectIterator(visited, nodeData, object) {
+    var keys = Object.keys(object);
+    var l = keys.length;
+    var i = 0;
+    return new iterator(function next() {
+      var neighborData = null;
+
+      do {
+        if (i >= l) {
+          if (visited) visited.wrap(object);
+          return {
+            done: true
+          };
+        }
+
+        var edgeData = object[keys[i++]];
+        var sourceData = edgeData.source;
+        var targetData = edgeData.target;
+        neighborData = sourceData === nodeData ? targetData : sourceData;
+
+        if (visited && visited.has(neighborData.key)) {
+          neighborData = null;
+          continue;
+        }
+      } while (neighborData === null);
+
+      return {
+        done: false,
+        value: {
+          neighbor: neighborData.key,
+          attributes: neighborData.attributes
+        }
+      };
+    });
+  }
+
+  function createNeighborIterator(type, direction, nodeData) {
+    // If we want only undirected or in or out, we can roll some optimizations
+    if (type !== 'mixed') {
+      if (type === 'undirected') return createDedupedObjectIterator(null, nodeData, nodeData.undirected);
+      if (typeof direction === 'string') return createDedupedObjectIterator(null, nodeData, nodeData[direction]);
+    }
+
+    var iterator$1 = iterator.empty(); // Else we need to keep a set of neighbors not to return duplicates
+    // We cheat by querying the other adjacencies
+
+    var visited = new CompositeSetWrapper();
+
+    if (type !== 'undirected') {
+      if (direction !== 'out') {
+        iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData["in"]));
+      }
+
+      if (direction !== 'in') {
+        iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData.out));
+      }
+    }
+
+    if (type !== 'directed') {
+      iterator$1 = chain(iterator$1, createDedupedObjectIterator(visited, nodeData, nodeData.undirected));
+    }
+
+    return iterator$1;
+  }
+  /**
+   * Function attaching a neighbors array creator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachNeighborArrayCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    /**
+     * Function returning an array of certain neighbors.
+     *
+     * @param  {any}   node   - Target node.
+     * @return {array} - The neighbors of neighbors.
+     *
+     * @throws {Error} - Will throw if node is not found in the graph.
+     */
+
+    Class.prototype[name] = function (node) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return [];
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(name, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return createNeighborArrayForNode(type === 'mixed' ? this.type : type, direction, nodeData);
+    };
+  }
+  /**
+   * Function attaching a neighbors callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachForEachNeighbor(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var forEachName = 'forEach' + name[0].toUpperCase() + name.slice(1, -1);
+    /**
+     * Function iterating over all the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[forEachName] = function (node, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(forEachName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      forEachNeighbor(false, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    };
+    /**
+     * Function mapping the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var mapName = 'map' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[mapName] = function (node, callback) {
+      // TODO: optimize when size is known beforehand
+      var result = [];
+      this[forEachName](node, function (n, a) {
+        result.push(callback(n, a));
+      });
+      return result;
+    };
+    /**
+     * Function filtering the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var filterName = 'filter' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[filterName] = function (node, callback) {
+      var result = [];
+      this[forEachName](node, function (n, a) {
+        if (callback(n, a)) result.push(n);
+      });
+      return result;
+    };
+    /**
+     * Function reducing the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var reduceName = 'reduce' + name[0].toUpperCase() + name.slice(1);
+
+    Class.prototype[reduceName] = function (node, callback, initialValue) {
+      if (arguments.length < 3) throw new InvalidArgumentsGraphError("Graph.".concat(reduceName, ": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));
+      var accumulator = initialValue;
+      this[forEachName](node, function (n, a) {
+        accumulator = callback(accumulator, n, a);
+      });
+      return accumulator;
+    };
+  }
+  /**
+   * Function attaching a breakable neighbors callback iterator method to the
+   * Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachFindNeighbor(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var capitalizedSingular = name[0].toUpperCase() + name.slice(1, -1);
+    var findName = 'find' + capitalizedSingular;
+    /**
+     * Function iterating over all the relevant neighbors using a callback.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {undefined}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[findName] = function (node, callback) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return;
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(findName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return forEachNeighbor(true, type === 'mixed' ? this.type : type, direction, nodeData, callback);
+    };
+    /**
+     * Function iterating over all the relevant neighbors to find if any of them
+     * matches the given predicate.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var someName = 'some' + capitalizedSingular;
+
+    Class.prototype[someName] = function (node, callback) {
+      var found = this[findName](node, callback);
+      if (found) return true;
+      return false;
+    };
+    /**
+     * Function iterating over all the relevant neighbors to find if all of them
+     * matche the given predicate.
+     *
+     * @param  {any}      node     - Target node.
+     * @param  {function} callback - Callback to use.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+
+    var everyName = 'every' + capitalizedSingular;
+
+    Class.prototype[everyName] = function (node, callback) {
+      var found = this[findName](node, function (n, a) {
+        return !callback(n, a);
+      });
+      if (found) return false;
+      return true;
+    };
+  }
+  /**
+   * Function attaching a neighbors callback iterator method to the Graph prototype.
+   *
+   * @param {function} Class       - Target class.
+   * @param {object}   description - Method description.
+   */
+
+
+  function attachNeighborIteratorCreator(Class, description) {
+    var name = description.name,
+        type = description.type,
+        direction = description.direction;
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    /**
+     * Function returning an iterator over all the relevant neighbors.
+     *
+     * @param  {any}      node     - Target node.
+     * @return {Iterator}
+     *
+     * @throws {Error} - Will throw if there are too many arguments.
+     */
+
+    Class.prototype[iteratorName] = function (node) {
+      // Early termination
+      if (type !== 'mixed' && this.type !== 'mixed' && type !== this.type) return iterator.empty();
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (typeof nodeData === 'undefined') throw new NotFoundGraphError("Graph.".concat(iteratorName, ": could not find the \"").concat(node, "\" node in the graph.")); // Here, we want to iterate over a node's relevant neighbors
+
+      return createNeighborIterator(type === 'mixed' ? this.type : type, direction, nodeData);
+    };
+  }
+  /**
+   * Function attaching every neighbor iteration method to the Graph class.
+   *
+   * @param {function} Graph - Graph class.
+   */
+
+
+  function attachNeighborIterationMethods(Graph) {
+    NEIGHBORS_ITERATION.forEach(function (description) {
+      attachNeighborArrayCreator(Graph, description);
+      attachForEachNeighbor(Graph, description);
+      attachFindNeighbor(Graph, description);
+      attachNeighborIteratorCreator(Graph, description);
+    });
+  }
+
+  /**
+   * Graphology Adjacency Iteration
+   * ===============================
+   *
+   * Attaching some methods to the Graph class to be able to iterate over a
+   * graph's adjacency.
+   */
+
+  /**
+   * Function iterating over a simple graph's adjacency using a callback.
+   *
+   * @param {boolean}  breakable         - Can we break?
+   * @param {boolean}  assymetric        - Whether to emit undirected edges only once.
+   * @param {boolean}  disconnectedNodes - Whether to emit disconnected nodes.
+   * @param {Graph}    graph             - Target Graph instance.
+   * @param {callback} function          - Iteration callback.
+   */
+  function forEachAdjacency(breakable, assymetric, disconnectedNodes, graph, callback) {
+    var iterator = graph._nodes.values();
+
+    var type = graph.type;
+    var step, sourceData, neighbor, adj, edgeData, targetData, shouldBreak;
+
+    while (step = iterator.next(), step.done !== true) {
+      var hasEdges = false;
+      sourceData = step.value;
+
+      if (type !== 'undirected') {
+        adj = sourceData.out;
+
+        for (neighbor in adj) {
+          edgeData = adj[neighbor];
+
+          do {
+            targetData = edgeData.target;
+            hasEdges = true;
+            shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+            if (breakable && shouldBreak) return edgeData;
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (type !== 'directed') {
+        adj = sourceData.undirected;
+
+        for (neighbor in adj) {
+          if (assymetric && sourceData.key > neighbor) continue;
+          edgeData = adj[neighbor];
+
+          do {
+            targetData = edgeData.target;
+            if (targetData.key !== neighbor) targetData = edgeData.source;
+            hasEdges = true;
+            shouldBreak = callback(sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.key, edgeData.attributes, edgeData.undirected);
+            if (breakable && shouldBreak) return edgeData;
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (disconnectedNodes && !hasEdges) {
+        shouldBreak = callback(sourceData.key, null, sourceData.attributes, null, null, null, null);
+        if (breakable && shouldBreak) return null;
+      }
+    }
+
+    return;
+  }
+
+  /**
+   * Graphology Serialization Utilities
+   * ===================================
+   *
+   * Collection of functions used by the graph serialization schemes.
+   */
+  /**
+   * Formats internal node data into a serialized node.
+   *
+   * @param  {any}    key  - The node's key.
+   * @param  {object} data - Internal node's data.
+   * @return {array}       - The serialized node.
+   */
+
+  function serializeNode(key, data) {
+    var serialized = {
+      key: key
+    };
+    if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+    return serialized;
+  }
+  /**
+   * Formats internal edge data into a serialized edge.
+   *
+   * @param  {any}    key  - The edge's key.
+   * @param  {object} data - Internal edge's data.
+   * @return {array}       - The serialized edge.
+   */
+
+  function serializeEdge(key, data) {
+    var serialized = {
+      key: key,
+      source: data.source.key,
+      target: data.target.key
+    };
+    if (!isEmpty(data.attributes)) serialized.attributes = assign({}, data.attributes);
+    if (data.undirected) serialized.undirected = true;
+    return serialized;
+  }
+  /**
+   * Checks whether the given value is a serialized node.
+   *
+   * @param  {mixed} value - Target value.
+   * @return {string|null}
+   */
+
+  function validateSerializedNode(value) {
+    if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');
+    if (!('key' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized node is missing its key.');
+    if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+  }
+  /**
+   * Checks whether the given value is a serialized edge.
+   *
+   * @param  {mixed} value - Target value.
+   * @return {string|null}
+   */
+
+  function validateSerializedEdge(value) {
+    if (!isPlainObject(value)) throw new InvalidArgumentsGraphError('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');
+    if (!('source' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its source.');
+    if (!('target' in value)) throw new InvalidArgumentsGraphError('Graph.import: serialized edge is missing its target.');
+    if ('attributes' in value && (!isPlainObject(value.attributes) || value.attributes === null)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.');
+    if ('undirected' in value && typeof value.undirected !== 'boolean') throw new InvalidArgumentsGraphError('Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.');
+  }
+
+  /**
+   * Constants.
+   */
+
+  var INSTANCE_ID = incrementalIdStartingFromRandomByte();
+  /**
+   * Enums.
+   */
+
+  var TYPES = new Set(['directed', 'undirected', 'mixed']);
+  var EMITTER_PROPS = new Set(['domain', '_events', '_eventsCount', '_maxListeners']);
+  var EDGE_ADD_METHODS = [{
+    name: function name(verb) {
+      return "".concat(verb, "Edge");
+    },
+    generateKey: true
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "DirectedEdge");
+    },
+    generateKey: true,
+    type: 'directed'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "UndirectedEdge");
+    },
+    generateKey: true,
+    type: 'undirected'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "EdgeWithKey");
+    }
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "DirectedEdgeWithKey");
+    },
+    type: 'directed'
+  }, {
+    name: function name(verb) {
+      return "".concat(verb, "UndirectedEdgeWithKey");
+    },
+    type: 'undirected'
+  }];
+  /**
+   * Default options.
+   */
+
+  var DEFAULTS = {
+    allowSelfLoops: true,
+    multi: false,
+    type: 'mixed'
+  };
+  /**
+   * Abstract functions used by the Graph class for various methods.
+   */
+
+  /**
+   * Internal method used to add a node to the given graph
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {any}     node            - The node's key.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @return {NodeData}                - Created node data.
+   */
+
+  function _addNode(graph, node, attributes) {
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.addNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+    node = '' + node;
+    attributes = attributes || {};
+    if (graph._nodes.has(node)) throw new UsageGraphError("Graph.addNode: the \"".concat(node, "\" node already exist in the graph."));
+    var data = new graph.NodeDataClass(node, attributes); // Adding the node to internal register
+
+    graph._nodes.set(node, data); // Emitting
+
+
+    graph.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return data;
+  }
+  /**
+   * Same as the above but without sanity checks because we call this in contexts
+   * where necessary checks were already done.
+   */
+
+
+  function unsafeAddNode(graph, node, attributes) {
+    var data = new graph.NodeDataClass(node, attributes);
+
+    graph._nodes.set(node, data);
+
+    graph.emit('nodeAdded', {
+      key: node,
+      attributes: attributes
+    });
+    return data;
+  }
+  /**
+   * Internal method used to add an arbitrary edge to the given graph.
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {string}  name            - Name of the child method for errors.
+   * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+   * @param  {boolean} undirected      - Whether the edge is undirected.
+   * @param  {any}     edge            - The edge's key.
+   * @param  {any}     source          - The source node.
+   * @param  {any}     target          - The target node.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @return {any}                     - The edge.
+   *
+   * @throws {Error} - Will throw if the graph is of the wrong type.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   * @throws {Error} - Will throw if the edge already exist.
+   */
+
+
+  function addEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes) {
+    // Checking validity of operation
+    if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));
+    if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));
+    if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\"")); // Coercion of source & target:
+
+    source = '' + source;
+    target = '' + target;
+    attributes = attributes || {};
+    if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+    var sourceData = graph._nodes.get(source),
+        targetData = graph._nodes.get(target);
+
+    if (!sourceData) throw new NotFoundGraphError("Graph.".concat(name, ": source node \"").concat(source, "\" not found."));
+    if (!targetData) throw new NotFoundGraphError("Graph.".concat(name, ": target node \"").concat(target, "\" not found.")); // Must the graph generate an id for this edge?
+
+    var eventData = {
+      key: null,
+      undirected: undirected,
+      source: source,
+      target: target,
+      attributes: attributes
+    };
+
+    if (mustGenerateKey) {
+      // NOTE: in this case we can guarantee that the key does not already
+      // exist and is already correctly casted as a string
+      edge = graph._edgeKeyGenerator();
+    } else {
+      // Coercion of edge key
+      edge = '' + edge; // Here, we have a key collision
+
+      if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+    } // Here, we might have a source / target collision
+
+
+    if (!graph.multi && (undirected ? typeof sourceData.undirected[target] !== 'undefined' : typeof sourceData.out[target] !== 'undefined')) {
+      throw new UsageGraphError("Graph.".concat(name, ": an edge linking \"").concat(source, "\" to \"").concat(target, "\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));
+    } // Storing some data
+
+
+    var edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+    graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+    var isSelfLoop = source === target;
+
+    if (undirected) {
+      sourceData.undirectedDegree++;
+      targetData.undirectedDegree++;
+      if (isSelfLoop) graph._undirectedSelfLoopCount++;
+    } else {
+      sourceData.outDegree++;
+      targetData.inDegree++;
+      if (isSelfLoop) graph._directedSelfLoopCount++;
+    } // Updating relevant index
+
+
+    if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+    if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+    eventData.key = edge;
+    graph.emit('edgeAdded', eventData);
+    return edge;
+  }
+  /**
+   * Internal method used to add an arbitrary edge to the given graph.
+   *
+   * @param  {Graph}   graph           - Target graph.
+   * @param  {string}  name            - Name of the child method for errors.
+   * @param  {boolean} mustGenerateKey - Should the graph generate an id?
+   * @param  {boolean} undirected      - Whether the edge is undirected.
+   * @param  {any}     edge            - The edge's key.
+   * @param  {any}     source          - The source node.
+   * @param  {any}     target          - The target node.
+   * @param  {object}  [attributes]    - Optional attributes.
+   * @param  {boolean} [asUpdater]       - Are we updating or merging?
+   * @return {any}                     - The edge.
+   *
+   * @throws {Error} - Will throw if the graph is of the wrong type.
+   * @throws {Error} - Will throw if the given attributes are not an object.
+   * @throws {Error} - Will throw if source or target doesn't exist.
+   * @throws {Error} - Will throw if the edge already exist.
+   */
+
+
+  function mergeEdge(graph, name, mustGenerateKey, undirected, edge, source, target, attributes, asUpdater) {
+    // Checking validity of operation
+    if (!undirected && graph.type === 'undirected') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));
+    if (undirected && graph.type === 'directed') throw new UsageGraphError("Graph.".concat(name, ": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));
+
+    if (attributes) {
+      if (asUpdater) {
+        if (typeof attributes !== 'function') throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid updater function. Expecting a function but got \"").concat(attributes, "\""));
+      } else {
+        if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.".concat(name, ": invalid attributes. Expecting an object but got \"").concat(attributes, "\""));
+      }
+    } // Coercion of source & target:
+
+
+    source = '' + source;
+    target = '' + target;
+    var updater;
+
+    if (asUpdater) {
+      updater = attributes;
+      attributes = undefined;
+    }
+
+    if (!graph.allowSelfLoops && source === target) throw new UsageGraphError("Graph.".concat(name, ": source & target are the same (\"").concat(source, "\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));
+
+    var sourceData = graph._nodes.get(source);
+
+    var targetData = graph._nodes.get(target);
+
+    var edgeData; // Do we need to handle duplicate?
+
+    var alreadyExistingEdgeData;
+
+    if (!mustGenerateKey) {
+      edgeData = graph._edges.get(edge);
+
+      if (edgeData) {
+        // Here, we need to ensure, if the user gave a key, that source & target
+        // are consistent
+        if (edgeData.source.key !== source || edgeData.target.key !== target) {
+          // If source or target inconsistent
+          if (!undirected || edgeData.source.key !== target || edgeData.target.key !== source) {
+            // If directed, or source/target aren't flipped
+            throw new UsageGraphError("Graph.".concat(name, ": inconsistency detected when attempting to merge the \"").concat(edge, "\" edge with \"").concat(source, "\" source & \"").concat(target, "\" target vs. (\"").concat(edgeData.source.key, "\", \"").concat(edgeData.target.key, "\")."));
+          }
+        }
+
+        alreadyExistingEdgeData = edgeData;
+      }
+    } // Here, we might have a source / target collision
+
+
+    if (!alreadyExistingEdgeData && !graph.multi && sourceData) {
+      alreadyExistingEdgeData = undirected ? sourceData.undirected[target] : sourceData.out[target];
+    } // Handling duplicates
+
+
+    if (alreadyExistingEdgeData) {
+      var info = [alreadyExistingEdgeData.key, false, false, false]; // We can skip the attribute merging part if the user did not provide them
+
+      if (asUpdater ? !updater : !attributes) return info; // Updating the attributes
+
+      if (asUpdater) {
+        var oldAttributes = alreadyExistingEdgeData.attributes;
+        alreadyExistingEdgeData.attributes = updater(oldAttributes);
+        graph.emit('edgeAttributesUpdated', {
+          type: 'replace',
+          key: alreadyExistingEdgeData.key,
+          attributes: alreadyExistingEdgeData.attributes
+        });
+      } // Merging the attributes
+      else {
+        assign(alreadyExistingEdgeData.attributes, attributes);
+        graph.emit('edgeAttributesUpdated', {
+          type: 'merge',
+          key: alreadyExistingEdgeData.key,
+          attributes: alreadyExistingEdgeData.attributes,
+          data: attributes
+        });
+      }
+
+      return info;
+    }
+
+    attributes = attributes || {};
+    if (asUpdater && updater) attributes = updater(attributes); // Must the graph generate an id for this edge?
+
+    var eventData = {
+      key: null,
+      undirected: undirected,
+      source: source,
+      target: target,
+      attributes: attributes
+    };
+
+    if (mustGenerateKey) {
+      // NOTE: in this case we can guarantee that the key does not already
+      // exist and is already correctly casted as a string
+      edge = graph._edgeKeyGenerator();
+    } else {
+      // Coercion of edge key
+      edge = '' + edge; // Here, we have a key collision
+
+      if (graph._edges.has(edge)) throw new UsageGraphError("Graph.".concat(name, ": the \"").concat(edge, "\" edge already exists in the graph."));
+    }
+
+    var sourceWasAdded = false;
+    var targetWasAdded = false;
+
+    if (!sourceData) {
+      sourceData = unsafeAddNode(graph, source, {});
+      sourceWasAdded = true;
+
+      if (source === target) {
+        targetData = sourceData;
+        targetWasAdded = true;
+      }
+    }
+
+    if (!targetData) {
+      targetData = unsafeAddNode(graph, target, {});
+      targetWasAdded = true;
+    } // Storing some data
+
+
+    edgeData = new EdgeData(undirected, edge, sourceData, targetData, attributes); // Adding the edge to the internal register
+
+    graph._edges.set(edge, edgeData); // Incrementing node degree counters
+
+
+    var isSelfLoop = source === target;
+
+    if (undirected) {
+      sourceData.undirectedDegree++;
+      targetData.undirectedDegree++;
+      if (isSelfLoop) graph._undirectedSelfLoopCount++;
+    } else {
+      sourceData.outDegree++;
+      targetData.inDegree++;
+      if (isSelfLoop) graph._directedSelfLoopCount++;
+    } // Updating relevant index
+
+
+    if (graph.multi) edgeData.attachMulti();else edgeData.attach();
+    if (undirected) graph._undirectedSize++;else graph._directedSize++; // Emitting
+
+    eventData.key = edge;
+    graph.emit('edgeAdded', eventData);
+    return [edge, true, sourceWasAdded, targetWasAdded];
+  }
+  /**
+   * Internal method used to drop an edge.
+   *
+   * @param  {Graph}    graph    - Target graph.
+   * @param  {EdgeData} edgeData - Data of the edge to drop.
+   */
+
+
+  function dropEdgeFromData(graph, edgeData) {
+    // Dropping the edge from the register
+    graph._edges["delete"](edgeData.key); // Updating related degrees
+
+
+    var sourceData = edgeData.source,
+        targetData = edgeData.target,
+        attributes = edgeData.attributes;
+    var undirected = edgeData.undirected;
+    var isSelfLoop = sourceData === targetData;
+
+    if (undirected) {
+      sourceData.undirectedDegree--;
+      targetData.undirectedDegree--;
+      if (isSelfLoop) graph._undirectedSelfLoopCount--;
+    } else {
+      sourceData.outDegree--;
+      targetData.inDegree--;
+      if (isSelfLoop) graph._directedSelfLoopCount--;
+    } // Clearing index
+
+
+    if (graph.multi) edgeData.detachMulti();else edgeData.detach();
+    if (undirected) graph._undirectedSize--;else graph._directedSize--; // Emitting
+
+    graph.emit('edgeDropped', {
+      key: edgeData.key,
+      attributes: attributes,
+      source: sourceData.key,
+      target: targetData.key,
+      undirected: undirected
+    });
+  }
+  /**
+   * Graph class
+   *
+   * @constructor
+   * @param  {object}  [options] - Options:
+   * @param  {boolean}   [allowSelfLoops] - Allow self loops?
+   * @param  {string}    [type]           - Type of the graph.
+   * @param  {boolean}   [map]            - Allow references as keys?
+   * @param  {boolean}   [multi]          - Allow parallel edges?
+   *
+   * @throws {Error} - Will throw if the arguments are not valid.
+   */
+
+
+  var Graph = /*#__PURE__*/function (_EventEmitter) {
+    _inheritsLoose(Graph, _EventEmitter);
+
+    function Graph(options) {
+      var _this;
+
+      _this = _EventEmitter.call(this) || this; //-- Solving options
+
+      options = assign({}, DEFAULTS, options); // Enforcing options validity
+
+      if (typeof options.multi !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(options.multi, "\"."));
+      if (!TYPES.has(options.type)) throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'type' option. Should be one of \"mixed\", \"directed\" or \"undirected\" but got \"".concat(options.type, "\"."));
+      if (typeof options.allowSelfLoops !== 'boolean') throw new InvalidArgumentsGraphError("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(options.allowSelfLoops, "\".")); //-- Private properties
+      // Utilities
+
+      var NodeDataClass = options.type === 'mixed' ? MixedNodeData : options.type === 'directed' ? DirectedNodeData : UndirectedNodeData;
+      privateProperty(_assertThisInitialized(_this), 'NodeDataClass', NodeDataClass); // Internal edge key generator
+      // NOTE: this internal generator produce keys that are strings
+      // composed of a weird prefix, an incremental instance id starting from
+      // a random byte and finally an internal instance incremental id.
+      // All this to avoid intra-frame and cross-frame adversarial inputs
+      // that can force a single #.addEdge call to degenerate into a O(n)
+      // available key search loop.
+      // It also ensures that automatically generated edge keys are unlikely
+      // to produce collisions with arbitrary keys given by users.
+
+      var instancePrefix = 'geid_' + INSTANCE_ID() + '_';
+      var edgeId = 0;
+
+      var edgeKeyGenerator = function edgeKeyGenerator() {
+        var availableEdgeKey;
+
+        do {
+          availableEdgeKey = instancePrefix + edgeId++;
+        } while (_this._edges.has(availableEdgeKey));
+
+        return availableEdgeKey;
+      }; // Indexes
+
+
+      privateProperty(_assertThisInitialized(_this), '_attributes', {});
+      privateProperty(_assertThisInitialized(_this), '_nodes', new Map());
+      privateProperty(_assertThisInitialized(_this), '_edges', new Map());
+      privateProperty(_assertThisInitialized(_this), '_directedSize', 0);
+      privateProperty(_assertThisInitialized(_this), '_undirectedSize', 0);
+      privateProperty(_assertThisInitialized(_this), '_directedSelfLoopCount', 0);
+      privateProperty(_assertThisInitialized(_this), '_undirectedSelfLoopCount', 0);
+      privateProperty(_assertThisInitialized(_this), '_edgeKeyGenerator', edgeKeyGenerator); // Options
+
+      privateProperty(_assertThisInitialized(_this), '_options', options); // Emitter properties
+
+      EMITTER_PROPS.forEach(function (prop) {
+        return privateProperty(_assertThisInitialized(_this), prop, _this[prop]);
+      }); //-- Properties readers
+
+      readOnlyProperty(_assertThisInitialized(_this), 'order', function () {
+        return _this._nodes.size;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'size', function () {
+        return _this._edges.size;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'directedSize', function () {
+        return _this._directedSize;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'undirectedSize', function () {
+        return _this._undirectedSize;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'selfLoopCount', function () {
+        return _this._directedSelfLoopCount + _this._undirectedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'directedSelfLoopCount', function () {
+        return _this._directedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'undirectedSelfLoopCount', function () {
+        return _this._undirectedSelfLoopCount;
+      });
+      readOnlyProperty(_assertThisInitialized(_this), 'multi', _this._options.multi);
+      readOnlyProperty(_assertThisInitialized(_this), 'type', _this._options.type);
+      readOnlyProperty(_assertThisInitialized(_this), 'allowSelfLoops', _this._options.allowSelfLoops);
+      readOnlyProperty(_assertThisInitialized(_this), 'implementation', function () {
+        return 'graphology';
+      });
+      return _this;
+    }
+
+    var _proto = Graph.prototype;
+
+    _proto._resetInstanceCounters = function _resetInstanceCounters() {
+      this._directedSize = 0;
+      this._undirectedSize = 0;
+      this._directedSelfLoopCount = 0;
+      this._undirectedSelfLoopCount = 0;
+    }
+    /**---------------------------------------------------------------------------
+     * Read
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning whether the given node is found in the graph.
+     *
+     * @param  {any}     node - The node.
+     * @return {boolean}
+     */
+    ;
+
+    _proto.hasNode = function hasNode(node) {
+      return this._nodes.has('' + node);
+    }
+    /**
+     * Method returning whether the given directed edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasDirectedEdge = function hasDirectedEdge(source, target) {
+      // Early termination
+      if (this.type === 'undirected') return false;
+
+      if (arguments.length === 1) {
+        var edge = '' + source;
+
+        var edgeData = this._edges.get(edge);
+
+        return !!edgeData && !edgeData.undirected;
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = nodeData.out[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning whether the given undirected edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasUndirectedEdge = function hasUndirectedEdge(source, target) {
+      // Early termination
+      if (this.type === 'directed') return false;
+
+      if (arguments.length === 1) {
+        var edge = '' + source;
+
+        var edgeData = this._edges.get(edge);
+
+        return !!edgeData && edgeData.undirected;
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = nodeData.undirected[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning whether the given edge is found in the graph.
+     *
+     * Arity 1:
+     * @param  {any}     edge - The edge's key.
+     *
+     * Arity 2:
+     * @param  {any}     source - The edge's source.
+     * @param  {any}     target - The edge's target.
+     *
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the arguments are invalid.
+     */
+    ;
+
+    _proto.hasEdge = function hasEdge(source, target) {
+      if (arguments.length === 1) {
+        var edge = '' + source;
+        return this._edges.has(edge);
+      } else if (arguments.length === 2) {
+        source = '' + source;
+        target = '' + target; // If the node source or the target is not in the graph we break
+
+        var nodeData = this._nodes.get(source);
+
+        if (!nodeData) return false; // Is there a directed edge pointing toward target?
+
+        var edges = typeof nodeData.out !== 'undefined' && nodeData.out[target];
+        if (!edges) edges = typeof nodeData.undirected !== 'undefined' && nodeData.undirected[target];
+        if (!edges) return false;
+        return this.multi ? !!edges.size : true;
+      }
+
+      throw new InvalidArgumentsGraphError("Graph.hasEdge: invalid arity (".concat(arguments.length, ", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."));
+    }
+    /**
+     * Method returning the edge matching source & target in a directed fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.directedEdge = function directedEdge(source, target) {
+      if (this.type === 'undirected') return;
+      source = '' + source;
+      target = '' + target;
+      if (this.multi) throw new UsageGraphError('Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.');
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.directedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.out && sourceData.out[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning the edge matching source & target in a undirected fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.undirectedEdge = function undirectedEdge(source, target) {
+      if (this.type === 'directed') return;
+      source = '' + source;
+      target = '' + target;
+      if (this.multi) throw new UsageGraphError('Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.');
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.undirectedEdge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.undirected && sourceData.undirected[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning the edge matching source & target in a mixed fashion.
+     *
+     * @param  {any} source - The edge's source.
+     * @param  {any} target - The edge's target.
+     *
+     * @return {any|undefined}
+     *
+     * @throws {Error} - Will throw if the graph is multi.
+     * @throws {Error} - Will throw if source or target doesn't exist.
+     */
+    ;
+
+    _proto.edge = function edge(source, target) {
+      if (this.multi) throw new UsageGraphError('Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.');
+      source = '' + source;
+      target = '' + target;
+
+      var sourceData = this._nodes.get(source);
+
+      if (!sourceData) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(source, "\" source node in the graph."));
+      if (!this._nodes.has(target)) throw new NotFoundGraphError("Graph.edge: could not find the \"".concat(target, "\" target node in the graph."));
+      var edgeData = sourceData.out && sourceData.out[target] || sourceData.undirected && sourceData.undirected[target] || undefined;
+      if (edgeData) return edgeData.key;
+    }
+    /**
+     * Method returning whether two nodes are directed neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areDirectedNeighbors = function areDirectedNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areDirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData["in"] || neighbor in nodeData.out;
+    }
+    /**
+     * Method returning whether two nodes are out neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areOutNeighbors = function areOutNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areOutNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData.out;
+    }
+    /**
+     * Method returning whether two nodes are in neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areInNeighbors = function areInNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areInNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return false;
+      return neighbor in nodeData["in"];
+    }
+    /**
+     * Method returning whether two nodes are undirected neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areUndirectedNeighbors = function areUndirectedNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areUndirectedNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return false;
+      return neighbor in nodeData.undirected;
+    }
+    /**
+     * Method returning whether two nodes are neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areNeighbors = function areNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData["in"] || neighbor in nodeData.out) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether two nodes are inbound neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areInboundNeighbors = function areInboundNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areInboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData["in"]) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether two nodes are outbound neighbors.
+     *
+     * @param  {any}     node     - The node's key.
+     * @param  {any}     neighbor - The neighbor's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.areOutboundNeighbors = function areOutboundNeighbors(node, neighbor) {
+      node = '' + node;
+      neighbor = '' + neighbor;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.areOutboundNeighbors: could not find the \"".concat(node, "\" node in the graph."));
+
+      if (this.type !== 'undirected') {
+        if (neighbor in nodeData.out) return true;
+      }
+
+      if (this.type !== 'directed') {
+        if (neighbor in nodeData.undirected) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning the given node's in degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inDegree = function inDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.inDegree;
+    }
+    /**
+     * Method returning the given node's out degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outDegree = function outDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.outDegree;
+    }
+    /**
+     * Method returning the given node's directed degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.directedDegree = function directedDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.directedDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      return nodeData.inDegree + nodeData.outDegree;
+    }
+    /**
+     * Method returning the given node's undirected degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.undirectedDegree = function undirectedDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegree: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return 0;
+      return nodeData.undirectedDegree;
+    }
+    /**
+     * Method returning the given node's inbound degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's inbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inboundDegree = function inboundDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's outbound degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's outbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outboundDegree = function outboundDegree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.outDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's directed degree.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.degree = function degree(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.degree: could not find the \"".concat(node, "\" node in the graph."));
+      var degree = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree + nodeData.outDegree;
+      }
+
+      return degree;
+    }
+    /**
+     * Method returning the given node's in degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inDegreeWithoutSelfLoops = function inDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData["in"][node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.inDegree - loops;
+    }
+    /**
+     * Method returning the given node's out degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outDegreeWithoutSelfLoops = function outDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData.out[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.outDegree - loops;
+    }
+    /**
+     * Method returning the given node's directed degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.directedDegreeWithoutSelfLoops = function directedDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.directedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'undirected') return 0;
+      var self = nodeData.out[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.inDegree + nodeData.outDegree - loops * 2;
+    }
+    /**
+     * Method returning the given node's undirected degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's in degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.undirectedDegreeWithoutSelfLoops = function undirectedDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.undirectedDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      if (this.type === 'directed') return 0;
+      var self = nodeData.undirected[node];
+      var loops = self ? this.multi ? self.size : 1 : 0;
+      return nodeData.undirectedDegree - loops * 2;
+    }
+    /**
+     * Method returning the given node's inbound degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's inbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.inboundDegreeWithoutSelfLoops = function inboundDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.inboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree;
+        self = nodeData.out[node];
+        loops += self ? this.multi ? self.size : 1 : 0;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given node's outbound degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's outbound degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.outboundDegreeWithoutSelfLoops = function outboundDegreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.outboundDegreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.outDegree;
+        self = nodeData["in"][node];
+        loops += self ? this.multi ? self.size : 1 : 0;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given node's directed degree without considering self loops.
+     *
+     * @param  {any}     node - The node's key.
+     * @return {number}       - The node's degree.
+     *
+     * @throws {Error} - Will throw if the node isn't in the graph.
+     */
+    ;
+
+    _proto.degreeWithoutSelfLoops = function degreeWithoutSelfLoops(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.degreeWithoutSelfLoops: could not find the \"".concat(node, "\" node in the graph."));
+      var self;
+      var degree = 0;
+      var loops = 0;
+
+      if (this.type !== 'directed') {
+        degree += nodeData.undirectedDegree;
+        self = nodeData.undirected[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      if (this.type !== 'undirected') {
+        degree += nodeData.inDegree + nodeData.outDegree;
+        self = nodeData.out[node];
+        loops += (self ? this.multi ? self.size : 1 : 0) * 2;
+      }
+
+      return degree - loops;
+    }
+    /**
+     * Method returning the given edge's source.
+     *
+     * @param  {any} edge - The edge's key.
+     * @return {any}      - The edge's source.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.source = function source(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.source: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source.key;
+    }
+    /**
+     * Method returning the given edge's target.
+     *
+     * @param  {any} edge - The edge's key.
+     * @return {any}      - The edge's target.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.target = function target(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.target: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.target.key;
+    }
+    /**
+     * Method returning the given edge's extremities.
+     *
+     * @param  {any}   edge - The edge's key.
+     * @return {array}      - The edge's extremities.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.extremities = function extremities(edge) {
+      edge = '' + edge;
+
+      var edgeData = this._edges.get(edge);
+
+      if (!edgeData) throw new NotFoundGraphError("Graph.extremities: could not find the \"".concat(edge, "\" edge in the graph."));
+      return [edgeData.source.key, edgeData.target.key];
+    }
+    /**
+     * Given a node & an edge, returns the other extremity of the edge.
+     *
+     * @param  {any}   node - The node's key.
+     * @param  {any}   edge - The edge's key.
+     * @return {any}        - The related node.
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph or if the
+     *                   edge & node are not related.
+     */
+    ;
+
+    _proto.opposite = function opposite(node, edge) {
+      node = '' + node;
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.opposite: could not find the \"".concat(edge, "\" edge in the graph."));
+      var source = data.source.key;
+      var target = data.target.key;
+      if (node === source) return target;
+      if (node === target) return source;
+      throw new NotFoundGraphError("Graph.opposite: the \"".concat(node, "\" node is not attached to the \"").concat(edge, "\" edge (").concat(source, ", ").concat(target, ")."));
+    }
+    /**
+     * Returns whether the given edge has the given node as extremity.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @param  {any}     node - The node's key.
+     * @return {boolean}      - The related node.
+     *
+     * @throws {Error} - Will throw if either the node or the edge isn't in the graph.
+     */
+    ;
+
+    _proto.hasExtremity = function hasExtremity(edge, node) {
+      edge = '' + edge;
+      node = '' + node;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.hasExtremity: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source.key === node || data.target.key === node;
+    }
+    /**
+     * Method returning whether the given edge is undirected.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isUndirected = function isUndirected(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isUndirected: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.undirected;
+    }
+    /**
+     * Method returning whether the given edge is directed.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isDirected = function isDirected(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isDirected: could not find the \"".concat(edge, "\" edge in the graph."));
+      return !data.undirected;
+    }
+    /**
+     * Method returning whether the given edge is a self loop.
+     *
+     * @param  {any}     edge - The edge's key.
+     * @return {boolean}
+     *
+     * @throws {Error} - Will throw if the edge isn't in the graph.
+     */
+    ;
+
+    _proto.isSelfLoop = function isSelfLoop(edge) {
+      edge = '' + edge;
+
+      var data = this._edges.get(edge);
+
+      if (!data) throw new NotFoundGraphError("Graph.isSelfLoop: could not find the \"".concat(edge, "\" edge in the graph."));
+      return data.source === data.target;
+    }
+    /**---------------------------------------------------------------------------
+     * Mutation
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used to add a node to the graph.
+     *
+     * @param  {any}    node         - The node.
+     * @param  {object} [attributes] - Optional attributes.
+     * @return {any}                 - The node.
+     *
+     * @throws {Error} - Will throw if the given node already exist.
+     * @throws {Error} - Will throw if the given attributes are not an object.
+     */
+    ;
+
+    _proto.addNode = function addNode(node, attributes) {
+      var nodeData = _addNode(this, node, attributes);
+
+      return nodeData.key;
+    }
+    /**
+     * Method used to merge a node into the graph.
+     *
+     * @param  {any}    node         - The node.
+     * @param  {object} [attributes] - Optional attributes.
+     * @return {any}                 - The node.
+     */
+    ;
+
+    _proto.mergeNode = function mergeNode(node, attributes) {
+      if (attributes && !isPlainObject(attributes)) throw new InvalidArgumentsGraphError("Graph.mergeNode: invalid attributes. Expecting an object but got \"".concat(attributes, "\"")); // String coercion
+
+      node = '' + node;
+      attributes = attributes || {}; // If the node already exists, we merge the attributes
+
+      var data = this._nodes.get(node);
+
+      if (data) {
+        if (attributes) {
+          assign(data.attributes, attributes);
+          this.emit('nodeAttributesUpdated', {
+            type: 'merge',
+            key: node,
+            attributes: data.attributes,
+            data: attributes
+          });
+        }
+
+        return [node, false];
+      }
+
+      data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+      this._nodes.set(node, data); // Emitting
+
+
+      this.emit('nodeAdded', {
+        key: node,
+        attributes: attributes
+      });
+      return [node, true];
+    }
+    /**
+     * Method used to add a node if it does not exist in the graph or else to
+     * update its attributes using a function.
+     *
+     * @param  {any}      node      - The node.
+     * @param  {function} [updater] - Optional updater function.
+     * @return {any}                - The node.
+     */
+    ;
+
+    _proto.updateNode = function updateNode(node, updater) {
+      if (updater && typeof updater !== 'function') throw new InvalidArgumentsGraphError("Graph.updateNode: invalid updater function. Expecting a function but got \"".concat(updater, "\"")); // String coercion
+
+      node = '' + node; // If the node already exists, we update the attributes
+
+      var data = this._nodes.get(node);
+
+      if (data) {
+        if (updater) {
+          var oldAttributes = data.attributes;
+          data.attributes = updater(oldAttributes);
+          this.emit('nodeAttributesUpdated', {
+            type: 'replace',
+            key: node,
+            attributes: data.attributes
+          });
+        }
+
+        return [node, false];
+      }
+
+      var attributes = updater ? updater({}) : {};
+      data = new this.NodeDataClass(node, attributes); // Adding the node to internal register
+
+      this._nodes.set(node, data); // Emitting
+
+
+      this.emit('nodeAdded', {
+        key: node,
+        attributes: attributes
+      });
+      return [node, true];
+    }
+    /**
+     * Method used to drop a single node & all its attached edges from the graph.
+     *
+     * @param  {any}    node - The node.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the node doesn't exist.
+     */
+    ;
+
+    _proto.dropNode = function dropNode(node) {
+      node = '' + node;
+
+      var nodeData = this._nodes.get(node);
+
+      if (!nodeData) throw new NotFoundGraphError("Graph.dropNode: could not find the \"".concat(node, "\" node in the graph."));
+      var edgeData; // Removing attached edges
+      // NOTE: we could be faster here, but this is such a pain to maintain
+
+      if (this.type !== 'undirected') {
+        for (var neighbor in nodeData.out) {
+          edgeData = nodeData.out[neighbor];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+
+        for (var _neighbor in nodeData["in"]) {
+          edgeData = nodeData["in"][_neighbor];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      }
+
+      if (this.type !== 'directed') {
+        for (var _neighbor2 in nodeData.undirected) {
+          edgeData = nodeData.undirected[_neighbor2];
+
+          do {
+            dropEdgeFromData(this, edgeData);
+            edgeData = edgeData.next;
+          } while (edgeData);
+        }
+      } // Dropping the node from the register
+
+
+      this._nodes["delete"](node); // Emitting
+
+
+      this.emit('nodeDropped', {
+        key: node,
+        attributes: nodeData.attributes
+      });
+    }
+    /**
+     * Method used to drop a single edge from the graph.
+     *
+     * Arity 1:
+     * @param  {any}    edge - The edge.
+     *
+     * Arity 2:
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropEdge = function dropEdge(edge) {
+      var edgeData;
+
+      if (arguments.length > 1) {
+        var source = '' + arguments[0];
+        var target = '' + arguments[1];
+        edgeData = getMatchingEdge(this, source, target, this.type);
+        if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      } else {
+        edge = '' + edge;
+        edgeData = this._edges.get(edge);
+        if (!edgeData) throw new NotFoundGraphError("Graph.dropEdge: could not find the \"".concat(edge, "\" edge in the graph."));
+      }
+
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to drop a single directed edge from the graph.
+     *
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropDirectedEdge = function dropDirectedEdge(source, target) {
+      if (arguments.length < 2) throw new UsageGraphError('Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+      if (this.multi) throw new UsageGraphError('Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+      source = '' + source;
+      target = '' + target;
+      var edgeData = getMatchingEdge(this, source, target, 'directed');
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropDirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to drop a single undirected edge from the graph.
+     *
+     * @param  {any}    source - Source node.
+     * @param  {any}    target - Target node.
+     *
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if the edge doesn't exist.
+     */
+    ;
+
+    _proto.dropUndirectedEdge = function dropUndirectedEdge(source, target) {
+      if (arguments.length < 2) throw new UsageGraphError('Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.');
+      if (this.multi) throw new UsageGraphError('Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.');
+      var edgeData = getMatchingEdge(this, source, target, 'undirected');
+      if (!edgeData) throw new NotFoundGraphError("Graph.dropUndirectedEdge: could not find a \"".concat(source, "\" -> \"").concat(target, "\" edge in the graph."));
+      dropEdgeFromData(this, edgeData);
+      return this;
+    }
+    /**
+     * Method used to remove every edge & every node from the graph.
+     *
+     * @return {Graph}
+     */
+    ;
+
+    _proto.clear = function clear() {
+      // Clearing edges
+      this._edges.clear(); // Clearing nodes
+
+
+      this._nodes.clear(); // Reset counters
+
+
+      this._resetInstanceCounters(); // Emitting
+
+
+      this.emit('cleared');
+    }
+    /**
+     * Method used to remove every edge from the graph.
+     *
+     * @return {Graph}
+     */
+    ;
+
+    _proto.clearEdges = function clearEdges() {
+      // Clearing structure index
+      var iterator = this._nodes.values();
+
+      var step;
+
+      while (step = iterator.next(), step.done !== true) {
+        step.value.clear();
+      } // Clearing edges
+
+
+      this._edges.clear(); // Reset counters
+
+
+      this._resetInstanceCounters(); // Emitting
+
+
+      this.emit('edgesCleared');
+    }
+    /**---------------------------------------------------------------------------
+     * Attributes-related methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning the desired graph's attribute.
+     *
+     * @param  {string} name - Name of the attribute.
+     * @return {any}
+     */
+    ;
+
+    _proto.getAttribute = function getAttribute(name) {
+      return this._attributes[name];
+    }
+    /**
+     * Method returning the graph's attributes.
+     *
+     * @return {object}
+     */
+    ;
+
+    _proto.getAttributes = function getAttributes() {
+      return this._attributes;
+    }
+    /**
+     * Method returning whether the graph has the desired attribute.
+     *
+     * @param  {string}  name - Name of the attribute.
+     * @return {boolean}
+     */
+    ;
+
+    _proto.hasAttribute = function hasAttribute(name) {
+      return this._attributes.hasOwnProperty(name);
+    }
+    /**
+     * Method setting a value for the desired graph's attribute.
+     *
+     * @param  {string}  name  - Name of the attribute.
+     * @param  {any}     value - Value for the attribute.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.setAttribute = function setAttribute(name, value) {
+      this._attributes[name] = value; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'set',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method using a function to update the desired graph's attribute's value.
+     *
+     * @param  {string}   name    - Name of the attribute.
+     * @param  {function} updater - Function use to update the attribute's value.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.updateAttribute = function updateAttribute(name, updater) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttribute: updater should be a function.');
+      var value = this._attributes[name];
+      this._attributes[name] = updater(value); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'set',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method removing the desired graph's attribute.
+     *
+     * @param  {string} name  - Name of the attribute.
+     * @return {Graph}
+     */
+    ;
+
+    _proto.removeAttribute = function removeAttribute(name) {
+      delete this._attributes[name]; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'remove',
+        attributes: this._attributes,
+        name: name
+      });
+      return this;
+    }
+    /**
+     * Method replacing the graph's attributes.
+     *
+     * @param  {object} attributes - New attributes.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given attributes are not a plain object.
+     */
+    ;
+
+    _proto.replaceAttributes = function replaceAttributes(attributes) {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.replaceAttributes: provided attributes are not a plain object.');
+      this._attributes = attributes; // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'replace',
+        attributes: this._attributes
+      });
+      return this;
+    }
+    /**
+     * Method merging the graph's attributes.
+     *
+     * @param  {object} attributes - Attributes to merge.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given attributes are not a plain object.
+     */
+    ;
+
+    _proto.mergeAttributes = function mergeAttributes(attributes) {
+      if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError('Graph.mergeAttributes: provided attributes are not a plain object.');
+      assign(this._attributes, attributes); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'merge',
+        attributes: this._attributes,
+        data: attributes
+      });
+      return this;
+    }
+    /**
+     * Method updating the graph's attributes.
+     *
+     * @param  {function} updater - Function used to update the attributes.
+     * @return {Graph}
+     *
+     * @throws {Error} - Will throw if given updater is not a function.
+     */
+    ;
+
+    _proto.updateAttributes = function updateAttributes(updater) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateAttributes: provided updater is not a function.');
+      this._attributes = updater(this._attributes); // Emitting
+
+      this.emit('attributesUpdated', {
+        type: 'update',
+        attributes: this._attributes
+      });
+      return this;
+    }
+    /**
+     * Method used to update each node's attributes using the given function.
+     *
+     * @param {function}  updater - Updater function to use.
+     * @param {object}    [hints] - Optional hints.
+     */
+    ;
+
+    _proto.updateEachNodeAttributes = function updateEachNodeAttributes(updater, hints) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: expecting an updater function.');
+      if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        nodeData.attributes = updater(nodeData.key, nodeData.attributes);
+      }
+
+      this.emit('eachNodeAttributesUpdated', {
+        hints: hints ? hints : null
+      });
+    }
+    /**
+     * Method used to update each edge's attributes using the given function.
+     *
+     * @param {function}  updater - Updater function to use.
+     * @param {object}    [hints] - Optional hints.
+     */
+    ;
+
+    _proto.updateEachEdgeAttributes = function updateEachEdgeAttributes(updater, hints) {
+      if (typeof updater !== 'function') throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: expecting an updater function.');
+      if (hints && !validateHints(hints)) throw new InvalidArgumentsGraphError('Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}');
+
+      var iterator = this._edges.values();
+
+      var step, edgeData, sourceData, targetData;
+
+      while (step = iterator.next(), step.done !== true) {
+        edgeData = step.value;
+        sourceData = edgeData.source;
+        targetData = edgeData.target;
+        edgeData.attributes = updater(edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected);
+      }
+
+      this.emit('eachEdgeAttributesUpdated', {
+        hints: hints ? hints : null
+      });
+    }
+    /**---------------------------------------------------------------------------
+     * Iteration-related methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method iterating over the graph's adjacency using the given callback.
+     *
+     * @param  {function}  callback - Callback to use.
+     */
+    ;
+
+    _proto.forEachAdjacencyEntry = function forEachAdjacencyEntry(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntry: expecting a callback.');
+      forEachAdjacency(false, false, false, this, callback);
+    };
+
+    _proto.forEachAdjacencyEntryWithOrphans = function forEachAdjacencyEntryWithOrphans(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.');
+      forEachAdjacency(false, false, true, this, callback);
+    }
+    /**
+     * Method iterating over the graph's assymetric adjacency using the given callback.
+     *
+     * @param  {function}  callback - Callback to use.
+     */
+    ;
+
+    _proto.forEachAssymetricAdjacencyEntry = function forEachAssymetricAdjacencyEntry(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntry: expecting a callback.');
+      forEachAdjacency(false, true, false, this, callback);
+    };
+
+    _proto.forEachAssymetricAdjacencyEntryWithOrphans = function forEachAssymetricAdjacencyEntryWithOrphans(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.');
+      forEachAdjacency(false, true, true, this, callback);
+    }
+    /**
+     * Method returning the list of the graph's nodes.
+     *
+     * @return {array} - The nodes.
+     */
+    ;
+
+    _proto.nodes = function nodes() {
+      if (typeof Array.from === 'function') return Array.from(this._nodes.keys());
+      return take(this._nodes.keys(), this._nodes.size);
+    }
+    /**
+     * Method iterating over the graph's nodes using the given callback.
+     *
+     * @param  {function}  callback - Callback (key, attributes, index).
+     */
+    ;
+
+    _proto.forEachNode = function forEachNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.forEachNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        callback(nodeData.key, nodeData.attributes);
+      }
+    }
+    /**
+     * Method iterating attempting to find a node matching the given predicate
+     * function.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.findNode = function findNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.findNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) return nodeData.key;
+      }
+
+      return;
+    }
+    /**
+     * Method mapping nodes.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.mapNodes = function mapNodes(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.mapNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+      var result = new Array(this.order);
+      var i = 0;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        result[i++] = callback(nodeData.key, nodeData.attributes);
+      }
+
+      return result;
+    }
+    /**
+     * Method returning whether some node verify the given predicate.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.someNode = function someNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.someNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) return true;
+      }
+
+      return false;
+    }
+    /**
+     * Method returning whether all node verify the given predicate.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.everyNode = function everyNode(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.everyNode: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (!callback(nodeData.key, nodeData.attributes)) return false;
+      }
+
+      return true;
+    }
+    /**
+     * Method filtering nodes.
+     *
+     * @param  {function}  callback - Callback (key, attributes).
+     */
+    ;
+
+    _proto.filterNodes = function filterNodes(callback) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.filterNodes: expecting a callback.');
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+      var result = [];
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        if (callback(nodeData.key, nodeData.attributes)) result.push(nodeData.key);
+      }
+
+      return result;
+    }
+    /**
+     * Method reducing nodes.
+     *
+     * @param  {function}  callback - Callback (accumulator, key, attributes).
+     */
+    ;
+
+    _proto.reduceNodes = function reduceNodes(callback, initialValue) {
+      if (typeof callback !== 'function') throw new InvalidArgumentsGraphError('Graph.reduceNodes: expecting a callback.');
+      if (arguments.length < 2) throw new InvalidArgumentsGraphError('Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.');
+      var accumulator = initialValue;
+
+      var iterator = this._nodes.values();
+
+      var step, nodeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        nodeData = step.value;
+        accumulator = callback(accumulator, nodeData.key, nodeData.attributes);
+      }
+
+      return accumulator;
+    }
+    /**
+     * Method returning an iterator over the graph's node entries.
+     *
+     * @return {Iterator}
+     */
+    ;
+
+    _proto.nodeEntries = function nodeEntries() {
+      var iterator$1 = this._nodes.values();
+
+      return new iterator(function () {
+        var step = iterator$1.next();
+        if (step.done) return step;
+        var data = step.value;
+        return {
+          value: {
+            node: data.key,
+            attributes: data.attributes
+          },
+          done: false
+        };
+      });
+    }
+    /**---------------------------------------------------------------------------
+     * Serialization
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used to export the whole graph.
+     *
+     * @return {object} - The serialized graph.
+     */
+    ;
+
+    _proto["export"] = function _export() {
+      var nodes = new Array(this._nodes.size);
+      var i = 0;
+
+      this._nodes.forEach(function (data, key) {
+        nodes[i++] = serializeNode(key, data);
+      });
+
+      var edges = new Array(this._edges.size);
+      i = 0;
+
+      this._edges.forEach(function (data, key) {
+        edges[i++] = serializeEdge(key, data);
+      });
+
+      return {
+        options: {
+          type: this.type,
+          multi: this.multi,
+          allowSelfLoops: this.allowSelfLoops
+        },
+        attributes: this.getAttributes(),
+        nodes: nodes,
+        edges: edges
+      };
+    }
+    /**
+     * Method used to import a serialized graph.
+     *
+     * @param  {object|Graph} data  - The serialized graph.
+     * @param  {boolean}      merge - Whether to merge data.
+     * @return {Graph}              - Returns itself for chaining.
+     */
+    ;
+
+    _proto["import"] = function _import(data) {
+      var _this2 = this;
+
+      var merge = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+      // Importing a Graph instance directly
+      if (isGraph(data)) {
+        // Nodes
+        data.forEachNode(function (n, a) {
+          if (merge) _this2.mergeNode(n, a);else _this2.addNode(n, a);
+        }); // Edges
+
+        data.forEachEdge(function (e, a, s, t, _sa, _ta, u) {
+          if (merge) {
+            if (u) _this2.mergeUndirectedEdgeWithKey(e, s, t, a);else _this2.mergeDirectedEdgeWithKey(e, s, t, a);
+          } else {
+            if (u) _this2.addUndirectedEdgeWithKey(e, s, t, a);else _this2.addDirectedEdgeWithKey(e, s, t, a);
+          }
+        });
+        return this;
+      } // Importing a serialized graph
+
+
+      if (!isPlainObject(data)) throw new InvalidArgumentsGraphError('Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.');
+
+      if (data.attributes) {
+        if (!isPlainObject(data.attributes)) throw new InvalidArgumentsGraphError('Graph.import: invalid attributes. Expecting a plain object.');
+        if (merge) this.mergeAttributes(data.attributes);else this.replaceAttributes(data.attributes);
+      }
+
+      var i, l, list, node, edge;
+
+      if (data.nodes) {
+        list = data.nodes;
+        if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid nodes. Expecting an array.');
+
+        for (i = 0, l = list.length; i < l; i++) {
+          node = list[i]; // Validating
+
+          validateSerializedNode(node); // Adding the node
+
+          var _node = node,
+              key = _node.key,
+              attributes = _node.attributes;
+          if (merge) this.mergeNode(key, attributes);else this.addNode(key, attributes);
+        }
+      }
+
+      if (data.edges) {
+        list = data.edges;
+        if (!Array.isArray(list)) throw new InvalidArgumentsGraphError('Graph.import: invalid edges. Expecting an array.');
+
+        for (i = 0, l = list.length; i < l; i++) {
+          edge = list[i]; // Validating
+
+          validateSerializedEdge(edge); // Adding the edge
+
+          var _edge = edge,
+              source = _edge.source,
+              target = _edge.target,
+              _attributes = _edge.attributes,
+              _edge$undirected = _edge.undirected,
+              undirected = _edge$undirected === void 0 ? false : _edge$undirected;
+          var method = void 0;
+
+          if ('key' in edge) {
+            method = merge ? undirected ? this.mergeUndirectedEdgeWithKey : this.mergeDirectedEdgeWithKey : undirected ? this.addUndirectedEdgeWithKey : this.addDirectedEdgeWithKey;
+            method.call(this, edge.key, source, target, _attributes);
+          } else {
+            method = merge ? undirected ? this.mergeUndirectedEdge : this.mergeDirectedEdge : undirected ? this.addUndirectedEdge : this.addDirectedEdge;
+            method.call(this, source, target, _attributes);
+          }
+        }
+      }
+
+      return this;
+    }
+    /**---------------------------------------------------------------------------
+     * Utils
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method returning a null copy of the graph, i.e. a graph without nodes
+     * & edges but with the exact same options.
+     *
+     * @param  {object} options - Options to merge with the current ones.
+     * @return {Graph}          - The null copy.
+     */
+    ;
+
+    _proto.nullCopy = function nullCopy(options) {
+      var graph = new Graph(assign({}, this._options, options));
+      graph.replaceAttributes(assign({}, this.getAttributes()));
+      return graph;
+    }
+    /**
+     * Method returning an empty copy of the graph, i.e. a graph without edges but
+     * with the exact same options.
+     *
+     * @param  {object} options - Options to merge with the current ones.
+     * @return {Graph}          - The empty copy.
+     */
+    ;
+
+    _proto.emptyCopy = function emptyCopy(options) {
+      var graph = this.nullCopy(options);
+
+      this._nodes.forEach(function (nodeData, key) {
+        var attributes = assign({}, nodeData.attributes); // NOTE: no need to emit events since user cannot access the instance yet
+
+        nodeData = new graph.NodeDataClass(key, attributes);
+
+        graph._nodes.set(key, nodeData);
+      });
+
+      return graph;
+    }
+    /**
+     * Method returning an exact copy of the graph.
+     *
+     * @param  {object} options - Upgrade options.
+     * @return {Graph}          - The copy.
+     */
+    ;
+
+    _proto.copy = function copy(options) {
+      options = options || {};
+      if (typeof options.type === 'string' && options.type !== this.type && options.type !== 'mixed') throw new UsageGraphError("Graph.copy: cannot create an incompatible copy from \"".concat(this.type, "\" type to \"").concat(options.type, "\" because this would mean losing information about the current graph."));
+      if (typeof options.multi === 'boolean' && options.multi !== this.multi && options.multi !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.');
+      if (typeof options.allowSelfLoops === 'boolean' && options.allowSelfLoops !== this.allowSelfLoops && options.allowSelfLoops !== true) throw new UsageGraphError('Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.');
+      var graph = this.emptyCopy(options);
+
+      var iterator = this._edges.values();
+
+      var step, edgeData;
+
+      while (step = iterator.next(), step.done !== true) {
+        edgeData = step.value; // NOTE: no need to emit events since user cannot access the instance yet
+
+        addEdge(graph, 'copy', false, edgeData.undirected, edgeData.key, edgeData.source.key, edgeData.target.key, assign({}, edgeData.attributes));
+      }
+
+      return graph;
+    }
+    /**---------------------------------------------------------------------------
+     * Known methods
+     **---------------------------------------------------------------------------
+     */
+
+    /**
+     * Method used by JavaScript to perform JSON serialization.
+     *
+     * @return {object} - The serialized graph.
+     */
+    ;
+
+    _proto.toJSON = function toJSON() {
+      return this["export"]();
+    }
+    /**
+     * Method returning [object Graph].
+     */
+    ;
+
+    _proto.toString = function toString() {
+      return '[object Graph]';
+    }
+    /**
+     * Method used internally by node's console to display a custom object.
+     *
+     * @return {object} - Formatted object representation of the graph.
+     */
+    ;
+
+    _proto.inspect = function inspect() {
+      var _this3 = this;
+
+      var nodes = {};
+
+      this._nodes.forEach(function (data, key) {
+        nodes[key] = data.attributes;
+      });
+
+      var edges = {},
+          multiIndex = {};
+
+      this._edges.forEach(function (data, key) {
+        var direction = data.undirected ? '--' : '->';
+        var label = '';
+        var source = data.source.key;
+        var target = data.target.key;
+        var tmp;
+
+        if (data.undirected && source > target) {
+          tmp = source;
+          source = target;
+          target = tmp;
+        }
+
+        var desc = "(".concat(source, ")").concat(direction, "(").concat(target, ")");
+
+        if (!key.startsWith('geid_')) {
+          label += "[".concat(key, "]: ");
+        } else if (_this3.multi) {
+          if (typeof multiIndex[desc] === 'undefined') {
+            multiIndex[desc] = 0;
+          } else {
+            multiIndex[desc]++;
+          }
+
+          label += "".concat(multiIndex[desc], ". ");
+        }
+
+        label += desc;
+        edges[label] = data.attributes;
+      });
+
+      var dummy = {};
+
+      for (var k in this) {
+        if (this.hasOwnProperty(k) && !EMITTER_PROPS.has(k) && typeof this[k] !== 'function' && _typeof(k) !== 'symbol') dummy[k] = this[k];
+      }
+
+      dummy.attributes = this._attributes;
+      dummy.nodes = nodes;
+      dummy.edges = edges;
+      privateProperty(dummy, 'constructor', this.constructor);
+      return dummy;
+    };
+
+    return Graph;
+  }(events.exports.EventEmitter);
+  if (typeof Symbol !== 'undefined') Graph.prototype[Symbol["for"]('nodejs.util.inspect.custom')] = Graph.prototype.inspect;
+  /**
+   * Related to edge addition.
+   */
+
+  EDGE_ADD_METHODS.forEach(function (method) {
+    ['add', 'merge', 'update'].forEach(function (verb) {
+      var name = method.name(verb);
+      var fn = verb === 'add' ? addEdge : mergeEdge;
+
+      if (method.generateKey) {
+        Graph.prototype[name] = function (source, target, attributes) {
+          return fn(this, name, true, (method.type || this.type) === 'undirected', null, source, target, attributes, verb === 'update');
+        };
+      } else {
+        Graph.prototype[name] = function (edge, source, target, attributes) {
+          return fn(this, name, false, (method.type || this.type) === 'undirected', edge, source, target, attributes, verb === 'update');
+        };
+      }
+    });
+  });
+  /**
+   * Attributes-related.
+   */
+
+  attachNodeAttributesMethods(Graph);
+  attachEdgeAttributesMethods(Graph);
+  /**
+   * Edge iteration-related.
+   */
+
+  attachEdgeIterationMethods(Graph);
+  /**
+   * Neighbor iteration-related.
+   */
+
+  attachNeighborIterationMethods(Graph);
+
+  /**
+   * Alternative constructors.
+   */
+
+  var DirectedGraph = /*#__PURE__*/function (_Graph) {
+    _inheritsLoose(DirectedGraph, _Graph);
+
+    function DirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'directed'
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+      if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('DirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph.call(this, finalOptions) || this;
+    }
+
+    return DirectedGraph;
+  }(Graph);
+
+  var UndirectedGraph = /*#__PURE__*/function (_Graph2) {
+    _inheritsLoose(UndirectedGraph, _Graph2);
+
+    function UndirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'undirected'
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== false) throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!');
+      if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('UndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph2.call(this, finalOptions) || this;
+    }
+
+    return UndirectedGraph;
+  }(Graph);
+
+  var MultiGraph = /*#__PURE__*/function (_Graph3) {
+    _inheritsLoose(MultiGraph, _Graph3);
+
+    function MultiGraph(options) {
+      var finalOptions = assign({
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiGraph.from: inconsistent indication that the graph should be simple in given options!');
+      return _Graph3.call(this, finalOptions) || this;
+    }
+
+    return MultiGraph;
+  }(Graph);
+
+  var MultiDirectedGraph = /*#__PURE__*/function (_Graph4) {
+    _inheritsLoose(MultiDirectedGraph, _Graph4);
+
+    function MultiDirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'directed',
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+      if (finalOptions.type !== 'directed') throw new InvalidArgumentsGraphError('MultiDirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph4.call(this, finalOptions) || this;
+    }
+
+    return MultiDirectedGraph;
+  }(Graph);
+
+  var MultiUndirectedGraph = /*#__PURE__*/function (_Graph5) {
+    _inheritsLoose(MultiUndirectedGraph, _Graph5);
+
+    function MultiUndirectedGraph(options) {
+      var finalOptions = assign({
+        type: 'undirected',
+        multi: true
+      }, options);
+      if ('multi' in finalOptions && finalOptions.multi !== true) throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!');
+      if (finalOptions.type !== 'undirected') throw new InvalidArgumentsGraphError('MultiUndirectedGraph.from: inconsistent "' + finalOptions.type + '" type in given options!');
+      return _Graph5.call(this, finalOptions) || this;
+    }
+
+    return MultiUndirectedGraph;
+  }(Graph);
+  /**
+   * Attaching static #.from method to each of the constructors.
+   */
+
+
+  function attachStaticFromMethod(Class) {
+    /**
+     * Builds a graph from serialized data or another graph's data.
+     *
+     * @param  {Graph|SerializedGraph} data      - Hydratation data.
+     * @param  {object}                [options] - Options.
+     * @return {Class}
+     */
+    Class.from = function (data, options) {
+      // Merging given options with serialized ones
+      var finalOptions = assign({}, data.options, options);
+      var instance = new Class(finalOptions);
+      instance["import"](data);
+      return instance;
+    };
+  }
+
+  attachStaticFromMethod(Graph);
+  attachStaticFromMethod(DirectedGraph);
+  attachStaticFromMethod(UndirectedGraph);
+  attachStaticFromMethod(MultiGraph);
+  attachStaticFromMethod(MultiDirectedGraph);
+  attachStaticFromMethod(MultiUndirectedGraph);
+  Graph.Graph = Graph;
+  Graph.DirectedGraph = DirectedGraph;
+  Graph.UndirectedGraph = UndirectedGraph;
+  Graph.MultiGraph = MultiGraph;
+  Graph.MultiDirectedGraph = MultiDirectedGraph;
+  Graph.MultiUndirectedGraph = MultiUndirectedGraph;
+  Graph.InvalidArgumentsGraphError = InvalidArgumentsGraphError;
+  Graph.NotFoundGraphError = NotFoundGraphError;
+  Graph.UsageGraphError = UsageGraphError;
+
+  /**
+   * Graphology CommonJS Endoint
+   * ============================
+   *
+   * Endpoint for CommonJS modules consumers.
+   */
+
+  return Graph;
+
+}));
+//# sourceMappingURL=graphology.umd.js.map
diff --git a/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.min.js b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..9cb5b8ff230cba8bec64c51bb01df2574dae2cb7
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/dist/graphology.umd.min.js
@@ -0,0 +1,2 @@
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).graphology=e()}(this,(function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,r(t,e)}function n(t){return n=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)},n(t)}function r(t,e){return r=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},r(t,e)}function i(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}function o(t,e,n){return o=i()?Reflect.construct:function(t,e,n){var i=[null];i.push.apply(i,e);var o=new(Function.bind.apply(t,i));return n&&r(o,n.prototype),o},o.apply(null,arguments)}function a(t){var e="function"==typeof Map?new Map:void 0;return a=function(t){if(null===t||(i=t,-1===Function.toString.call(i).indexOf("[native code]")))return t;var i;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,a)}function a(){return o(t,arguments,n(this).constructor)}return a.prototype=Object.create(t.prototype,{constructor:{value:a,enumerable:!1,writable:!0,configurable:!0}}),r(a,t)},a(t)}function u(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}var c=function(){for(var t=arguments[0],e=1,n=arguments.length;e<n;e++)if(arguments[e])for(var r in arguments[e])t[r]=arguments[e][r];return t};function s(t,e,n,r){var i=t._nodes.get(e),o=null;return i?o="mixed"===r?i.out&&i.out[n]||i.undirected&&i.undirected[n]:"directed"===r?i.out&&i.out[n]:i.undirected&&i.undirected[n]:o}function d(e){return null!==e&&"object"===t(e)&&"function"==typeof e.addUndirectedEdgeWithKey&&"function"==typeof e.dropNode}function h(e){return"object"===t(e)&&null!==e&&e.constructor===Object}function p(t){var e;for(e in t)return!1;return!0}function f(t,e,n){Object.defineProperty(t,e,{enumerable:!1,configurable:!1,writable:!0,value:n})}function l(t,e,n){var r={enumerable:!0,configurable:!0};"function"==typeof n?r.get=n:(r.value=n,r.writable=!1),Object.defineProperty(t,e,r)}function g(t){return!!h(t)&&!(t.attributes&&!Array.isArray(t.attributes))}"function"==typeof Object.assign&&(c=Object.assign);var y,w={exports:{}},v="object"==typeof Reflect?Reflect:null,b=v&&"function"==typeof v.apply?v.apply:function(t,e,n){return Function.prototype.apply.call(t,e,n)};y=v&&"function"==typeof v.ownKeys?v.ownKeys:Object.getOwnPropertySymbols?function(t){return Object.getOwnPropertyNames(t).concat(Object.getOwnPropertySymbols(t))}:function(t){return Object.getOwnPropertyNames(t)};var m=Number.isNaN||function(t){return t!=t};function k(){k.init.call(this)}w.exports=k,w.exports.once=function(t,e){return new Promise((function(n,r){function i(n){t.removeListener(e,o),r(n)}function o(){"function"==typeof t.removeListener&&t.removeListener("error",i),n([].slice.call(arguments))}N(t,e,o,{once:!0}),"error"!==e&&function(t,e,n){"function"==typeof t.on&&N(t,"error",e,n)}(t,i,{once:!0})}))},k.EventEmitter=k,k.prototype._events=void 0,k.prototype._eventsCount=0,k.prototype._maxListeners=void 0;var _=10;function G(t){if("function"!=typeof t)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof t)}function x(t){return void 0===t._maxListeners?k.defaultMaxListeners:t._maxListeners}function E(t,e,n,r){var i,o,a,u;if(G(n),void 0===(o=t._events)?(o=t._events=Object.create(null),t._eventsCount=0):(void 0!==o.newListener&&(t.emit("newListener",e,n.listener?n.listener:n),o=t._events),a=o[e]),void 0===a)a=o[e]=n,++t._eventsCount;else if("function"==typeof a?a=o[e]=r?[n,a]:[a,n]:r?a.unshift(n):a.push(n),(i=x(t))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(e)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=t,c.type=e,c.count=a.length,u=c,console&&console.warn&&console.warn(u)}return t}function A(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function S(t,e,n){var r={fired:!1,wrapFn:void 0,target:t,type:e,listener:n},i=A.bind(r);return i.listener=n,r.wrapFn=i,i}function D(t,e,n){var r=t._events;if(void 0===r)return[];var i=r[e];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(t){for(var e=new Array(t.length),n=0;n<e.length;++n)e[n]=t[n].listener||t[n];return e}(i):U(i,i.length)}function L(t){var e=this._events;if(void 0!==e){var n=e[t];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function U(t,e){for(var n=new Array(e),r=0;r<e;++r)n[r]=t[r];return n}function N(t,e,n,r){if("function"==typeof t.on)r.once?t.once(e,n):t.on(e,n);else{if("function"!=typeof t.addEventListener)throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type '+typeof t);t.addEventListener(e,(function i(o){r.once&&t.removeEventListener(e,i),n(o)}))}}function j(t){if("function"!=typeof t)throw new Error("obliterator/iterator: expecting a function!");this.next=t}Object.defineProperty(k,"defaultMaxListeners",{enumerable:!0,get:function(){return _},set:function(t){if("number"!=typeof t||t<0||m(t))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+t+".");_=t}}),k.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},k.prototype.setMaxListeners=function(t){if("number"!=typeof t||t<0||m(t))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+t+".");return this._maxListeners=t,this},k.prototype.getMaxListeners=function(){return x(this)},k.prototype.emit=function(t){for(var e=[],n=1;n<arguments.length;n++)e.push(arguments[n]);var r="error"===t,i=this._events;if(void 0!==i)r=r&&void 0===i.error;else if(!r)return!1;if(r){var o;if(e.length>0&&(o=e[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var u=i[t];if(void 0===u)return!1;if("function"==typeof u)b(u,this,e);else{var c=u.length,s=U(u,c);for(n=0;n<c;++n)b(s[n],this,e)}return!0},k.prototype.addListener=function(t,e){return E(this,t,e,!1)},k.prototype.on=k.prototype.addListener,k.prototype.prependListener=function(t,e){return E(this,t,e,!0)},k.prototype.once=function(t,e){return G(e),this.on(t,S(this,t,e)),this},k.prototype.prependOnceListener=function(t,e){return G(e),this.prependListener(t,S(this,t,e)),this},k.prototype.removeListener=function(t,e){var n,r,i,o,a;if(G(e),void 0===(r=this._events))return this;if(void 0===(n=r[t]))return this;if(n===e||n.listener===e)0==--this._eventsCount?this._events=Object.create(null):(delete r[t],r.removeListener&&this.emit("removeListener",t,n.listener||e));else if("function"!=typeof n){for(i=-1,o=n.length-1;o>=0;o--)if(n[o]===e||n[o].listener===e){a=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():function(t,e){for(;e+1<t.length;e++)t[e]=t[e+1];t.pop()}(n,i),1===n.length&&(r[t]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",t,a||e)}return this},k.prototype.off=k.prototype.removeListener,k.prototype.removeAllListeners=function(t){var e,n,r;if(void 0===(n=this._events))return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[t]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[t]),this;if(0===arguments.length){var i,o=Object.keys(n);for(r=0;r<o.length;++r)"removeListener"!==(i=o[r])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(e=n[t]))this.removeListener(t,e);else if(void 0!==e)for(r=e.length-1;r>=0;r--)this.removeListener(t,e[r]);return this},k.prototype.listeners=function(t){return D(this,t,!0)},k.prototype.rawListeners=function(t){return D(this,t,!1)},k.listenerCount=function(t,e){return"function"==typeof t.listenerCount?t.listenerCount(e):L.call(t,e)},k.prototype.listenerCount=L,k.prototype.eventNames=function(){return this._eventsCount>0?y(this._events):[]},"undefined"!=typeof Symbol&&(j.prototype[Symbol.iterator]=function(){return this}),j.of=function(){var t=arguments,e=t.length,n=0;return new j((function(){return n>=e?{done:!0}:{done:!1,value:t[n++]}}))},j.empty=function(){return new j((function(){return{done:!0}}))},j.fromSequence=function(t){var e=0,n=t.length;return new j((function(){return e>=n?{done:!0}:{done:!1,value:t[e++]}}))},j.is=function(t){return t instanceof j||"object"==typeof t&&null!==t&&"function"==typeof t.next};var O=j,C={};C.ARRAY_BUFFER_SUPPORT="undefined"!=typeof ArrayBuffer,C.SYMBOL_SUPPORT="undefined"!=typeof Symbol;var z=O,M=C,W=M.ARRAY_BUFFER_SUPPORT,P=M.SYMBOL_SUPPORT;var R=function(t){var e=function(t){return"string"==typeof t||Array.isArray(t)||W&&ArrayBuffer.isView(t)?z.fromSequence(t):"object"!=typeof t||null===t?null:P&&"function"==typeof t[Symbol.iterator]?t[Symbol.iterator]():"function"==typeof t.next?t:null}(t);if(!e)throw new Error("obliterator: target is not iterable nor a valid iterator.");return e},K=R,T=function(t,e){for(var n,r=arguments.length>1?e:1/0,i=r!==1/0?new Array(r):[],o=0,a=K(t);;){if(o===r)return i;if((n=a.next()).done)return o!==e&&(i.length=o),i;i[o++]=n.value}},B=function(t){function n(e){var n;return(n=t.call(this)||this).name="GraphError",n.message=e,n}return e(n,t),n}(a(Error)),F=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="InvalidArgumentsGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B),I=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="NotFoundGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B),Y=function(t){function n(e){var r;return(r=t.call(this,e)||this).name="UsageGraphError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(u(r),n.prototype.constructor),r}return e(n,t),n}(B);function q(t,e){this.key=t,this.attributes=e,this.clear()}function J(t,e){this.key=t,this.attributes=e,this.clear()}function V(t,e){this.key=t,this.attributes=e,this.clear()}function H(t,e,n,r,i){this.key=e,this.attributes=i,this.undirected=t,this.source=n,this.target=r}q.prototype.clear=function(){this.inDegree=0,this.outDegree=0,this.undirectedDegree=0,this.in={},this.out={},this.undirected={}},J.prototype.clear=function(){this.inDegree=0,this.outDegree=0,this.in={},this.out={}},V.prototype.clear=function(){this.undirectedDegree=0,this.undirected={}},H.prototype.attach=function(){var t="out",e="in";this.undirected&&(t=e="undirected");var n=this.source.key,r=this.target.key;this.source[t][r]=this,this.undirected&&n===r||(this.target[e][n]=this)},H.prototype.attachMulti=function(){var t="out",e="in",n=this.source.key,r=this.target.key;this.undirected&&(t=e="undirected");var i=this.source[t],o=i[r];if(void 0===o)return i[r]=this,void(this.undirected&&n===r||(this.target[e][n]=this));o.previous=this,this.next=o,i[r]=this,this.target[e][n]=this},H.prototype.detach=function(){var t=this.source.key,e=this.target.key,n="out",r="in";this.undirected&&(n=r="undirected"),delete this.source[n][e],delete this.target[r][t]},H.prototype.detachMulti=function(){var t=this.source.key,e=this.target.key,n="out",r="in";this.undirected&&(n=r="undirected"),void 0===this.previous?void 0===this.next?(delete this.source[n][e],delete this.target[r][t]):(this.next.previous=void 0,this.source[n][e]=this.next,this.target[r][t]=this.next):(this.previous.next=this.next,void 0!==this.next&&(this.next.previous=this.previous))};function Q(t,e,n,r,i,o,a){var u,c,s,d;if(r=""+r,0===n){if(!(u=t._nodes.get(r)))throw new I("Graph.".concat(e,': could not find the "').concat(r,'" node in the graph.'));s=i,d=o}else if(3===n){if(i=""+i,!(c=t._edges.get(i)))throw new I("Graph.".concat(e,': could not find the "').concat(i,'" edge in the graph.'));var h=c.source.key,p=c.target.key;if(r===h)u=c.target;else{if(r!==p)throw new I("Graph.".concat(e,': the "').concat(r,'" node is not attached to the "').concat(i,'" edge (').concat(h,", ").concat(p,")."));u=c.source}s=o,d=a}else{if(!(c=t._edges.get(r)))throw new I("Graph.".concat(e,': could not find the "').concat(r,'" edge in the graph.'));u=1===n?c.source:c.target,s=i,d=o}return[u,s,d]}var X=[{name:function(t){return"get".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return a.attributes[u]}}},{name:function(t){return"get".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){return Q(this,e,n,t,r)[0].attributes}}},{name:function(t){return"has".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return a.attributes.hasOwnProperty(u)}}},{name:function(t){return"set".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i,o){var a=Q(this,e,n,t,r,i,o),u=a[0],c=a[1],s=a[2];return u.attributes[c]=s,this.emit("nodeAttributesUpdated",{key:u.key,type:"set",attributes:u.attributes,name:c}),this}}},{name:function(t){return"update".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i,o){var a=Q(this,e,n,t,r,i,o),u=a[0],c=a[1],s=a[2];if("function"!=typeof s)throw new F("Graph.".concat(e,": updater should be a function."));var d=u.attributes,h=s(d[c]);return d[c]=h,this.emit("nodeAttributesUpdated",{key:u.key,type:"set",attributes:u.attributes,name:c}),this}}},{name:function(t){return"remove".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];return delete a.attributes[u],this.emit("nodeAttributesUpdated",{key:a.key,type:"remove",attributes:a.attributes,name:u}),this}}},{name:function(t){return"replace".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if(!h(u))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return a.attributes=u,this.emit("nodeAttributesUpdated",{key:a.key,type:"replace",attributes:a.attributes}),this}}},{name:function(t){return"merge".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if(!h(u))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return c(a.attributes,u),this.emit("nodeAttributesUpdated",{key:a.key,type:"merge",attributes:a.attributes,data:u}),this}}},{name:function(t){return"update".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o=Q(this,e,n,t,r,i),a=o[0],u=o[1];if("function"!=typeof u)throw new F("Graph.".concat(e,": provided updater is not a function."));return a.attributes=u(a.attributes),this.emit("nodeAttributesUpdated",{key:a.key,type:"update",attributes:a.attributes}),this}}}];var Z=[{name:function(t){return"get".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return i.attributes[r]}}},{name:function(t){return"get".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t){var r;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>1){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var i=""+t,o=""+arguments[1];if(!(r=s(this,i,o,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(i,'" - "').concat(o,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(r=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return r.attributes}}},{name:function(t){return"has".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return i.attributes.hasOwnProperty(r)}}},{name:function(t){return"set".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>3){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var a=""+t,u=""+r;if(r=arguments[2],i=arguments[3],!(o=s(this,a,u,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(a,'" - "').concat(u,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(o=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return o.attributes[r]=i,this.emit("edgeAttributesUpdated",{key:o.key,type:"set",attributes:o.attributes,name:r}),this}}},{name:function(t){return"update".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r,i){var o;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>3){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var a=""+t,u=""+r;if(r=arguments[2],i=arguments[3],!(o=s(this,a,u,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(a,'" - "').concat(u,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(o=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if("function"!=typeof i)throw new F("Graph.".concat(e,": updater should be a function."));return o.attributes[r]=i(o.attributes[r]),this.emit("edgeAttributesUpdated",{key:o.key,type:"set",attributes:o.attributes,name:r}),this}}},{name:function(t){return"remove".concat(t,"Attribute")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}return delete i.attributes[r],this.emit("edgeAttributesUpdated",{key:i.key,type:"remove",attributes:i.attributes,name:r}),this}}},{name:function(t){return"replace".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if(!h(r))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return i.attributes=r,this.emit("edgeAttributesUpdated",{key:i.key,type:"replace",attributes:i.attributes}),this}}},{name:function(t){return"merge".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if(!h(r))throw new F("Graph.".concat(e,": provided attributes are not a plain object."));return c(i.attributes,r),this.emit("edgeAttributesUpdated",{key:i.key,type:"merge",attributes:i.attributes,data:r}),this}}},{name:function(t){return"update".concat(t,"Attributes")},attacher:function(t,e,n){t.prototype[e]=function(t,r){var i;if("mixed"!==this.type&&"mixed"!==n&&n!==this.type)throw new Y("Graph.".concat(e,": cannot find this type of edges in your ").concat(this.type," graph."));if(arguments.length>2){if(this.multi)throw new Y("Graph.".concat(e,": cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about."));var o=""+t,a=""+r;if(r=arguments[2],!(i=s(this,o,a,n)))throw new I("Graph.".concat(e,': could not find an edge for the given path ("').concat(o,'" - "').concat(a,'").'))}else{if("mixed"!==n)throw new Y("Graph.".concat(e,": calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type."));if(t=""+t,!(i=this._edges.get(t)))throw new I("Graph.".concat(e,': could not find the "').concat(t,'" edge in the graph.'))}if("function"!=typeof r)throw new F("Graph.".concat(e,": provided updater is not a function."));return i.attributes=r(i.attributes),this.emit("edgeAttributesUpdated",{key:i.key,type:"update",attributes:i.attributes}),this}}}];var $=O,tt=R,et=function(){var t=arguments,e=null,n=-1;return new $((function(){for(var r=null;;){if(null===e){if(++n>=t.length)return{done:!0};e=tt(t[n])}if(!0!==(r=e.next()).done)break;e=null}return r}))},nt=[{name:"edges",type:"mixed"},{name:"inEdges",type:"directed",direction:"in"},{name:"outEdges",type:"directed",direction:"out"},{name:"inboundEdges",type:"mixed",direction:"in"},{name:"outboundEdges",type:"mixed",direction:"out"},{name:"directedEdges",type:"directed"},{name:"undirectedEdges",type:"undirected"}];function rt(t,e,n,r){var i=!1;for(var o in e)if(o!==r){var a=e[o];if(i=n(a.key,a.attributes,a.source.key,a.target.key,a.source.attributes,a.target.attributes,a.undirected),t&&i)return a.key}}function it(t,e,n,r){var i,o,a,u=!1;for(var c in e)if(c!==r){i=e[c];do{if(o=i.source,a=i.target,u=n(i.key,i.attributes,o.key,a.key,o.attributes,a.attributes,i.undirected),t&&u)return i.key;i=i.next}while(void 0!==i)}}function ot(t,e){var n,r=Object.keys(t),i=r.length,o=0;return new O((function(){do{if(n)n=n.next;else{if(o>=i)return{done:!0};var a=r[o++];if(a===e){n=void 0;continue}n=t[a]}}while(!n);return{done:!1,value:{edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected}}}))}function at(t,e,n,r){var i=e[n];if(i){var o=i.source,a=i.target;return r(i.key,i.attributes,o.key,a.key,o.attributes,a.attributes,i.undirected)&&t?i.key:void 0}}function ut(t,e,n,r){var i=e[n];if(i){var o=!1;do{if(o=r(i.key,i.attributes,i.source.key,i.target.key,i.source.attributes,i.target.attributes,i.undirected),t&&o)return i.key;i=i.next}while(void 0!==i)}}function ct(t,e){var n=t[e];return void 0!==n.next?new O((function(){if(!n)return{done:!0};var t={edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected};return n=n.next,{done:!1,value:t}})):O.of({edge:n.key,attributes:n.attributes,source:n.source.key,target:n.target.key,sourceAttributes:n.source.attributes,targetAttributes:n.target.attributes,undirected:n.undirected})}function st(t,e){if(0===t.size)return[];if("mixed"===e||e===t.type)return"function"==typeof Array.from?Array.from(t._edges.keys()):T(t._edges.keys(),t._edges.size);for(var n,r,i="undirected"===e?t.undirectedSize:t.directedSize,o=new Array(i),a="undirected"===e,u=t._edges.values(),c=0;!0!==(n=u.next()).done;)(r=n.value).undirected===a&&(o[c++]=r.key);return o}function dt(t,e,n,r){if(0!==e.size)for(var i,o,a="mixed"!==n&&n!==e.type,u="undirected"===n,c=!1,s=e._edges.values();!0!==(i=s.next()).done;)if(o=i.value,!a||o.undirected===u){var d=o,h=d.key,p=d.attributes,f=d.source,l=d.target;if(c=r(h,p,f.key,l.key,f.attributes,l.attributes,o.undirected),t&&c)return h}}function ht(t,e){if(0===t.size)return O.empty();var n="mixed"!==e&&e!==t.type,r="undirected"===e,i=t._edges.values();return new O((function(){for(var t,e;;){if((t=i.next()).done)return t;if(e=t.value,!n||e.undirected===r)break}return{value:{edge:e.key,attributes:e.attributes,source:e.source.key,target:e.target.key,sourceAttributes:e.source.attributes,targetAttributes:e.target.attributes,undirected:e.undirected},done:!1}}))}function pt(t,e,n,r,i,o){var a,u=e?it:rt;if("undirected"!==n){if("out"!==r&&(a=u(t,i.in,o),t&&a))return a;if("in"!==r&&(a=u(t,i.out,o,r?void 0:i.key),t&&a))return a}if("directed"!==n&&(a=u(t,i.undirected,o),t&&a))return a}function ft(t,e,n,r){var i=[];return pt(!1,t,e,n,r,(function(t){i.push(t)})),i}function lt(t,e,n){var r=O.empty();return"undirected"!==t&&("out"!==e&&void 0!==n.in&&(r=et(r,ot(n.in))),"in"!==e&&void 0!==n.out&&(r=et(r,ot(n.out,e?void 0:n.key)))),"directed"!==t&&void 0!==n.undirected&&(r=et(r,ot(n.undirected))),r}function gt(t,e,n,r,i,o,a){var u,c=n?ut:at;if("undirected"!==e){if(void 0!==i.in&&"out"!==r&&(u=c(t,i.in,o,a),t&&u))return u;if(void 0!==i.out&&"in"!==r&&(r||i.key!==o)&&(u=c(t,i.out,o,a),t&&u))return u}if("directed"!==e&&void 0!==i.undirected&&(u=c(t,i.undirected,o,a),t&&u))return u}function yt(t,e,n,r,i){var o=[];return gt(!1,t,e,n,r,i,(function(t){o.push(t)})),o}function wt(t,e,n,r){var i=O.empty();return"undirected"!==t&&(void 0!==n.in&&"out"!==e&&r in n.in&&(i=et(i,ct(n.in,r))),void 0!==n.out&&"in"!==e&&r in n.out&&(e||n.key!==r)&&(i=et(i,ct(n.out,r)))),"directed"!==t&&void 0!==n.undirected&&r in n.undirected&&(i=et(i,ct(n.undirected,r))),i}var vt=[{name:"neighbors",type:"mixed"},{name:"inNeighbors",type:"directed",direction:"in"},{name:"outNeighbors",type:"directed",direction:"out"},{name:"inboundNeighbors",type:"mixed",direction:"in"},{name:"outboundNeighbors",type:"mixed",direction:"out"},{name:"directedNeighbors",type:"directed"},{name:"undirectedNeighbors",type:"undirected"}];function bt(){this.A=null,this.B=null}function mt(t,e,n,r,i){for(var o in r){var a=r[o],u=a.source,c=a.target,s=u===n?c:u;if(!e||!e.has(s.key)){var d=i(s.key,s.attributes);if(t&&d)return s.key}}}function kt(t,e,n,r,i){if("mixed"!==e){if("undirected"===e)return mt(t,null,r,r.undirected,i);if("string"==typeof n)return mt(t,null,r,r[n],i)}var o,a=new bt;if("undirected"!==e){if("out"!==n){if(o=mt(t,null,r,r.in,i),t&&o)return o;a.wrap(r.in)}if("in"!==n){if(o=mt(t,a,r,r.out,i),t&&o)return o;a.wrap(r.out)}}if("directed"!==e&&(o=mt(t,a,r,r.undirected,i),t&&o))return o}function _t(t,e,n){var r=Object.keys(n),i=r.length,o=0;return new O((function(){var a=null;do{if(o>=i)return t&&t.wrap(n),{done:!0};var u=n[r[o++]],c=u.source,s=u.target;a=c===e?s:c,t&&t.has(a.key)&&(a=null)}while(null===a);return{done:!1,value:{neighbor:a.key,attributes:a.attributes}}}))}function Gt(t,e){var n=e.name,r=e.type,i=e.direction;t.prototype[n]=function(t){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return[];t=""+t;var e=this._nodes.get(t);if(void 0===e)throw new I("Graph.".concat(n,': could not find the "').concat(t,'" node in the graph.'));return function(t,e,n){if("mixed"!==t){if("undirected"===t)return Object.keys(n.undirected);if("string"==typeof e)return Object.keys(n[e])}var r=[];return kt(!1,t,e,n,(function(t){r.push(t)})),r}("mixed"===r?this.type:r,i,e)}}function xt(t,e){var n=e.name,r=e.type,i=e.direction,o=n.slice(0,-1)+"Entries";t.prototype[o]=function(t){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return O.empty();t=""+t;var e=this._nodes.get(t);if(void 0===e)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return function(t,e,n){if("mixed"!==t){if("undirected"===t)return _t(null,n,n.undirected);if("string"==typeof e)return _t(null,n,n[e])}var r=O.empty(),i=new bt;return"undirected"!==t&&("out"!==e&&(r=et(r,_t(i,n,n.in))),"in"!==e&&(r=et(r,_t(i,n,n.out)))),"directed"!==t&&(r=et(r,_t(i,n,n.undirected))),r}("mixed"===r?this.type:r,i,e)}}function Et(t,e,n,r,i){for(var o,a,u,c,s,d,h,p=r._nodes.values(),f=r.type;!0!==(o=p.next()).done;){var l=!1;if(a=o.value,"undirected"!==f)for(u in c=a.out){s=c[u];do{if(d=s.target,l=!0,h=i(a.key,d.key,a.attributes,d.attributes,s.key,s.attributes,s.undirected),t&&h)return s;s=s.next}while(s)}if("directed"!==f)for(u in c=a.undirected)if(!(e&&a.key>u)){s=c[u];do{if((d=s.target).key!==u&&(d=s.source),l=!0,h=i(a.key,d.key,a.attributes,d.attributes,s.key,s.attributes,s.undirected),t&&h)return s;s=s.next}while(s)}if(n&&!l&&(h=i(a.key,null,a.attributes,null,null,null,null),t&&h))return null}}function At(t){if(!h(t))throw new F('Graph.import: invalid serialized node. A serialized node should be a plain object with at least a "key" property.');if(!("key"in t))throw new F("Graph.import: serialized node is missing its key.");if("attributes"in t&&(!h(t.attributes)||null===t.attributes))throw new F("Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.")}function St(t){if(!h(t))throw new F('Graph.import: invalid serialized edge. A serialized edge should be a plain object with at least a "source" & "target" property.');if(!("source"in t))throw new F("Graph.import: serialized edge is missing its source.");if(!("target"in t))throw new F("Graph.import: serialized edge is missing its target.");if("attributes"in t&&(!h(t.attributes)||null===t.attributes))throw new F("Graph.import: invalid attributes. Attributes should be a plain object, null or omitted.");if("undirected"in t&&"boolean"!=typeof t.undirected)throw new F("Graph.import: invalid undirectedness information. Undirected should be boolean or omitted.")}bt.prototype.wrap=function(t){null===this.A?this.A=t:null===this.B&&(this.B=t)},bt.prototype.has=function(t){return null!==this.A&&t in this.A||null!==this.B&&t in this.B};var Dt,Lt=(Dt=255&Math.floor(256*Math.random()),function(){return Dt++}),Ut=new Set(["directed","undirected","mixed"]),Nt=new Set(["domain","_events","_eventsCount","_maxListeners"]),jt={allowSelfLoops:!0,multi:!1,type:"mixed"};function Ot(t,e,n){var r=new t.NodeDataClass(e,n);return t._nodes.set(e,r),t.emit("nodeAdded",{key:e,attributes:n}),r}function Ct(t,e,n,r,i,o,a,u){if(!r&&"undirected"===t.type)throw new Y("Graph.".concat(e,": you cannot add a directed edge to an undirected graph. Use the #.addEdge or #.addUndirectedEdge instead."));if(r&&"directed"===t.type)throw new Y("Graph.".concat(e,": you cannot add an undirected edge to a directed graph. Use the #.addEdge or #.addDirectedEdge instead."));if(u&&!h(u))throw new F("Graph.".concat(e,': invalid attributes. Expecting an object but got "').concat(u,'"'));if(o=""+o,a=""+a,u=u||{},!t.allowSelfLoops&&o===a)throw new Y("Graph.".concat(e,': source & target are the same ("').concat(o,"\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));var c=t._nodes.get(o),s=t._nodes.get(a);if(!c)throw new I("Graph.".concat(e,': source node "').concat(o,'" not found.'));if(!s)throw new I("Graph.".concat(e,': target node "').concat(a,'" not found.'));var d={key:null,undirected:r,source:o,target:a,attributes:u};if(n)i=t._edgeKeyGenerator();else if(i=""+i,t._edges.has(i))throw new Y("Graph.".concat(e,': the "').concat(i,'" edge already exists in the graph.'));if(!t.multi&&(r?void 0!==c.undirected[a]:void 0!==c.out[a]))throw new Y("Graph.".concat(e,': an edge linking "').concat(o,'" to "').concat(a,"\" already exists. If you really want to add multiple edges linking those nodes, you should create a multi graph by using the 'multi' option."));var p=new H(r,i,c,s,u);t._edges.set(i,p);var f=o===a;return r?(c.undirectedDegree++,s.undirectedDegree++,f&&t._undirectedSelfLoopCount++):(c.outDegree++,s.inDegree++,f&&t._directedSelfLoopCount++),t.multi?p.attachMulti():p.attach(),r?t._undirectedSize++:t._directedSize++,d.key=i,t.emit("edgeAdded",d),i}function zt(t,e,n,r,i,o,a,u,s){if(!r&&"undirected"===t.type)throw new Y("Graph.".concat(e,": you cannot merge/update a directed edge to an undirected graph. Use the #.mergeEdge/#.updateEdge or #.addUndirectedEdge instead."));if(r&&"directed"===t.type)throw new Y("Graph.".concat(e,": you cannot merge/update an undirected edge to a directed graph. Use the #.mergeEdge/#.updateEdge or #.addDirectedEdge instead."));if(u)if(s){if("function"!=typeof u)throw new F("Graph.".concat(e,': invalid updater function. Expecting a function but got "').concat(u,'"'))}else if(!h(u))throw new F("Graph.".concat(e,': invalid attributes. Expecting an object but got "').concat(u,'"'));var d;if(o=""+o,a=""+a,s&&(d=u,u=void 0),!t.allowSelfLoops&&o===a)throw new Y("Graph.".concat(e,': source & target are the same ("').concat(o,"\"), thus creating a loop explicitly forbidden by this graph 'allowSelfLoops' option set to false."));var p,f,l=t._nodes.get(o),g=t._nodes.get(a);if(!n&&(p=t._edges.get(i))){if(!(p.source.key===o&&p.target.key===a||r&&p.source.key===a&&p.target.key===o))throw new Y("Graph.".concat(e,': inconsistency detected when attempting to merge the "').concat(i,'" edge with "').concat(o,'" source & "').concat(a,'" target vs. ("').concat(p.source.key,'", "').concat(p.target.key,'").'));f=p}if(f||t.multi||!l||(f=r?l.undirected[a]:l.out[a]),f){var y=[f.key,!1,!1,!1];if(s?!d:!u)return y;if(s){var w=f.attributes;f.attributes=d(w),t.emit("edgeAttributesUpdated",{type:"replace",key:f.key,attributes:f.attributes})}else c(f.attributes,u),t.emit("edgeAttributesUpdated",{type:"merge",key:f.key,attributes:f.attributes,data:u});return y}u=u||{},s&&d&&(u=d(u));var v={key:null,undirected:r,source:o,target:a,attributes:u};if(n)i=t._edgeKeyGenerator();else if(i=""+i,t._edges.has(i))throw new Y("Graph.".concat(e,': the "').concat(i,'" edge already exists in the graph.'));var b=!1,m=!1;l||(l=Ot(t,o,{}),b=!0,o===a&&(g=l,m=!0)),g||(g=Ot(t,a,{}),m=!0),p=new H(r,i,l,g,u),t._edges.set(i,p);var k=o===a;return r?(l.undirectedDegree++,g.undirectedDegree++,k&&t._undirectedSelfLoopCount++):(l.outDegree++,g.inDegree++,k&&t._directedSelfLoopCount++),t.multi?p.attachMulti():p.attach(),r?t._undirectedSize++:t._directedSize++,v.key=i,t.emit("edgeAdded",v),[i,!0,b,m]}function Mt(t,e){t._edges.delete(e.key);var n=e.source,r=e.target,i=e.attributes,o=e.undirected,a=n===r;o?(n.undirectedDegree--,r.undirectedDegree--,a&&t._undirectedSelfLoopCount--):(n.outDegree--,r.inDegree--,a&&t._directedSelfLoopCount--),t.multi?e.detachMulti():e.detach(),o?t._undirectedSize--:t._directedSize--,t.emit("edgeDropped",{key:e.key,attributes:i,source:n.key,target:r.key,undirected:o})}var Wt=function(n){function r(t){var e;if(e=n.call(this)||this,"boolean"!=typeof(t=c({},jt,t)).multi)throw new F("Graph.constructor: invalid 'multi' option. Expecting a boolean but got \"".concat(t.multi,'".'));if(!Ut.has(t.type))throw new F('Graph.constructor: invalid \'type\' option. Should be one of "mixed", "directed" or "undirected" but got "'.concat(t.type,'".'));if("boolean"!=typeof t.allowSelfLoops)throw new F("Graph.constructor: invalid 'allowSelfLoops' option. Expecting a boolean but got \"".concat(t.allowSelfLoops,'".'));var r="mixed"===t.type?q:"directed"===t.type?J:V;f(u(e),"NodeDataClass",r);var i="geid_"+Lt()+"_",o=0;return f(u(e),"_attributes",{}),f(u(e),"_nodes",new Map),f(u(e),"_edges",new Map),f(u(e),"_directedSize",0),f(u(e),"_undirectedSize",0),f(u(e),"_directedSelfLoopCount",0),f(u(e),"_undirectedSelfLoopCount",0),f(u(e),"_edgeKeyGenerator",(function(){var t;do{t=i+o++}while(e._edges.has(t));return t})),f(u(e),"_options",t),Nt.forEach((function(t){return f(u(e),t,e[t])})),l(u(e),"order",(function(){return e._nodes.size})),l(u(e),"size",(function(){return e._edges.size})),l(u(e),"directedSize",(function(){return e._directedSize})),l(u(e),"undirectedSize",(function(){return e._undirectedSize})),l(u(e),"selfLoopCount",(function(){return e._directedSelfLoopCount+e._undirectedSelfLoopCount})),l(u(e),"directedSelfLoopCount",(function(){return e._directedSelfLoopCount})),l(u(e),"undirectedSelfLoopCount",(function(){return e._undirectedSelfLoopCount})),l(u(e),"multi",e._options.multi),l(u(e),"type",e._options.type),l(u(e),"allowSelfLoops",e._options.allowSelfLoops),l(u(e),"implementation",(function(){return"graphology"})),e}e(r,n);var i=r.prototype;return i._resetInstanceCounters=function(){this._directedSize=0,this._undirectedSize=0,this._directedSelfLoopCount=0,this._undirectedSelfLoopCount=0},i.hasNode=function(t){return this._nodes.has(""+t)},i.hasDirectedEdge=function(t,e){if("undirected"===this.type)return!1;if(1===arguments.length){var n=""+t,r=this._edges.get(n);return!!r&&!r.undirected}if(2===arguments.length){t=""+t,e=""+e;var i=this._nodes.get(t);if(!i)return!1;var o=i.out[e];return!!o&&(!this.multi||!!o.size)}throw new F("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.hasUndirectedEdge=function(t,e){if("directed"===this.type)return!1;if(1===arguments.length){var n=""+t,r=this._edges.get(n);return!!r&&r.undirected}if(2===arguments.length){t=""+t,e=""+e;var i=this._nodes.get(t);if(!i)return!1;var o=i.undirected[e];return!!o&&(!this.multi||!!o.size)}throw new F("Graph.hasDirectedEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.hasEdge=function(t,e){if(1===arguments.length){var n=""+t;return this._edges.has(n)}if(2===arguments.length){t=""+t,e=""+e;var r=this._nodes.get(t);if(!r)return!1;var i=void 0!==r.out&&r.out[e];return i||(i=void 0!==r.undirected&&r.undirected[e]),!!i&&(!this.multi||!!i.size)}throw new F("Graph.hasEdge: invalid arity (".concat(arguments.length,", instead of 1 or 2). You can either ask for an edge id or for the existence of an edge between a source & a target."))},i.directedEdge=function(t,e){if("undirected"!==this.type){if(t=""+t,e=""+e,this.multi)throw new Y("Graph.directedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.directedEdges instead.");var n=this._nodes.get(t);if(!n)throw new I('Graph.directedEdge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.directedEdge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.out&&n.out[e]||void 0;return r?r.key:void 0}},i.undirectedEdge=function(t,e){if("directed"!==this.type){if(t=""+t,e=""+e,this.multi)throw new Y("Graph.undirectedEdge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.undirectedEdges instead.");var n=this._nodes.get(t);if(!n)throw new I('Graph.undirectedEdge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.undirectedEdge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.undirected&&n.undirected[e]||void 0;return r?r.key:void 0}},i.edge=function(t,e){if(this.multi)throw new Y("Graph.edge: this method is irrelevant with multigraphs since there might be multiple edges between source & target. See #.edges instead.");t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.edge: could not find the "'.concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I('Graph.edge: could not find the "'.concat(e,'" target node in the graph.'));var r=n.out&&n.out[e]||n.undirected&&n.undirected[e]||void 0;if(r)return r.key},i.areDirectedNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areDirectedNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&(e in n.in||e in n.out)},i.areOutNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areOutNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.out},i.areInNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areInNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.in},i.areUndirectedNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areUndirectedNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"directed"!==this.type&&e in n.undirected},i.areNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&(e in n.in||e in n.out)||"directed"!==this.type&&e in n.undirected},i.areInboundNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areInboundNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.in||"directed"!==this.type&&e in n.undirected},i.areOutboundNeighbors=function(t,e){t=""+t,e=""+e;var n=this._nodes.get(t);if(!n)throw new I('Graph.areOutboundNeighbors: could not find the "'.concat(t,'" node in the graph.'));return"undirected"!==this.type&&e in n.out||"directed"!==this.type&&e in n.undirected},i.inDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.inDegree},i.outDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.outDegree},i.directedDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.directedDegree: could not find the "'.concat(t,'" node in the graph.'));return"undirected"===this.type?0:e.inDegree+e.outDegree},i.undirectedDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.undirectedDegree: could not find the "'.concat(t,'" node in the graph.'));return"directed"===this.type?0:e.undirectedDegree},i.inboundDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inboundDegree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.inDegree),n},i.outboundDegree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outboundDegree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.outDegree),n},i.degree=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.degree: could not find the "'.concat(t,'" node in the graph.'));var n=0;return"directed"!==this.type&&(n+=e.undirectedDegree),"undirected"!==this.type&&(n+=e.inDegree+e.outDegree),n},i.inDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.inDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.in[t],r=n?this.multi?n.size:1:0;return e.inDegree-r},i.outDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.outDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.out[t],r=n?this.multi?n.size:1:0;return e.outDegree-r},i.directedDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.directedDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("undirected"===this.type)return 0;var n=e.out[t],r=n?this.multi?n.size:1:0;return e.inDegree+e.outDegree-2*r},i.undirectedDegreeWithoutSelfLoops=function(t){t=""+t;var e=this._nodes.get(t);if(!e)throw new I('Graph.undirectedDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));if("directed"===this.type)return 0;var n=e.undirected[t],r=n?this.multi?n.size:1:0;return e.undirectedDegree-2*r},i.inboundDegreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.inboundDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.inDegree,i+=(e=n.out[t])?this.multi?e.size:1:0),r-i},i.outboundDegreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.outboundDegreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.outDegree,i+=(e=n.in[t])?this.multi?e.size:1:0),r-i},i.degreeWithoutSelfLoops=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.degreeWithoutSelfLoops: could not find the "'.concat(t,'" node in the graph.'));var r=0,i=0;return"directed"!==this.type&&(r+=n.undirectedDegree,i+=2*((e=n.undirected[t])?this.multi?e.size:1:0)),"undirected"!==this.type&&(r+=n.inDegree+n.outDegree,i+=2*((e=n.out[t])?this.multi?e.size:1:0)),r-i},i.source=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.source: could not find the "'.concat(t,'" edge in the graph.'));return e.source.key},i.target=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.target: could not find the "'.concat(t,'" edge in the graph.'));return e.target.key},i.extremities=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.extremities: could not find the "'.concat(t,'" edge in the graph.'));return[e.source.key,e.target.key]},i.opposite=function(t,e){t=""+t,e=""+e;var n=this._edges.get(e);if(!n)throw new I('Graph.opposite: could not find the "'.concat(e,'" edge in the graph.'));var r=n.source.key,i=n.target.key;if(t===r)return i;if(t===i)return r;throw new I('Graph.opposite: the "'.concat(t,'" node is not attached to the "').concat(e,'" edge (').concat(r,", ").concat(i,")."))},i.hasExtremity=function(t,e){t=""+t,e=""+e;var n=this._edges.get(t);if(!n)throw new I('Graph.hasExtremity: could not find the "'.concat(t,'" edge in the graph.'));return n.source.key===e||n.target.key===e},i.isUndirected=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isUndirected: could not find the "'.concat(t,'" edge in the graph.'));return e.undirected},i.isDirected=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isDirected: could not find the "'.concat(t,'" edge in the graph.'));return!e.undirected},i.isSelfLoop=function(t){t=""+t;var e=this._edges.get(t);if(!e)throw new I('Graph.isSelfLoop: could not find the "'.concat(t,'" edge in the graph.'));return e.source===e.target},i.addNode=function(t,e){var n=function(t,e,n){if(n&&!h(n))throw new F('Graph.addNode: invalid attributes. Expecting an object but got "'.concat(n,'"'));if(e=""+e,n=n||{},t._nodes.has(e))throw new Y('Graph.addNode: the "'.concat(e,'" node already exist in the graph.'));var r=new t.NodeDataClass(e,n);return t._nodes.set(e,r),t.emit("nodeAdded",{key:e,attributes:n}),r}(this,t,e);return n.key},i.mergeNode=function(t,e){if(e&&!h(e))throw new F('Graph.mergeNode: invalid attributes. Expecting an object but got "'.concat(e,'"'));t=""+t,e=e||{};var n=this._nodes.get(t);return n?(e&&(c(n.attributes,e),this.emit("nodeAttributesUpdated",{type:"merge",key:t,attributes:n.attributes,data:e})),[t,!1]):(n=new this.NodeDataClass(t,e),this._nodes.set(t,n),this.emit("nodeAdded",{key:t,attributes:e}),[t,!0])},i.updateNode=function(t,e){if(e&&"function"!=typeof e)throw new F('Graph.updateNode: invalid updater function. Expecting a function but got "'.concat(e,'"'));t=""+t;var n=this._nodes.get(t);if(n){if(e){var r=n.attributes;n.attributes=e(r),this.emit("nodeAttributesUpdated",{type:"replace",key:t,attributes:n.attributes})}return[t,!1]}var i=e?e({}):{};return n=new this.NodeDataClass(t,i),this._nodes.set(t,n),this.emit("nodeAdded",{key:t,attributes:i}),[t,!0]},i.dropNode=function(t){t=""+t;var e,n=this._nodes.get(t);if(!n)throw new I('Graph.dropNode: could not find the "'.concat(t,'" node in the graph.'));if("undirected"!==this.type){for(var r in n.out){e=n.out[r];do{Mt(this,e),e=e.next}while(e)}for(var i in n.in){e=n.in[i];do{Mt(this,e),e=e.next}while(e)}}if("directed"!==this.type)for(var o in n.undirected){e=n.undirected[o];do{Mt(this,e),e=e.next}while(e)}this._nodes.delete(t),this.emit("nodeDropped",{key:t,attributes:n.attributes})},i.dropEdge=function(t){var e;if(arguments.length>1){var n=""+arguments[0],r=""+arguments[1];if(!(e=s(this,n,r,this.type)))throw new I('Graph.dropEdge: could not find the "'.concat(n,'" -> "').concat(r,'" edge in the graph.'))}else if(t=""+t,!(e=this._edges.get(t)))throw new I('Graph.dropEdge: could not find the "'.concat(t,'" edge in the graph.'));return Mt(this,e),this},i.dropDirectedEdge=function(t,e){if(arguments.length<2)throw new Y("Graph.dropDirectedEdge: it does not make sense to try and drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.");if(this.multi)throw new Y("Graph.dropDirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.");var n=s(this,t=""+t,e=""+e,"directed");if(!n)throw new I('Graph.dropDirectedEdge: could not find a "'.concat(t,'" -> "').concat(e,'" edge in the graph.'));return Mt(this,n),this},i.dropUndirectedEdge=function(t,e){if(arguments.length<2)throw new Y("Graph.dropUndirectedEdge: it does not make sense to drop a directed edge by key. What if the edge with this key is undirected? Use #.dropEdge for this purpose instead.");if(this.multi)throw new Y("Graph.dropUndirectedEdge: cannot use a {source,target} combo when dropping an edge in a MultiGraph since we cannot infer the one you want to delete as there could be multiple ones.");var n=s(this,t,e,"undirected");if(!n)throw new I('Graph.dropUndirectedEdge: could not find a "'.concat(t,'" -> "').concat(e,'" edge in the graph.'));return Mt(this,n),this},i.clear=function(){this._edges.clear(),this._nodes.clear(),this._resetInstanceCounters(),this.emit("cleared")},i.clearEdges=function(){for(var t,e=this._nodes.values();!0!==(t=e.next()).done;)t.value.clear();this._edges.clear(),this._resetInstanceCounters(),this.emit("edgesCleared")},i.getAttribute=function(t){return this._attributes[t]},i.getAttributes=function(){return this._attributes},i.hasAttribute=function(t){return this._attributes.hasOwnProperty(t)},i.setAttribute=function(t,e){return this._attributes[t]=e,this.emit("attributesUpdated",{type:"set",attributes:this._attributes,name:t}),this},i.updateAttribute=function(t,e){if("function"!=typeof e)throw new F("Graph.updateAttribute: updater should be a function.");var n=this._attributes[t];return this._attributes[t]=e(n),this.emit("attributesUpdated",{type:"set",attributes:this._attributes,name:t}),this},i.removeAttribute=function(t){return delete this._attributes[t],this.emit("attributesUpdated",{type:"remove",attributes:this._attributes,name:t}),this},i.replaceAttributes=function(t){if(!h(t))throw new F("Graph.replaceAttributes: provided attributes are not a plain object.");return this._attributes=t,this.emit("attributesUpdated",{type:"replace",attributes:this._attributes}),this},i.mergeAttributes=function(t){if(!h(t))throw new F("Graph.mergeAttributes: provided attributes are not a plain object.");return c(this._attributes,t),this.emit("attributesUpdated",{type:"merge",attributes:this._attributes,data:t}),this},i.updateAttributes=function(t){if("function"!=typeof t)throw new F("Graph.updateAttributes: provided updater is not a function.");return this._attributes=t(this._attributes),this.emit("attributesUpdated",{type:"update",attributes:this._attributes}),this},i.updateEachNodeAttributes=function(t,e){if("function"!=typeof t)throw new F("Graph.updateEachNodeAttributes: expecting an updater function.");if(e&&!g(e))throw new F("Graph.updateEachNodeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}");for(var n,r,i=this._nodes.values();!0!==(n=i.next()).done;)(r=n.value).attributes=t(r.key,r.attributes);this.emit("eachNodeAttributesUpdated",{hints:e||null})},i.updateEachEdgeAttributes=function(t,e){if("function"!=typeof t)throw new F("Graph.updateEachEdgeAttributes: expecting an updater function.");if(e&&!g(e))throw new F("Graph.updateEachEdgeAttributes: invalid hints. Expecting an object having the following shape: {attributes?: [string]}");for(var n,r,i,o,a=this._edges.values();!0!==(n=a.next()).done;)i=(r=n.value).source,o=r.target,r.attributes=t(r.key,r.attributes,i.key,o.key,i.attributes,o.attributes,r.undirected);this.emit("eachEdgeAttributesUpdated",{hints:e||null})},i.forEachAdjacencyEntry=function(t){if("function"!=typeof t)throw new F("Graph.forEachAdjacencyEntry: expecting a callback.");Et(!1,!1,!1,this,t)},i.forEachAdjacencyEntryWithOrphans=function(t){if("function"!=typeof t)throw new F("Graph.forEachAdjacencyEntryWithOrphans: expecting a callback.");Et(!1,!1,!0,this,t)},i.forEachAssymetricAdjacencyEntry=function(t){if("function"!=typeof t)throw new F("Graph.forEachAssymetricAdjacencyEntry: expecting a callback.");Et(!1,!0,!1,this,t)},i.forEachAssymetricAdjacencyEntryWithOrphans=function(t){if("function"!=typeof t)throw new F("Graph.forEachAssymetricAdjacencyEntryWithOrphans: expecting a callback.");Et(!1,!0,!0,this,t)},i.nodes=function(){return"function"==typeof Array.from?Array.from(this._nodes.keys()):T(this._nodes.keys(),this._nodes.size)},i.forEachNode=function(t){if("function"!=typeof t)throw new F("Graph.forEachNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)t((n=e.value).key,n.attributes)},i.findNode=function(t){if("function"!=typeof t)throw new F("Graph.findNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(t((n=e.value).key,n.attributes))return n.key},i.mapNodes=function(t){if("function"!=typeof t)throw new F("Graph.mapNode: expecting a callback.");for(var e,n,r=this._nodes.values(),i=new Array(this.order),o=0;!0!==(e=r.next()).done;)n=e.value,i[o++]=t(n.key,n.attributes);return i},i.someNode=function(t){if("function"!=typeof t)throw new F("Graph.someNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(t((n=e.value).key,n.attributes))return!0;return!1},i.everyNode=function(t){if("function"!=typeof t)throw new F("Graph.everyNode: expecting a callback.");for(var e,n,r=this._nodes.values();!0!==(e=r.next()).done;)if(!t((n=e.value).key,n.attributes))return!1;return!0},i.filterNodes=function(t){if("function"!=typeof t)throw new F("Graph.filterNodes: expecting a callback.");for(var e,n,r=this._nodes.values(),i=[];!0!==(e=r.next()).done;)t((n=e.value).key,n.attributes)&&i.push(n.key);return i},i.reduceNodes=function(t,e){if("function"!=typeof t)throw new F("Graph.reduceNodes: expecting a callback.");if(arguments.length<2)throw new F("Graph.reduceNodes: missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array.");for(var n,r,i=e,o=this._nodes.values();!0!==(n=o.next()).done;)i=t(i,(r=n.value).key,r.attributes);return i},i.nodeEntries=function(){var t=this._nodes.values();return new O((function(){var e=t.next();if(e.done)return e;var n=e.value;return{value:{node:n.key,attributes:n.attributes},done:!1}}))},i.export=function(){var t=new Array(this._nodes.size),e=0;this._nodes.forEach((function(n,r){t[e++]=function(t,e){var n={key:t};return p(e.attributes)||(n.attributes=c({},e.attributes)),n}(r,n)}));var n=new Array(this._edges.size);return e=0,this._edges.forEach((function(t,r){n[e++]=function(t,e){var n={key:t,source:e.source.key,target:e.target.key};return p(e.attributes)||(n.attributes=c({},e.attributes)),e.undirected&&(n.undirected=!0),n}(r,t)})),{options:{type:this.type,multi:this.multi,allowSelfLoops:this.allowSelfLoops},attributes:this.getAttributes(),nodes:t,edges:n}},i.import=function(t){var e,n,r,i,o,a=this,u=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(d(t))return t.forEachNode((function(t,e){u?a.mergeNode(t,e):a.addNode(t,e)})),t.forEachEdge((function(t,e,n,r,i,o,c){u?c?a.mergeUndirectedEdgeWithKey(t,n,r,e):a.mergeDirectedEdgeWithKey(t,n,r,e):c?a.addUndirectedEdgeWithKey(t,n,r,e):a.addDirectedEdgeWithKey(t,n,r,e)})),this;if(!h(t))throw new F("Graph.import: invalid argument. Expecting a serialized graph or, alternatively, a Graph instance.");if(t.attributes){if(!h(t.attributes))throw new F("Graph.import: invalid attributes. Expecting a plain object.");u?this.mergeAttributes(t.attributes):this.replaceAttributes(t.attributes)}if(t.nodes){if(r=t.nodes,!Array.isArray(r))throw new F("Graph.import: invalid nodes. Expecting an array.");for(e=0,n=r.length;e<n;e++){At(i=r[e]);var c=i,s=c.key,p=c.attributes;u?this.mergeNode(s,p):this.addNode(s,p)}}if(t.edges){if(r=t.edges,!Array.isArray(r))throw new F("Graph.import: invalid edges. Expecting an array.");for(e=0,n=r.length;e<n;e++){St(o=r[e]);var f=o,l=f.source,g=f.target,y=f.attributes,w=f.undirected,v=void 0!==w&&w;"key"in o?(u?v?this.mergeUndirectedEdgeWithKey:this.mergeDirectedEdgeWithKey:v?this.addUndirectedEdgeWithKey:this.addDirectedEdgeWithKey).call(this,o.key,l,g,y):(u?v?this.mergeUndirectedEdge:this.mergeDirectedEdge:v?this.addUndirectedEdge:this.addDirectedEdge).call(this,l,g,y)}}return this},i.nullCopy=function(t){var e=new r(c({},this._options,t));return e.replaceAttributes(c({},this.getAttributes())),e},i.emptyCopy=function(t){var e=this.nullCopy(t);return this._nodes.forEach((function(t,n){var r=c({},t.attributes);t=new e.NodeDataClass(n,r),e._nodes.set(n,t)})),e},i.copy=function(t){if("string"==typeof(t=t||{}).type&&t.type!==this.type&&"mixed"!==t.type)throw new Y('Graph.copy: cannot create an incompatible copy from "'.concat(this.type,'" type to "').concat(t.type,'" because this would mean losing information about the current graph.'));if("boolean"==typeof t.multi&&t.multi!==this.multi&&!0!==t.multi)throw new Y("Graph.copy: cannot create an incompatible copy by downgrading a multi graph to a simple one because this would mean losing information about the current graph.");if("boolean"==typeof t.allowSelfLoops&&t.allowSelfLoops!==this.allowSelfLoops&&!0!==t.allowSelfLoops)throw new Y("Graph.copy: cannot create an incompatible copy from a graph allowing self loops to one that does not because this would mean losing information about the current graph.");for(var e,n,r=this.emptyCopy(t),i=this._edges.values();!0!==(e=i.next()).done;)Ct(r,"copy",!1,(n=e.value).undirected,n.key,n.source.key,n.target.key,c({},n.attributes));return r},i.toJSON=function(){return this.export()},i.toString=function(){return"[object Graph]"},i.inspect=function(){var e=this,n={};this._nodes.forEach((function(t,e){n[e]=t.attributes}));var r={},i={};this._edges.forEach((function(t,n){var o,a=t.undirected?"--":"->",u="",c=t.source.key,s=t.target.key;t.undirected&&c>s&&(o=c,c=s,s=o);var d="(".concat(c,")").concat(a,"(").concat(s,")");n.startsWith("geid_")?e.multi&&(void 0===i[d]?i[d]=0:i[d]++,u+="".concat(i[d],". ")):u+="[".concat(n,"]: "),r[u+=d]=t.attributes}));var o={};for(var a in this)this.hasOwnProperty(a)&&!Nt.has(a)&&"function"!=typeof this[a]&&"symbol"!==t(a)&&(o[a]=this[a]);return o.attributes=this._attributes,o.nodes=n,o.edges=r,f(o,"constructor",this.constructor),o},r}(w.exports.EventEmitter);"undefined"!=typeof Symbol&&(Wt.prototype[Symbol.for("nodejs.util.inspect.custom")]=Wt.prototype.inspect),[{name:function(t){return"".concat(t,"Edge")},generateKey:!0},{name:function(t){return"".concat(t,"DirectedEdge")},generateKey:!0,type:"directed"},{name:function(t){return"".concat(t,"UndirectedEdge")},generateKey:!0,type:"undirected"},{name:function(t){return"".concat(t,"EdgeWithKey")}},{name:function(t){return"".concat(t,"DirectedEdgeWithKey")},type:"directed"},{name:function(t){return"".concat(t,"UndirectedEdgeWithKey")},type:"undirected"}].forEach((function(t){["add","merge","update"].forEach((function(e){var n=t.name(e),r="add"===e?Ct:zt;t.generateKey?Wt.prototype[n]=function(i,o,a){return r(this,n,!0,"undirected"===(t.type||this.type),null,i,o,a,"update"===e)}:Wt.prototype[n]=function(i,o,a,u){return r(this,n,!1,"undirected"===(t.type||this.type),i,o,a,u,"update"===e)}}))})),function(t){X.forEach((function(e){var n=e.name,r=e.attacher;r(t,n("Node"),0),r(t,n("Source"),1),r(t,n("Target"),2),r(t,n("Opposite"),3)}))}(Wt),function(t){Z.forEach((function(e){var n=e.name,r=e.attacher;r(t,n("Edge"),"mixed"),r(t,n("DirectedEdge"),"directed"),r(t,n("UndirectedEdge"),"undirected")}))}(Wt),function(t){nt.forEach((function(e){!function(t,e){var n=e.name,r=e.type,i=e.direction;t.prototype[n]=function(t,e){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return[];if(!arguments.length)return st(this,r);if(1===arguments.length){t=""+t;var o=this._nodes.get(t);if(void 0===o)throw new I("Graph.".concat(n,': could not find the "').concat(t,'" node in the graph.'));return ft(this.multi,"mixed"===r?this.type:r,i,o)}if(2===arguments.length){t=""+t,e=""+e;var a=this._nodes.get(t);if(!a)throw new I("Graph.".concat(n,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(n,':  could not find the "').concat(e,'" target node in the graph.'));return yt(r,this.multi,i,a,e)}throw new F("Graph.".concat(n,": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length,")."))}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="forEach"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e,n){if("mixed"===r||"mixed"===this.type||r===this.type){if(1===arguments.length)return dt(!1,this,r,n=t);if(2===arguments.length){t=""+t,n=e;var a=this._nodes.get(t);if(void 0===a)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return pt(!1,this.multi,"mixed"===r?this.type:r,i,a,n)}if(3===arguments.length){t=""+t,e=""+e;var u=this._nodes.get(t);if(!u)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return gt(!1,r,this.multi,i,u,e,n)}throw new F("Graph.".concat(o,": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length,")."))}};var a="map"+n[0].toUpperCase()+n.slice(1);t.prototype[a]=function(){var t,e=Array.prototype.slice.call(arguments),n=e.pop();if(0===e.length){var i=0;"directed"!==r&&(i+=this.undirectedSize),"undirected"!==r&&(i+=this.directedSize),t=new Array(i);var a=0;e.push((function(e,r,i,o,u,c,s){t[a++]=n(e,r,i,o,u,c,s)}))}else t=[],e.push((function(e,r,i,o,a,u,c){t.push(n(e,r,i,o,a,u,c))}));return this[o].apply(this,e),t};var u="filter"+n[0].toUpperCase()+n.slice(1);t.prototype[u]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop(),n=[];return t.push((function(t,r,i,o,a,u,c){e(t,r,i,o,a,u,c)&&n.push(t)})),this[o].apply(this,t),n};var c="reduce"+n[0].toUpperCase()+n.slice(1);t.prototype[c]=function(){var t,e,n=Array.prototype.slice.call(arguments);if(n.length<2||n.length>4)throw new F("Graph.".concat(c,": invalid number of arguments (expecting 2, 3 or 4 and got ").concat(n.length,")."));if("function"==typeof n[n.length-1]&&"function"!=typeof n[n.length-2])throw new F("Graph.".concat(c,": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));2===n.length?(t=n[0],e=n[1],n=[]):3===n.length?(t=n[1],e=n[2],n=[n[0]]):4===n.length&&(t=n[2],e=n[3],n=[n[0],n[1]]);var r=e;return n.push((function(e,n,i,o,a,u,c){r=t(r,e,n,i,o,a,u,c)})),this[o].apply(this,n),r}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="find"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e,n){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return!1;if(1===arguments.length)return dt(!0,this,r,n=t);if(2===arguments.length){t=""+t,n=e;var a=this._nodes.get(t);if(void 0===a)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return pt(!0,this.multi,"mixed"===r?this.type:r,i,a,n)}if(3===arguments.length){t=""+t,e=""+e;var u=this._nodes.get(t);if(!u)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return gt(!0,r,this.multi,i,u,e,n)}throw new F("Graph.".concat(o,": too many arguments (expecting 1, 2 or 3 and got ").concat(arguments.length,")."))};var a="some"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[a]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop();return t.push((function(t,n,r,i,o,a,u){return e(t,n,r,i,o,a,u)})),!!this[o].apply(this,t)};var u="every"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[u]=function(){var t=Array.prototype.slice.call(arguments),e=t.pop();return t.push((function(t,n,r,i,o,a,u){return!e(t,n,r,i,o,a,u)})),!this[o].apply(this,t)}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o=n.slice(0,-1)+"Entries";t.prototype[o]=function(t,e){if("mixed"!==r&&"mixed"!==this.type&&r!==this.type)return O.empty();if(!arguments.length)return ht(this,r);if(1===arguments.length){t=""+t;var n=this._nodes.get(t);if(!n)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));return lt(r,i,n)}if(2===arguments.length){t=""+t,e=""+e;var a=this._nodes.get(t);if(!a)throw new I("Graph.".concat(o,':  could not find the "').concat(t,'" source node in the graph.'));if(!this._nodes.has(e))throw new I("Graph.".concat(o,':  could not find the "').concat(e,'" target node in the graph.'));return wt(r,i,a,e)}throw new F("Graph.".concat(o,": too many arguments (expecting 0, 1 or 2 and got ").concat(arguments.length,")."))}}(t,e)}))}(Wt),function(t){vt.forEach((function(e){Gt(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o="forEach"+n[0].toUpperCase()+n.slice(1,-1);t.prototype[o]=function(t,e){if("mixed"===r||"mixed"===this.type||r===this.type){t=""+t;var n=this._nodes.get(t);if(void 0===n)throw new I("Graph.".concat(o,': could not find the "').concat(t,'" node in the graph.'));kt(!1,"mixed"===r?this.type:r,i,n,e)}};var a="map"+n[0].toUpperCase()+n.slice(1);t.prototype[a]=function(t,e){var n=[];return this[o](t,(function(t,r){n.push(e(t,r))})),n};var u="filter"+n[0].toUpperCase()+n.slice(1);t.prototype[u]=function(t,e){var n=[];return this[o](t,(function(t,r){e(t,r)&&n.push(t)})),n};var c="reduce"+n[0].toUpperCase()+n.slice(1);t.prototype[c]=function(t,e,n){if(arguments.length<3)throw new F("Graph.".concat(c,": missing initial value. You must provide it because the callback takes more than one argument and we cannot infer the initial value from the first iteration, as you could with a simple array."));var r=n;return this[o](t,(function(t,n){r=e(r,t,n)})),r}}(t,e),function(t,e){var n=e.name,r=e.type,i=e.direction,o=n[0].toUpperCase()+n.slice(1,-1),a="find"+o;t.prototype[a]=function(t,e){if("mixed"===r||"mixed"===this.type||r===this.type){t=""+t;var n=this._nodes.get(t);if(void 0===n)throw new I("Graph.".concat(a,': could not find the "').concat(t,'" node in the graph.'));return kt(!0,"mixed"===r?this.type:r,i,n,e)}};var u="some"+o;t.prototype[u]=function(t,e){return!!this[a](t,e)};var c="every"+o;t.prototype[c]=function(t,e){return!this[a](t,(function(t,n){return!e(t,n)}))}}(t,e),xt(t,e)}))}(Wt);var Pt=function(t){function n(e){var n=c({type:"directed"},e);if("multi"in n&&!1!==n.multi)throw new F("DirectedGraph.from: inconsistent indication that the graph should be multi in given options!");if("directed"!==n.type)throw new F('DirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Rt=function(t){function n(e){var n=c({type:"undirected"},e);if("multi"in n&&!1!==n.multi)throw new F("UndirectedGraph.from: inconsistent indication that the graph should be multi in given options!");if("undirected"!==n.type)throw new F('UndirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Kt=function(t){function n(e){var n=c({multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiGraph.from: inconsistent indication that the graph should be simple in given options!");return t.call(this,n)||this}return e(n,t),n}(Wt),Tt=function(t){function n(e){var n=c({type:"directed",multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiDirectedGraph.from: inconsistent indication that the graph should be simple in given options!");if("directed"!==n.type)throw new F('MultiDirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt),Bt=function(t){function n(e){var n=c({type:"undirected",multi:!0},e);if("multi"in n&&!0!==n.multi)throw new F("MultiUndirectedGraph.from: inconsistent indication that the graph should be simple in given options!");if("undirected"!==n.type)throw new F('MultiUndirectedGraph.from: inconsistent "'+n.type+'" type in given options!');return t.call(this,n)||this}return e(n,t),n}(Wt);function Ft(t){t.from=function(e,n){var r=c({},e.options,n),i=new t(r);return i.import(e),i}}return Ft(Wt),Ft(Pt),Ft(Rt),Ft(Kt),Ft(Tt),Ft(Bt),Wt.Graph=Wt,Wt.DirectedGraph=Pt,Wt.UndirectedGraph=Rt,Wt.MultiGraph=Kt,Wt.MultiDirectedGraph=Tt,Wt.MultiUndirectedGraph=Bt,Wt.InvalidArgumentsGraphError=F,Wt.NotFoundGraphError=I,Wt.UsageGraphError=Y,Wt}));
+//# sourceMappingURL=graphology.umd.min.js.map
diff --git a/libs/shared/graph-layouts/node_modules/graphology/package.json b/libs/shared/graph-layouts/node_modules/graphology/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..192894e7d629fb3be2591579dae3dc3fb57731d3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/package.json
@@ -0,0 +1,84 @@
+{
+  "name": "graphology",
+  "version": "0.24.1",
+  "description": "A robust and multipurpose Graph object for JavaScript.",
+  "main": "dist/graphology.cjs.js",
+  "module": "dist/graphology.esm.js",
+  "browser": "dist/graphology.umd.min.js",
+  "types": "dist/graphology.d.ts",
+  "scripts": {
+    "clean": "rimraf dist specs",
+    "build": "npm run clean && rollup -c && babel tests --out-dir specs && cp src/endpoint.esm.d.ts dist/graphology.d.ts",
+    "prepare": "npm run build",
+    "prepublishOnly": "npm test && npm run test:types && npm run build",
+    "test": "mocha -u exports --require @babel/register ./test.js",
+    "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns --strictNullChecks ./test-types.ts"
+  },
+  "files": [
+    "dist/*.js",
+    "dist/*.ts",
+    "specs"
+  ],
+  "keywords": [
+    "graph",
+    "graph theory",
+    "directed",
+    "undirected",
+    "network"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/graphology/graphology.git"
+  },
+  "contributors": [
+    {
+      "name": "Alexis Jacomy",
+      "url": "http://github.com/jacomyal"
+    },
+    {
+      "name": "Benjamin Ooghe-Tabanou",
+      "url": "http://github.com/boogheta"
+    },
+    {
+      "name": "Guillaume Plique",
+      "url": "http://github.com/Yomguithereal"
+    }
+  ],
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/graphology/graphology/issues"
+  },
+  "homepage": "https://github.com/graphology/graphology#readme",
+  "dependencies": {
+    "events": "^3.3.0",
+    "obliterator": "^2.0.2"
+  },
+  "peerDependencies": {
+    "graphology-types": ">=0.24.0"
+  },
+  "babel": {
+    "presets": [
+      "@babel/preset-env"
+    ],
+    "plugins": [
+      [
+        "@babel/transform-classes",
+        {
+          "loose": true
+        }
+      ],
+      [
+        "@babel/transform-destructuring",
+        {
+          "loose": true
+        }
+      ],
+      [
+        "@babel/transform-spread",
+        {
+          "loose": true
+        }
+      ]
+    ]
+  }
+}
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/attributes.js b/libs/shared/graph-layouts/node_modules/graphology/specs/attributes.js
new file mode 100644
index 0000000000000000000000000000000000000000..f026c38eacec861c6fea6a2837d868a8233d9ed0
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/attributes.js
@@ -0,0 +1,992 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = attributes;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function attributes(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+
+  function commonTests(method) {
+    return _defineProperty({}, '#.' + method, {
+      'it should throw if the given path is not found.': function itShouldThrowIfTheGivenPathIsNotFound() {
+        if (!method.includes('Edge')) return;
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph[method]('source', 'target', 'name', 'value');
+        }, notFound());
+      },
+      'it should throw when using a path on a multi graph.': function itShouldThrowWhenUsingAPathOnAMultiGraph() {
+        if (!method.includes('Edge')) return;
+        var graph = new Graph({
+          multi: true
+        });
+
+        _assert["default"]["throws"](function () {
+          graph[method]('source', 'target', 'name', 'value');
+        }, usage());
+      },
+      'it should throw if the element is not found in the graph.': function itShouldThrowIfTheElementIsNotFoundInTheGraph() {
+        var graph = new Graph();
+
+        if (method.includes('Edge') && method.includes('Directed') || method.includes('Undirected')) {
+          _assert["default"]["throws"](function () {
+            graph[method]('Test');
+          }, usage());
+        } else {
+          _assert["default"]["throws"](function () {
+            graph[method]('Test');
+          }, notFound());
+        }
+      }
+    });
+  }
+
+  var tests = {};
+  var relevantMethods = Object.keys(Graph.prototype).filter(function (name) {
+    return (name.includes('NodeAttribute') || name.includes('EdgeAttribute') || name.includes('SourceAttribute') || name.includes('TargetAttribute') || name.includes('OppositeAttribute')) && !name.includes('Each');
+  });
+  relevantMethods.forEach(function (method) {
+    return (0, _helpers.deepMerge)(tests, commonTests(method));
+  });
+  return (0, _helpers.deepMerge)(tests, {
+    '#.getAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), undefined);
+      }
+    },
+    '#.getNodeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('Martha', 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('Martha', 'age'), undefined);
+      }
+    },
+    '#.getSourceAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        var _graph$mergeEdge = graph.mergeEdge('Martha', 'Riwan'),
+            edge = _graph$mergeEdge[0];
+
+        _assert["default"].strictEqual(graph.getSourceAttribute(edge, 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        var _graph$mergeEdge2 = graph.mergeEdge('Martha', 'Riwan'),
+            edge = _graph$mergeEdge2[0];
+
+        _assert["default"].strictEqual(graph.getSourceAttribute(edge, 'age'), undefined);
+      }
+    },
+    '#.getTargetAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        var _graph$mergeEdge3 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge3[0];
+
+        _assert["default"].strictEqual(graph.getTargetAttribute(edge, 'age'), 34);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        var _graph$mergeEdge4 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge4[0];
+
+        _assert["default"].strictEqual(graph.getTargetAttribute(edge, 'age'), undefined);
+      }
+    },
+    '#.getOppositeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+        graph.addNode('Riwan', {
+          age: 25
+        });
+
+        var _graph$mergeEdge5 = graph.mergeEdge('Riwan', 'Martha'),
+            edge = _graph$mergeEdge5[0];
+
+        _assert["default"].strictEqual(graph.getOppositeAttribute('Riwan', edge, 'age'), 34);
+
+        _assert["default"].strictEqual(graph.getOppositeAttribute('Martha', edge, 'age'), 25);
+      }
+    },
+    '#.getEdgeAttribute': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute('John', 'Thomas', 'weight'), 2);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      },
+      'it should return undefined if the attribute does not exist.': function itShouldReturnUndefinedIfTheAttributeDoesNotExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas');
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), undefined);
+      }
+    },
+    '#.getAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph'
+        });
+      },
+      'it should return an empty object if the node does not have attributes.': function itShouldReturnAnEmptyObjectIfTheNodeDoesNotHaveAttributes() {
+        var graph = new Graph();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {});
+      }
+    },
+    '#.getNodeAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {
+          age: 34
+        });
+      },
+      'it should return an empty object if the node does not have attributes.': function itShouldReturnAnEmptyObjectIfTheNodeDoesNotHaveAttributes() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {});
+      }
+    },
+    '#.getEdgeAttributes': {
+      'it should return the correct value.': function itShouldReturnTheCorrectValue() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas', {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Thomas'), {
+          weight: 2
+        });
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas', 'weight'), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas', 'weight'), {
+          weight: 3
+        });
+      },
+      'it should return an empty object if the edge does not have attributes.': function itShouldReturnAnEmptyObjectIfTheEdgeDoesNotHaveAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        var edge = graph.addEdge('John', 'Thomas');
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {});
+      }
+    },
+    '#.hasAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.hasAttribute('name'), true);
+
+        _assert["default"].strictEqual(graph.hasAttribute('info'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.hasAttribute('toString'), false);
+      }
+    },
+    '#.hasNodeAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'age'), true);
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'eyes'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('John', 'toString'), false);
+      }
+    },
+    '#.hasEdgeAttribute': {
+      'it should correctly return whether the attribute is set.': function itShouldCorrectlyReturnWhetherTheAttributeIsSet() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdgeWithKey('J->M', 'John', 'Martha', {
+          weight: 10
+        });
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'weight'), true);
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'type'), false);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdgeAttribute('John', 'Thomas', 'weight'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+      },
+      'it does not fail with typical prototypal properties.': function itDoesNotFailWithTypicalPrototypalProperties() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdgeWithKey('J->M', 'John', 'Martha', {
+          weight: 10
+        });
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute('J->M', 'toString'), false);
+      }
+    },
+    '#.setAttribute': {
+      "it should correctly set the graph's attribute.": function itShouldCorrectlySetTheGraphSAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      }
+    },
+    '#.setNodeAttribute': {
+      "it should correctly set the node's attribute.": function itShouldCorrectlySetTheNodeSAttribute() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+        graph.setNodeAttribute('John', 'age', 45);
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 45);
+      }
+    },
+    '#.setEdgeAttribute': {
+      "it should correctly set the edge's attribute.": function itShouldCorrectlySetTheEdgeSAttribute() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+        graph.setEdgeAttribute(edge, 'weight', 40);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 40);
+
+        graph.setEdgeAttribute('John', 'Martha', 'weight', 60);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 60);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.setDirectedEdgeAttribute('John', 'Thomas', 'weight', 2);
+        graph.setUndirectedEdgeAttribute('John', 'Thomas', 'weight', 3);
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      }
+    },
+    '#.updateAttribute': {
+      'it should throw if the updater is not a function.': function itShouldThrowIfTheUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        graph.setAttribute('count', 0);
+
+        _assert["default"]["throws"](function () {
+          graph.updateAttribute('count', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the graph's attribute.": function itShouldCorrectlySetTheGraphSAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.updateAttribute('name', function (name) {
+          return name + '1';
+        });
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph1');
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 'graph';
+        };
+
+        graph.updateAttribute('name', updater);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'graph');
+      }
+    },
+    '#.updateNodeAttribute': {
+      'it should throw if given an invalid updater.': function itShouldThrowIfGivenAnInvalidUpdater() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttribute('John', 'age', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      'it should throw if not enough arguments are provided.': function itShouldThrowIfNotEnoughArgumentsAreProvided() {
+        var graph = new Graph();
+        graph.addNode('Lucy');
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttribute('Lucy', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the node's attribute.": function itShouldCorrectlySetTheNodeSAttribute() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 20
+        });
+        graph.updateNodeAttribute('John', 'age', function (x) {
+          return x + 1;
+        });
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 21);
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 10;
+        };
+
+        graph.updateNodeAttribute('John', 'age', updater);
+
+        _assert["default"].strictEqual(graph.getNodeAttribute('John', 'age'), 10);
+      }
+    },
+    '#.updateEdgeAttribute': {
+      'it should throw if given an invalid updater.': function itShouldThrowIfGivenAnInvalidUpdater() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeAttribute('John', 'Martha', 'weight', {
+            hello: 'world'
+          });
+        }, invalid());
+      },
+      "it should correctly set the edge's attribute.": function itShouldCorrectlySetTheEdgeSAttribute() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 3
+        });
+        graph.updateEdgeAttribute(edge, 'weight', function (x) {
+          return x + 1;
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 4);
+
+        graph.updateEdgeAttribute('John', 'Martha', 'weight', function (x) {
+          return x + 2;
+        });
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 6);
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 0
+        });
+        graph.updateDirectedEdgeAttribute('John', 'Thomas', 'weight', function (x) {
+          return x + 2;
+        });
+        graph.updateUndirectedEdgeAttribute('John', 'Thomas', 'weight', function (x) {
+          return x + 3;
+        });
+
+        _assert["default"].strictEqual(graph.getDirectedEdgeAttribute('John', 'Thomas', 'weight'), 2);
+
+        _assert["default"].strictEqual(graph.getUndirectedEdgeAttribute('John', 'Thomas', 'weight'), 3);
+      },
+      'the given value should be undefined if not found.': function theGivenValueShouldBeUndefinedIfNotFound() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        var updater = function updater(x) {
+          _assert["default"].strictEqual(x, undefined);
+
+          return 10;
+        };
+
+        graph.updateEdgeAttribute(edge, 'weight', updater);
+
+        _assert["default"].strictEqual(graph.getEdgeAttribute(edge, 'weight'), 10);
+      }
+    },
+    '#.removeAttribute': {
+      'it should correctly remove the attribute.': function itShouldCorrectlyRemoveTheAttribute() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.removeAttribute('name');
+
+        _assert["default"].strictEqual(graph.hasAttribute('name'), false);
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {});
+      }
+    },
+    '#.removeNodeAttribute': {
+      'it should correctly remove the attribute.': function itShouldCorrectlyRemoveTheAttribute() {
+        var graph = new Graph();
+        graph.addNode('Martha', {
+          age: 34
+        });
+        graph.removeNodeAttribute('Martha', 'age');
+
+        _assert["default"].strictEqual(graph.hasNodeAttribute('Martha', 'age'), false);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('Martha'), {});
+      }
+    },
+    '#.removeEdgeAttribute': {
+      'it should correclty remove the attribute.': function itShouldCorrecltyRemoveTheAttribute() {
+        var graph = new Graph();
+
+        var _graph$mergeEdge6 = graph.mergeEdge('John', 'Martha', {
+          weight: 1,
+          size: 3
+        }),
+            edge = _graph$mergeEdge6[0];
+
+        graph.removeEdgeAttribute('John', 'Martha', 'weight');
+        graph.removeEdgeAttribute(edge, 'size');
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute(edge, 'weight'), false);
+
+        _assert["default"].strictEqual(graph.hasEdgeAttribute(edge, 'size'), false);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {});
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          weight: 2
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          weight: 3
+        });
+        graph.removeDirectedEdgeAttribute('John', 'Thomas', 'weight');
+        graph.removeUndirectedEdgeAttribute('John', 'Thomas', 'weight');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdgeAttribute('John', 'Thomas', 'weight'), false);
+      }
+    },
+    '#.replaceAttribute': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.replaceAttributes(true);
+        }, invalid());
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.replaceAttributes({
+          name: 'other graph'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'other graph'
+        });
+      }
+    },
+    '#.replaceNodeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.replaceNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.replaceNodeAttributes('John', {
+          age: 23,
+          eyes: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 23,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.replaceEdgeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.replaceEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.replaceDirectedEdgeAttributes('John', 'Thomas', {
+          weight: 2
+        });
+        graph.replaceUndirectedEdgeAttributes('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3
+        });
+      },
+      'it should correctly replace attributes.': function itShouldCorrectlyReplaceAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.replaceEdgeAttributes(edge, {
+          weight: 4,
+          type: 'KNOWS'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 4,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.mergeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.mergeAttributes(true);
+        }, invalid());
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.mergeAttributes({
+          color: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph',
+          color: 'blue'
+        });
+      }
+    },
+    '#.mergeNodeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.mergeNodeAttributes('John', {
+          eyes: 'blue'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 45,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.mergeEdgeAttributes': {
+      'it should throw if given attributes are not a plain object.': function itShouldThrowIfGivenAttributesAreNotAPlainObject() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.mergeDirectedEdgeAttributes('John', 'Thomas', {
+          weight: 2
+        });
+        graph.mergeUndirectedEdgeAttributes('John', 'Thomas', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2,
+          test: 0
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3,
+          test: 0
+        });
+      },
+      'it should correctly merge attributes.': function itShouldCorrectlyMergeAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.mergeEdgeAttributes(edge, {
+          type: 'KNOWS'
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 1,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.updateAttributes': {
+      'it should throw if given updater is not a function.': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateAttribute(true);
+        }, invalid());
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('name', 'graph');
+        graph.updateAttributes(function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            color: 'blue'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph',
+          color: 'blue'
+        });
+      }
+    },
+    '#.updateNodeAttributes': {
+      'it should throw if given updater is not a function': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          graph.updateNodeAttributes('John', true);
+        }, invalid());
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 45
+        });
+        graph.updateNodeAttributes('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            eyes: 'blue'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          age: 45,
+          eyes: 'blue'
+        });
+      }
+    },
+    '#.updateEdgeAttributes': {
+      'it should throw if given updater is not a function.': function itShouldThrowIfGivenUpdaterIsNotAFunction() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeAttributes(edge, true);
+        }, invalid());
+      },
+      'it should also work with typed edges.': function itShouldAlsoWorkWithTypedEdges() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addDirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.addUndirectedEdge('John', 'Thomas', {
+          test: 0
+        });
+        graph.updateDirectedEdgeAttributes('John', 'Thomas', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 2
+          });
+        });
+        graph.updateUndirectedEdgeAttributes('John', 'Thomas', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 3
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getDirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 2,
+          test: 0
+        });
+
+        _assert["default"].deepStrictEqual(graph.getUndirectedEdgeAttributes('John', 'Thomas'), {
+          weight: 3,
+          test: 0
+        });
+      },
+      'it should correctly update attributes.': function itShouldCorrectlyUpdateAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        var edge = graph.addEdge('John', 'Martha', {
+          weight: 1
+        });
+        graph.updateEdgeAttributes(edge, function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            type: 'KNOWS'
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes(edge), {
+          weight: 1,
+          type: 'KNOWS'
+        });
+      }
+    },
+    '#.updateEachNodeAttributes': {
+      'it should throw when given invalid arguments.': function itShouldThrowWhenGivenInvalidArguments() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(null);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(Function.prototype, 'test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachNodeAttributes(Function.prototype, {
+            attributes: 'yes'
+          });
+        }, invalid());
+      },
+      "it should update each node's attributes.": function itShouldUpdateEachNodeSAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes().map(function (n) {
+          return graph.getNodeAttributes(n);
+        }), [{
+          age: 35
+        }, {
+          age: 57
+        }, {
+          age: 14
+        }]);
+      }
+    },
+    '#.updateEachEdgeAttributes': {
+      'it should throw when given invalid arguments.': function itShouldThrowWhenGivenInvalidArguments() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(null);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(Function.prototype, 'test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph.updateEachEdgeAttributes(Function.prototype, {
+            attributes: 'yes'
+          });
+        }, invalid());
+      },
+      "it should update each node's attributes.": function itShouldUpdateEachNodeSAttributes() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        graph.updateEachEdgeAttributes(function (edge, attr, source, _t, _sa, _ta, undirected) {
+          _assert["default"].strictEqual(source, 'John');
+
+          _assert["default"].strictEqual(undirected, false);
+
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.mapEdges(function (_, attr) {
+          return attr;
+        }), [{
+          weight: 2
+        }, {
+          weight: 11
+        }]);
+      }
+    }
+  });
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/events.js b/libs/shared/graph-layouts/node_modules/graphology/specs/events.js
new file mode 100644
index 0000000000000000000000000000000000000000..df9c4c44fca1e880288ccaedf0bd5494d0599a17
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/events.js
@@ -0,0 +1,426 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = events;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var VALID_TYPES = new Set(['set', 'merge', 'replace', 'remove']);
+
+function events(Graph) {
+  return {
+    nodeAdded: {
+      'it should fire when a node is added.': function itShouldFireWhenANodeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'John');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            age: 34
+          });
+        });
+        graph.on('nodeAdded', handler);
+        graph.addNode('John', {
+          age: 34
+        });
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    edgeAdded: {
+      'it should fire when an edge is added.': function itShouldFireWhenAnEdgeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'J->T');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            weight: 1
+          });
+
+          _assert["default"].strictEqual(data.source, 'John');
+
+          _assert["default"].strictEqual(data.target, 'Thomas');
+
+          _assert["default"].strictEqual(data.undirected, false);
+        });
+        graph.on('edgeAdded', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas', {
+          weight: 1
+        });
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    nodeDropped: {
+      'it should fire when a node is dropped.': function itShouldFireWhenANodeIsDropped() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'John');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            age: 34
+          });
+        });
+        graph.on('nodeDropped', handler);
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.dropNode('John');
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    edgeDropped: {
+      'it should fire when an edge is added.': function itShouldFireWhenAnEdgeIsAdded() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (data) {
+          _assert["default"].strictEqual(data.key, 'J->T');
+
+          _assert["default"].deepStrictEqual(data.attributes, {
+            weight: 1
+          });
+
+          _assert["default"].strictEqual(data.source, 'John');
+
+          _assert["default"].strictEqual(data.target, 'Thomas');
+
+          _assert["default"].strictEqual(data.undirected, false);
+        });
+        graph.on('edgeDropped', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas', {
+          weight: 1
+        });
+        graph.dropEdge('J->T');
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    cleared: {
+      'it should fire when the graph is cleared.': function itShouldFireWhenTheGraphIsCleared() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)();
+        graph.on('cleared', handler);
+        graph.clear();
+        (0, _assert["default"])(handler.called);
+      }
+    },
+    attributesUpdated: {
+      'it should fire when a graph attribute is updated.': function itShouldFireWhenAGraphAttributeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'name');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'name');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              author: 'John'
+            });
+          }
+
+          _assert["default"].deepStrictEqual(payload.attributes, graph.getAttributes());
+        });
+        graph.on('attributesUpdated', handler);
+        graph.setAttribute('name', 'Awesome graph');
+        graph.replaceAttributes({
+          name: 'Shitty graph'
+        });
+        graph.mergeAttributes({
+          author: 'John'
+        });
+        graph.removeAttribute('name');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      }
+    },
+    nodeAttributesUpdated: {
+      "it should fire when a node's attributes are updated.": function itShouldFireWhenANodeSAttributesAreUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.key, 'John');
+
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'age');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'eyes');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              eyes: 'blue'
+            });
+          }
+
+          _assert["default"].strictEqual(payload.attributes, graph.getNodeAttributes(payload.key));
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.addNode('John');
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.replaceNodeAttributes('John', {
+          age: 56
+        });
+        graph.mergeNodeAttributes('John', {
+          eyes: 'blue'
+        });
+        graph.removeNodeAttribute('John', 'eyes');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      },
+      'it should fire when a node is merged.': function itShouldFireWhenANodeIsMerged() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'merge',
+            key: 'John',
+            attributes: {
+              count: 2
+            },
+            data: {
+              count: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(payload.key), {
+            count: 2
+          });
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.mergeNode('John', {
+          count: 1
+        });
+        graph.mergeNode('John', {
+          count: 2
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should fire when a node is updated.': function itShouldFireWhenANodeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'replace',
+            key: 'John',
+            attributes: {
+              count: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(payload.key), {
+            count: 2
+          });
+        });
+        graph.on('nodeAttributesUpdated', handler);
+        graph.mergeNode('John', {
+          count: 1
+        });
+        graph.updateNode('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            count: attr.count + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    edgeAttributesUpdated: {
+      "it should fire when an edge's attributes are updated.": function itShouldFireWhenAnEdgeSAttributesAreUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.key, 'J->T');
+
+          (0, _assert["default"])(VALID_TYPES.has(payload.type));
+
+          if (payload.type === 'set') {
+            _assert["default"].strictEqual(payload.name, 'weight');
+          } else if (payload.type === 'remove') {
+            _assert["default"].strictEqual(payload.name, 'type');
+          } else if (payload.type === 'merge') {
+            _assert["default"].deepStrictEqual(payload.data, {
+              type: 'KNOWS'
+            });
+          }
+
+          _assert["default"].strictEqual(payload.attributes, graph.getEdgeAttributes(payload.key));
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdgeWithKey('J->T', 'John', 'Thomas');
+        graph.setEdgeAttribute('J->T', 'weight', 34);
+        graph.replaceEdgeAttributes('J->T', {
+          weight: 56
+        });
+        graph.mergeEdgeAttributes('J->T', {
+          type: 'KNOWS'
+        });
+        graph.removeEdgeAttribute('J->T', 'type');
+
+        _assert["default"].strictEqual(handler.times, 4);
+      },
+      'it should fire when an edge is merged.': function itShouldFireWhenAnEdgeIsMerged() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'merge',
+            key: graph.edge('John', 'Mary'),
+            attributes: {
+              weight: 2
+            },
+            data: {
+              weight: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getEdgeAttributes(payload.key), {
+            weight: 2
+          });
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        graph.mergeEdge('John', 'Mary', {
+          weight: 1
+        });
+        graph.mergeEdge('John', 'Mary', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should fire when an edge is updated.': function itShouldFireWhenAnEdgeIsUpdated() {
+        var graph = new Graph();
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload, {
+            type: 'replace',
+            key: 'j->m',
+            attributes: {
+              weight: 2
+            }
+          });
+
+          _assert["default"].deepStrictEqual(graph.getEdgeAttributes(payload.key), {
+            weight: 2
+          });
+        });
+        graph.on('edgeAttributesUpdated', handler);
+        graph.mergeEdgeWithKey('j->m', 'John', 'Mary', {
+          weight: 1
+        });
+        graph.updateEdgeWithKey('j->m', 'John', 'Mary', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    eachNodeAttributesUpdated: {
+      'it should fire when using #.updateEachNodeAttributes.': function itShouldFireWhenUsingUpdateEachNodeAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.hints, null);
+        });
+        graph.on('eachNodeAttributesUpdated', handler);
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should provide hints when user gave them.': function itShouldProvideHintsWhenUserGaveThem() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Mary', {
+          age: 56
+        });
+        graph.addNode('Suz', {
+          age: 13
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload.hints, {
+            attributes: ['age']
+          });
+        });
+        graph.on('eachNodeAttributesUpdated', handler);
+        graph.updateEachNodeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.age + 1
+          });
+        }, {
+          attributes: ['age']
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    },
+    eachEdgeAttributesUpdated: {
+      'it should fire when using #.updateEachEdgeAttributes.': function itShouldFireWhenUsingUpdateEachEdgeAttributes() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].strictEqual(payload.hints, null);
+        });
+        graph.on('eachEdgeAttributesUpdated', handler);
+        graph.updateEachEdgeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            age: attr.weight + 1
+          });
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      },
+      'it should provide hints when user gave them.': function itShouldProvideHintsWhenUserGaveThem() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey(0, 'John', 'Lucy', {
+          weight: 1
+        });
+        graph.mergeEdgeWithKey(1, 'John', 'Mary', {
+          weight: 10
+        });
+        var handler = (0, _helpers.spy)(function (payload) {
+          _assert["default"].deepStrictEqual(payload.hints, {
+            attributes: ['weight']
+          });
+        });
+        graph.on('eachEdgeAttributesUpdated', handler);
+        graph.updateEachEdgeAttributes(function (node, attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: attr.weight + 1
+          });
+        }, {
+          attributes: ['weight']
+        });
+
+        _assert["default"].strictEqual(handler.times, 1);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/helpers.js b/libs/shared/graph-layouts/node_modules/graphology/specs/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..5198d01d9110c19b4fa898f1ba162c77030e26e6
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/helpers.js
@@ -0,0 +1,101 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.addNodesFrom = addNodesFrom;
+exports.capitalize = capitalize;
+exports.deepMerge = deepMerge;
+exports.sameMembers = sameMembers;
+exports.spy = spy;
+
+function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
+
+/**
+ * Graphology Specs Helpers
+ * =========================
+ *
+ * Miscellaneous helpers to test more easily.
+ */
+
+/**
+ * Capitalize function.
+ */
+function capitalize(string) {
+  return string[0].toUpperCase() + string.slice(1);
+}
+/**
+ * Simplistic deep merger function.
+ */
+
+
+function deepMerge() {
+  for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) {
+    objects[_key] = arguments[_key];
+  }
+
+  var o = objects[0];
+  var t, i, l, k;
+
+  for (i = 1, l = objects.length; i < l; i++) {
+    t = objects[i];
+
+    for (k in t) {
+      if (_typeof(t[k]) === 'object') {
+        o[k] = deepMerge(o[k] || {}, t[k]);
+      } else {
+        o[k] = t[k];
+      }
+    }
+  }
+
+  return o;
+}
+/**
+ * Checking that two arrays have the same members.
+ */
+
+
+function sameMembers(a1, a2) {
+  if (a1.length !== a2.length) return false;
+  var set = new Set(a1);
+
+  for (var i = 0, l = a2.length; i < l; i++) {
+    if (!set.has(a2[i])) return false;
+  }
+
+  return true;
+}
+/**
+ * Function spying on the execution of the provided function to ease some
+ * tests, notably related to event handling.
+ *
+ * @param {function} target - Target function.
+ * @param {function}        - The spy.
+ */
+
+
+function spy(target) {
+  var fn = function fn() {
+    fn.called = true;
+    fn.times++;
+    if (typeof target === 'function') return target.apply(null, arguments);
+  };
+
+  fn.called = false;
+  fn.times = 0;
+  return fn;
+}
+/**
+ * Function adding multiple nodes from an array to the given graph.
+ *
+ * @param {Graph} graph - Target graph.
+ * @param {array} nodes - Node array.
+ */
+
+
+function addNodesFrom(graph, nodes) {
+  nodes.forEach(function (node) {
+    graph.addNode(node);
+  });
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/index.js b/libs/shared/graph-layouts/node_modules/graphology/specs/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..18b6595fc9c64727a989d1b977807f7f2d3fc845
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/index.js
@@ -0,0 +1,78 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = specs;
+
+var _instantiation = _interopRequireDefault(require("./instantiation"));
+
+var _properties = _interopRequireDefault(require("./properties"));
+
+var _read = _interopRequireDefault(require("./read"));
+
+var _mutation = _interopRequireDefault(require("./mutation"));
+
+var _attributes = _interopRequireDefault(require("./attributes"));
+
+var _iteration = _interopRequireDefault(require("./iteration"));
+
+var _serialization = _interopRequireDefault(require("./serialization"));
+
+var _events = _interopRequireDefault(require("./events"));
+
+var _utils = _interopRequireDefault(require("./utils"));
+
+var _known = _interopRequireDefault(require("./known"));
+
+var _misc = _interopRequireDefault(require("./misc"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Specs
+ * =================
+ *
+ * Unit tests factory taking the Graph object implementation.
+ */
+var createErrorChecker = function createErrorChecker(name) {
+  return function () {
+    return function (error) {
+      return error && error.name === name;
+    };
+  };
+};
+/**
+ * Returning the unit tests to run.
+ *
+ * @param  {string} path - Path to the implementation (should be absolute).
+ * @return {object}      - The tests to run with Mocha.
+ */
+
+
+function specs(Graph, implementation) {
+  var errors = [['invalid', 'InvalidArgumentsGraphError'], ['notFound', 'NotFoundGraphError'], ['usage', 'UsageGraphError']]; // Building error checkers
+
+  var errorCheckers = {};
+  errors.forEach(function (_ref) {
+    var fn = _ref[0],
+        name = _ref[1];
+    return errorCheckers[fn] = createErrorChecker(name);
+  });
+  var tests = {
+    Basic: {
+      Instantiation: (0, _instantiation["default"])(Graph, implementation, errorCheckers),
+      Properties: (0, _properties["default"])(Graph, errorCheckers),
+      Mutation: (0, _mutation["default"])(Graph, errorCheckers),
+      Read: (0, _read["default"])(Graph, errorCheckers),
+      Attributes: (0, _attributes["default"])(Graph, errorCheckers),
+      Iteration: (0, _iteration["default"])(Graph, errorCheckers),
+      Serialization: (0, _serialization["default"])(Graph, errorCheckers),
+      Events: (0, _events["default"])(Graph),
+      Utils: (0, _utils["default"])(Graph, errorCheckers),
+      'Known Methods': (0, _known["default"])(Graph, errorCheckers),
+      Miscellaneous: (0, _misc["default"])(Graph)
+    }
+  };
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/instantiation.js b/libs/shared/graph-layouts/node_modules/graphology/specs/instantiation.js
new file mode 100644
index 0000000000000000000000000000000000000000..45945a419fc28d227c34ec7e3c04a144811f13f0
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/instantiation.js
@@ -0,0 +1,211 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = instantiation;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/* eslint no-unused-vars: 0 */
+
+/**
+ * Graphology Instantiation Specs
+ * ===============================
+ *
+ * Testing the instantiation of the graph.
+ */
+var CONSTRUCTORS = ['DirectedGraph', 'UndirectedGraph', 'MultiGraph', 'MultiDirectedGraph', 'MultiUndirectedGraph'];
+var OPTIONS = [{
+  multi: false,
+  type: 'directed'
+}, {
+  multi: false,
+  type: 'undirected'
+}, {
+  multi: true,
+  type: 'mixed'
+}, {
+  multi: true,
+  type: 'directed'
+}, {
+  multi: true,
+  type: 'undirected'
+}];
+
+function instantiation(Graph, implementation, checkers) {
+  var invalid = checkers.invalid;
+  return {
+    'Static #.from method': {
+      'it should be possible to create a Graph from a Graph instance.': function itShouldBePossibleToCreateAGraphFromAGraphInstance() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var other = Graph.from(graph);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), other.nodes());
+
+        _assert["default"].deepStrictEqual(graph.edges(), other.edges());
+      },
+      'it should be possible to create a Graph from a serialized graph': function itShouldBePossibleToCreateAGraphFromASerializedGraph() {
+        var graph = Graph.from({
+          nodes: [{
+            key: 'John'
+          }, {
+            key: 'Thomas'
+          }],
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to provide options.': function itShouldBePossibleToProvideOptions() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          }
+        }, {
+          type: 'directed'
+        });
+
+        _assert["default"].strictEqual(graph.type, 'directed');
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      },
+      'it should be possible to take options from the serialized format.': function itShouldBePossibleToTakeOptionsFromTheSerializedFormat() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          },
+          options: {
+            type: 'directed',
+            multi: true
+          }
+        });
+
+        _assert["default"].strictEqual(graph.type, 'directed');
+
+        _assert["default"].strictEqual(graph.multi, true);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      },
+      'given options should take precedence over the serialization ones.': function givenOptionsShouldTakePrecedenceOverTheSerializationOnes() {
+        var graph = Graph.from({
+          node: [{
+            key: 'John'
+          }],
+          attributes: {
+            name: 'Awesome graph'
+          },
+          options: {
+            type: 'directed',
+            multi: true
+          }
+        }, {
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(graph.type, 'undirected');
+
+        _assert["default"].strictEqual(graph.multi, true);
+
+        _assert["default"].strictEqual(graph.getAttribute('name'), 'Awesome graph');
+      }
+    },
+    Options: {
+      /**
+       * allowSelfLoops
+       */
+      allowSelfLoops: {
+        'providing a non-boolean value should throw.': function providingANonBooleanValueShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              allowSelfLoops: 'test'
+            });
+          }, invalid());
+        }
+      },
+
+      /**
+       * multi
+       */
+      multi: {
+        'providing a non-boolean value should throw.': function providingANonBooleanValueShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              multi: 'test'
+            });
+          }, invalid());
+        }
+      },
+
+      /**
+       * type
+       */
+      type: {
+        'providing an invalid type should throw.': function providingAnInvalidTypeShouldThrow() {
+          _assert["default"]["throws"](function () {
+            var graph = new Graph({
+              type: 'test'
+            });
+          }, invalid());
+        }
+      }
+    },
+    Constructors: {
+      'all alternative constructors should be available.': function allAlternativeConstructorsShouldBeAvailable() {
+        CONSTRUCTORS.forEach(function (name) {
+          return (0, _assert["default"])(name in implementation);
+        });
+      },
+      'alternative constructors should have the correct options.': function alternativeConstructorsShouldHaveTheCorrectOptions() {
+        CONSTRUCTORS.forEach(function (name, index) {
+          var graph = new implementation[name]();
+          var _OPTIONS$index = OPTIONS[index],
+              multi = _OPTIONS$index.multi,
+              type = _OPTIONS$index.type;
+
+          _assert["default"].strictEqual(graph.multi, multi);
+
+          _assert["default"].strictEqual(graph.type, type);
+        });
+      },
+      'alternative constructors should throw if given inconsistent options.': function alternativeConstructorsShouldThrowIfGivenInconsistentOptions() {
+        CONSTRUCTORS.forEach(function (name, index) {
+          var _OPTIONS$index2 = OPTIONS[index],
+              multi = _OPTIONS$index2.multi,
+              type = _OPTIONS$index2.type;
+
+          _assert["default"]["throws"](function () {
+            var graph = new implementation[name]({
+              multi: !multi
+            });
+          }, invalid());
+
+          if (type === 'mixed') return;
+
+          _assert["default"]["throws"](function () {
+            var graph = new implementation[name]({
+              type: type === 'directed' ? 'undirected' : 'directed'
+            });
+          }, invalid());
+        });
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/edges.js b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/edges.js
new file mode 100644
index 0000000000000000000000000000000000000000..8210ea7f2bbc2de7231af1f727c18e9ce3ad6623
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/edges.js
@@ -0,0 +1,894 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = edgesIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _take = _interopRequireDefault(require("obliterator/take"));
+
+var _map = _interopRequireDefault(require("obliterator/map"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var METHODS = ['edges', 'inEdges', 'outEdges', 'inboundEdges', 'outboundEdges', 'directedEdges', 'undirectedEdges'];
+
+function edgesIteration(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound;
+  var graph = new Graph({
+    multi: true
+  });
+  (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas', 'Martha', 'Roger', 'Catherine', 'Alone', 'Forever']);
+  graph.replaceNodeAttributes('John', {
+    age: 13
+  });
+  graph.replaceNodeAttributes('Martha', {
+    age: 15
+  });
+  graph.addDirectedEdgeWithKey('J->T', 'John', 'Thomas', {
+    weight: 14
+  });
+  graph.addDirectedEdgeWithKey('J->M', 'John', 'Martha');
+  graph.addDirectedEdgeWithKey('C->J', 'Catherine', 'John');
+  graph.addUndirectedEdgeWithKey('M<->R', 'Martha', 'Roger');
+  graph.addUndirectedEdgeWithKey('M<->J', 'Martha', 'John');
+  graph.addUndirectedEdgeWithKey('J<->R', 'John', 'Roger');
+  graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+  var ALL_EDGES = ['J->T', 'J->M', 'C->J', 'M<->R', 'M<->J', 'J<->R', 'T<->M'];
+  var ALL_DIRECTED_EDGES = ['J->T', 'J->M', 'C->J'];
+  var ALL_UNDIRECTED_EDGES = ['M<->R', 'M<->J', 'J<->R', 'T<->M'];
+  var TEST_DATA = {
+    edges: {
+      all: ALL_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J', 'J->T', 'J->M', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M', 'M<->J']
+      }
+    },
+    inEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: []
+      }
+    },
+    outEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['J->T', 'J->M']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M']
+      }
+    },
+    inboundEdges: {
+      all: ALL_DIRECTED_EDGES.concat(ALL_UNDIRECTED_EDGES),
+      node: {
+        key: 'John',
+        edges: ['C->J', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['M<->J']
+      }
+    },
+    outboundEdges: {
+      all: ALL_DIRECTED_EDGES.concat(ALL_UNDIRECTED_EDGES),
+      node: {
+        key: 'John',
+        edges: ['J->T', 'J->M', 'M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M', 'M<->J']
+      }
+    },
+    directedEdges: {
+      all: ALL_DIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['C->J', 'J->T', 'J->M']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['J->M']
+      }
+    },
+    undirectedEdges: {
+      all: ALL_UNDIRECTED_EDGES,
+      node: {
+        key: 'John',
+        edges: ['M<->J', 'J<->R']
+      },
+      path: {
+        source: 'John',
+        target: 'Martha',
+        edges: ['M<->J']
+      }
+    }
+  };
+
+  function commonTests(name) {
+    return _defineProperty({}, '#.' + name, {
+      'it should throw if too many arguments are provided.': function itShouldThrowIfTooManyArgumentsAreProvided() {
+        _assert["default"]["throws"](function () {
+          graph[name](1, 2, 3);
+        }, invalid());
+      },
+      'it should throw when the node is not found.': function itShouldThrowWhenTheNodeIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test');
+        }, notFound());
+      },
+      'it should throw if either source or target is not found.': function itShouldThrowIfEitherSourceOrTargetIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test', 'Alone');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph[name]('Alone', 'Test');
+        }, notFound());
+      }
+    });
+  }
+
+  function specificTests(name, data) {
+    var _ref2;
+
+    var capitalized = name[0].toUpperCase() + name.slice(1, -1);
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    var forEachName = 'forEach' + capitalized;
+    var findName = 'find' + capitalized;
+    var mapName = 'map' + capitalized + 's';
+    var filterName = 'filter' + capitalized + 's';
+    var reduceName = 'reduce' + capitalized + 's';
+    var someName = 'some' + capitalized;
+    var everyName = 'every' + capitalized;
+    return _ref2 = {}, _defineProperty(_ref2, '#.' + name, {
+      'it should return all the relevant edges.': function itShouldReturnAllTheRelevantEdges() {
+        var edges = graph[name]().sort();
+
+        _assert["default"].deepStrictEqual(edges, data.all.slice().sort());
+      },
+      "it should return a node's relevant edges.": function itShouldReturnANodeSRelevantEdges() {
+        var edges = graph[name](data.node.key);
+
+        _assert["default"].deepStrictEqual(edges, data.node.edges);
+
+        _assert["default"].deepStrictEqual(graph[name]('Alone'), []);
+      },
+      'it should return all the relevant edges between source & target.': function itShouldReturnAllTheRelevantEdgesBetweenSourceTarget() {
+        var edges = graph[name](data.path.source, data.path.target);
+        (0, _assert["default"])((0, _helpers.sameMembers)(edges, data.path.edges));
+
+        _assert["default"].deepStrictEqual(graph[name]('Forever', 'Alone'), []);
+      }
+    }), _defineProperty(_ref2, '#.' + forEachName, {
+      'it should possible to use callback iterators.': function itShouldPossibleToUseCallbackIterators() {
+        var edges = [];
+        graph[forEachName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        edges.sort();
+
+        _assert["default"].deepStrictEqual(edges, data.all.slice().sort());
+      },
+      "it should be possible to use callback iterators over a node's relevant edges.": function itShouldBePossibleToUseCallbackIteratorsOverANodeSRelevantEdges() {
+        var edges = [];
+        graph[forEachName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        edges.sort();
+
+        _assert["default"].deepStrictEqual(edges, data.node.edges.slice().sort());
+      },
+      'it should be possible to use callback iterators over all the relevant edges between source & target.': function itShouldBePossibleToUseCallbackIteratorsOverAllTheRelevantEdgesBetweenSourceTarget() {
+        var edges = [];
+        graph[forEachName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+        });
+        (0, _assert["default"])((0, _helpers.sameMembers)(edges, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + mapName, {
+      'it should possible to map edges.': function itShouldPossibleToMapEdges() {
+        var result = graph[mapName](function (key) {
+          return key;
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.all.slice().sort());
+      },
+      "it should be possible to map a node's relevant edges.": function itShouldBePossibleToMapANodeSRelevantEdges() {
+        var result = graph[mapName](data.node.key, function (key) {
+          return key;
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.node.edges.slice().sort());
+      },
+      'it should be possible to map the relevant edges between source & target.': function itShouldBePossibleToMapTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[mapName](data.path.source, data.path.target, function (key) {
+          return key;
+        });
+        result.sort();
+        (0, _assert["default"])((0, _helpers.sameMembers)(result, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + filterName, {
+      'it should possible to filter edges.': function itShouldPossibleToFilterEdges() {
+        var result = graph[filterName](function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.all.slice().sort());
+      },
+      "it should be possible to filter a node's relevant edges.": function itShouldBePossibleToFilterANodeSRelevantEdges() {
+        var result = graph[filterName](data.node.key, function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+
+        _assert["default"].deepStrictEqual(result, data.node.edges.slice().sort());
+      },
+      'it should be possible to filter the relevant edges between source & target.': function itShouldBePossibleToFilterTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[filterName](data.path.source, data.path.target, function (key) {
+          return data.all.includes(key);
+        });
+        result.sort();
+        (0, _assert["default"])((0, _helpers.sameMembers)(result, data.path.edges));
+      }
+    }), _defineProperty(_ref2, '#.' + reduceName, {
+      'it should throw when given bad arguments.': function itShouldThrowWhenGivenBadArguments() {
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('test');
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName](1, 2, 3, 4, 5);
+        }, invalid());
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('notafunction', 0);
+        }, TypeError);
+
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('test', function () {
+            return true;
+          });
+        }, invalid());
+      },
+      'it should possible to reduce edges.': function itShouldPossibleToReduceEdges() {
+        var result = graph[reduceName](function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.all.length);
+      },
+      "it should be possible to reduce a node's relevant edges.": function itShouldBePossibleToReduceANodeSRelevantEdges() {
+        var result = graph[reduceName](data.node.key, function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.node.edges.length);
+      },
+      'it should be possible to reduce the relevant edges between source & target.': function itShouldBePossibleToReduceTheRelevantEdgesBetweenSourceTarget() {
+        var result = graph[reduceName](data.path.source, data.path.target, function (x) {
+          return x + 1;
+        }, 0);
+
+        _assert["default"].strictEqual(result, data.path.edges.length);
+      }
+    }), _defineProperty(_ref2, '#.' + findName, {
+      'it should possible to find an edge.': function itShouldPossibleToFindAnEdge() {
+        var edges = [];
+        var found = graph[findName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[findName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      },
+      "it should be possible to find a node's edge.": function itShouldBePossibleToFindANodeSEdge() {
+        var edges = [];
+        var found = graph[findName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[findName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      },
+      'it should be possible to find an edge between source & target.': function itShouldBePossibleToFindAnEdgeBetweenSourceTarget() {
+        var edges = [];
+        var found = graph[findName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, edges[0]);
+
+        _assert["default"].strictEqual(edges.length, graph[name](data.path.source, data.path.target).length ? 1 : 0);
+
+        found = graph[findName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    }), _defineProperty(_ref2, '#.' + someName, {
+      'it should possible to assert whether any edge matches a predicate.': function itShouldPossibleToAssertWhetherAnyEdgeMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[someName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      "it should possible to assert whether any node's edge matches a predicate.": function itShouldPossibleToAssertWhetherAnyNodeSEdgeMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        _assert["default"].strictEqual(edges.length, 1);
+
+        found = graph[someName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should possible to assert whether any edge between source & target matches a predicate.': function itShouldPossibleToAssertWhetherAnyEdgeBetweenSourceTargetMatchesAPredicate() {
+        var edges = [];
+        var found = graph[someName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, graph[name](data.path.source, data.path.target).length !== 0);
+
+        _assert["default"].strictEqual(edges.length, graph[name](data.path.source, data.path.target).length ? 1 : 0);
+
+        found = graph[someName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should always return false on empty sets.': function itShouldAlwaysReturnFalseOnEmptySets() {
+        var empty = new Graph();
+
+        _assert["default"].strictEqual(empty[someName](function () {
+          return true;
+        }), false);
+      }
+    }), _defineProperty(_ref2, '#.' + everyName, {
+      'it should possible to assert whether all edges matches a predicate.': function itShouldPossibleToAssertWhetherAllEdgesMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      "it should possible to assert whether all of a node's edges matches a predicate.": function itShouldPossibleToAssertWhetherAllOfANodeSEdgesMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](data.node.key, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, false);
+      },
+      'it should possible to assert whether all edges between source & target matches a predicate.': function itShouldPossibleToAssertWhetherAllEdgesBetweenSourceTargetMatchesAPredicate() {
+        var edges = [];
+        var found = graph[everyName](data.path.source, data.path.target, function (key, attributes, source, target, sA, tA, u) {
+          edges.push(key);
+
+          _assert["default"].deepStrictEqual(attributes, key === 'J->T' ? {
+            weight: 14
+          } : {});
+
+          _assert["default"].strictEqual(source, graph.source(key));
+
+          _assert["default"].strictEqual(target, graph.target(key));
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(source), sA);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), tA);
+
+          _assert["default"].strictEqual(graph.isUndirected(key), u);
+
+          return true;
+        });
+        var isEmpty = graph[name](data.path.source, data.path.target).length === 0;
+
+        _assert["default"].strictEqual(found, true);
+
+        found = graph[everyName](data.path.source, data.path.target, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, isEmpty ? true : false);
+      },
+      'it should always return true on empty sets.': function itShouldAlwaysReturnTrueOnEmptySets() {
+        var empty = new Graph();
+
+        _assert["default"].strictEqual(empty[everyName](function () {
+          return true;
+        }), true);
+      }
+    }), _defineProperty(_ref2, '#.' + iteratorName, {
+      'it should be possible to return an iterator over the relevant edges.': function itShouldBePossibleToReturnAnIteratorOverTheRelevantEdges() {
+        var iterator = graph[iteratorName]();
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.all.map(function (edge) {
+          var _graph$extremities = graph.extremities(edge),
+              source = _graph$extremities[0],
+              target = _graph$extremities[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      },
+      "it should be possible to return an iterator over a node's relevant edges.": function itShouldBePossibleToReturnAnIteratorOverANodeSRelevantEdges() {
+        var iterator = graph[iteratorName](data.node.key);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.node.edges.map(function (edge) {
+          var _graph$extremities2 = graph.extremities(edge),
+              source = _graph$extremities2[0],
+              target = _graph$extremities2[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      },
+      'it should be possible to return an iterator over relevant edges between source & target.': function itShouldBePossibleToReturnAnIteratorOverRelevantEdgesBetweenSourceTarget() {
+        var iterator = graph[iteratorName](data.path.source, data.path.target);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.path.edges.map(function (edge) {
+          var _graph$extremities3 = graph.extremities(edge),
+              source = _graph$extremities3[0],
+              target = _graph$extremities3[1];
+
+          return {
+            edge: edge,
+            attributes: graph.getEdgeAttributes(edge),
+            source: source,
+            target: target,
+            sourceAttributes: graph.getNodeAttributes(source),
+            targetAttributes: graph.getNodeAttributes(target),
+            undirected: graph.isUndirected(edge)
+          };
+        }));
+      }
+    }), _ref2;
+  }
+
+  var tests = {
+    Miscellaneous: {
+      'simple graph indices should work.': function simpleGraphIndicesShouldWork() {
+        var simpleGraph = new Graph();
+        (0, _helpers.addNodesFrom)(simpleGraph, [1, 2, 3, 4]);
+        simpleGraph.addEdgeWithKey('1->2', 1, 2);
+        simpleGraph.addEdgeWithKey('1->3', 1, 3);
+        simpleGraph.addEdgeWithKey('1->4', 1, 4);
+
+        _assert["default"].deepStrictEqual(simpleGraph.edges(1), ['1->2', '1->3', '1->4']);
+      },
+      'it should also work with typed graphs.': function itShouldAlsoWorkWithTypedGraphs() {
+        var undirected = new Graph({
+          type: 'undirected'
+        }),
+            directed = new Graph({
+          type: 'directed'
+        });
+        undirected.mergeEdgeWithKey('1--2', 1, 2);
+        directed.mergeEdgeWithKey('1->2', 1, 2);
+
+        _assert["default"].deepStrictEqual(undirected.edges(1, 2), ['1--2']);
+
+        _assert["default"].deepStrictEqual(directed.edges(1, 2), ['1->2']);
+      },
+      'self loops should appear when using #.inEdges and should appear only once with #.edges.': function selfLoopsShouldAppearWhenUsingInEdgesAndShouldAppearOnlyOnceWithEdges() {
+        var directed = new Graph({
+          type: 'directed'
+        });
+        directed.addNode('Lucy');
+        directed.addEdgeWithKey('Lucy', 'Lucy', 'Lucy');
+
+        _assert["default"].deepStrictEqual(directed.inEdges('Lucy'), ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.inEdgeEntries('Lucy')).map(function (x) {
+          return x.edge;
+        }), ['Lucy']);
+
+        var edges = [];
+        directed.forEachInEdge('Lucy', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(directed.edges('Lucy'), ['Lucy']);
+
+        edges = [];
+        directed.forEachEdge('Lucy', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.edgeEntries('Lucy')).map(function (x) {
+          return x.edge;
+        }), ['Lucy']);
+      },
+      'it should be possible to retrieve self loops.': function itShouldBePossibleToRetrieveSelfLoops() {
+        var loopy = new Graph();
+        loopy.addNode('John');
+        loopy.addEdgeWithKey('d', 'John', 'John');
+        loopy.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].deepStrictEqual(new Set(loopy.edges('John', 'John')), new Set(['d', 'u']));
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('John', 'John'), ['d']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('John', 'John'), ['u']);
+
+        var edges = [];
+        loopy.forEachDirectedEdge('John', 'John', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['d']);
+
+        edges = [];
+        loopy.forEachUndirectedEdge('John', 'John', function (edge) {
+          edges.push(edge);
+        });
+
+        _assert["default"].deepStrictEqual(edges, ['u']);
+      },
+      'self loops in multi graphs should work properly (#352).': function selfLoopsInMultiGraphsShouldWorkProperly352() {
+        var loopy = new Graph({
+          multi: true
+        });
+        loopy.addNode('n');
+        loopy.addEdgeWithKey('e1', 'n', 'n');
+        loopy.addEdgeWithKey('e2', 'n', 'n');
+        loopy.addUndirectedEdgeWithKey('e3', 'n', 'n'); // Arrays
+
+        _assert["default"].deepStrictEqual(loopy.edges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outboundEdges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.inboundEdges('n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.inEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('n'), ['e3']);
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.edges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outboundEdges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.inboundEdges('n', 'n'), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(loopy.outEdges('n', 'n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.inEdges('n', 'n'), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(loopy.undirectedEdges('n', 'n'), ['e3']);
+
+        _assert["default"].deepStrictEqual(loopy.directedEdges('n', 'n'), ['e2', 'e1']); // Iterators
+
+
+        var mapKeys = function mapKeys(it) {
+          return (0, _take["default"])((0, _map["default"])(it, function (e) {
+            return e.edge;
+          }));
+        };
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.edgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outboundEdgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inboundEdgeEntries('n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.undirectedEdgeEntries('n')), ['e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.directedEdgeEntries('n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.edgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outboundEdgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inboundEdgeEntries('n', 'n')), ['e2', 'e1', 'e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.outEdgeEntries('n', 'n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.inEdgeEntries('n', 'n')), ['e2', 'e1']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.undirectedEdgeEntries('n', 'n')), ['e3']);
+
+        _assert["default"].deepStrictEqual(mapKeys(loopy.directedEdgeEntries('n', 'n')), ['e2', 'e1']);
+      },
+      'findOutboundEdge should work on multigraphs (#319).': function findOutboundEdgeShouldWorkOnMultigraphs319() {
+        var loopy = new Graph({
+          multi: true
+        });
+        loopy.mergeEdgeWithKey('e1', 'n', 'm');
+        loopy.mergeEdgeWithKey('e2', 'n', 'n');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge(function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge('n', function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+
+        _assert["default"].strictEqual(loopy.findOutboundEdge('n', 'n', function (_e, _a, s, t) {
+          return s === t;
+        }), 'e2');
+      }
+    }
+  }; // Common tests
+
+  METHODS.forEach(function (name) {
+    return (0, _helpers.deepMerge)(tests, commonTests(name));
+  }); // Specific tests
+
+  for (var name in TEST_DATA) {
+    (0, _helpers.deepMerge)(tests, specificTests(name, TEST_DATA[name]));
+  }
+
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/index.js b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf2e892c5affbf78127c05c101c06732b023b6c0
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/index.js
@@ -0,0 +1,161 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = iteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _nodes = _interopRequireDefault(require("./nodes"));
+
+var _edges = _interopRequireDefault(require("./edges"));
+
+var _neighbors = _interopRequireDefault(require("./neighbors"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Iteration Specs
+ * ===========================
+ *
+ * Testing the iteration-related methods of the graph.
+ */
+function iteration(Graph, checkers) {
+  return {
+    Adjacency: {
+      '#.forEachAdjacencyEntry': {
+        'it should iterate over the relevant elements.': function itShouldIterateOverTheRelevantElements() {
+          function test(multi) {
+            var graph = new Graph({
+              multi: multi
+            });
+            graph.addNode('John', {
+              hello: 'world'
+            });
+
+            var _graph$mergeUndirecte = graph.mergeUndirectedEdge('John', 'Mary', {
+              weight: 3
+            }),
+                e1 = _graph$mergeUndirecte[0];
+
+            graph.mergeUndirectedEdge('Thomas', 'John');
+            graph.mergeDirectedEdge('John', 'Thomas');
+            var count = 0;
+            graph.forEachAdjacencyEntry(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+
+              if (node === 'John') {
+                _assert["default"].deepStrictEqual(attr, {
+                  hello: 'world'
+                });
+              } else {
+                _assert["default"].deepStrictEqual(attr, {});
+              }
+
+              if (neighbor === 'John') {
+                _assert["default"].deepStrictEqual(neighborAttr, {
+                  hello: 'world'
+                });
+              } else {
+                _assert["default"].deepStrictEqual(neighborAttr, {});
+              }
+
+              if (edge === e1) {
+                _assert["default"].deepStrictEqual(edgeAttr, {
+                  weight: 3
+                });
+              } else {
+                _assert["default"].deepStrictEqual(edgeAttr, {});
+              }
+
+              _assert["default"].strictEqual(graph.isUndirected(edge), undirected);
+            });
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize * 2);
+
+            graph.addNode('Disconnected');
+            count = 0;
+            graph.forEachAdjacencyEntryWithOrphans(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+              if (node !== 'Disconnected') return;
+
+              _assert["default"].strictEqual(neighbor, null);
+
+              _assert["default"].strictEqual(neighborAttr, null);
+
+              _assert["default"].strictEqual(edge, null);
+
+              _assert["default"].strictEqual(edgeAttr, null);
+
+              _assert["default"].strictEqual(undirected, null);
+            }, true);
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize * 2 + 1);
+          }
+
+          test(false);
+          test(true);
+        }
+      },
+      '#.forEachAssymetricAdjacencyEntry': {
+        'it should iterate over the relevant elements.': function itShouldIterateOverTheRelevantElements() {
+          function test(multi) {
+            var graph = new Graph({
+              multi: multi
+            });
+            graph.addNode('John', {
+              hello: 'world'
+            });
+            graph.mergeUndirectedEdge('John', 'Mary', {
+              weight: 3
+            });
+            graph.mergeUndirectedEdge('Thomas', 'John');
+            graph.mergeDirectedEdge('John', 'Thomas');
+            var edges = [];
+            graph.forEachAssymetricAdjacencyEntry(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              if (undirected) {
+                _assert["default"].strictEqual(node < neighbor, true);
+              }
+
+              edges.push(edge);
+            });
+
+            _assert["default"].strictEqual(edges.length, graph.directedSize + graph.undirectedSize);
+
+            _assert["default"].deepStrictEqual(new Set(edges).size, edges.length);
+
+            graph.addNode('Disconnected');
+            var count = 0;
+            var nulls = 0;
+            graph.forEachAssymetricAdjacencyEntryWithOrphans(function (node, neighbor, attr, neighborAttr, edge, edgeAttr, undirected) {
+              count++;
+              if (neighbor) return;
+              nulls++;
+
+              _assert["default"].strictEqual(neighbor, null);
+
+              _assert["default"].strictEqual(neighborAttr, null);
+
+              _assert["default"].strictEqual(edge, null);
+
+              _assert["default"].strictEqual(edgeAttr, null);
+
+              _assert["default"].strictEqual(undirected, null);
+            }, true);
+
+            _assert["default"].strictEqual(count, graph.directedSize + graph.undirectedSize + 3);
+
+            _assert["default"].strictEqual(nulls, 3);
+          }
+
+          test(false);
+          test(true);
+        }
+      }
+    },
+    Nodes: (0, _nodes["default"])(Graph, checkers),
+    Edges: (0, _edges["default"])(Graph, checkers),
+    Neighbors: (0, _neighbors["default"])(Graph, checkers)
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/neighbors.js b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/neighbors.js
new file mode 100644
index 0000000000000000000000000000000000000000..7948bd77c1e018dc351142e1f8dfc014d04bbdc6
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/neighbors.js
@@ -0,0 +1,284 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = neighborsIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _take = _interopRequireDefault(require("obliterator/take"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var METHODS = ['neighbors', 'inNeighbors', 'outNeighbors', 'inboundNeighbors', 'outboundNeighbors', 'directedNeighbors', 'undirectedNeighbors'];
+
+function neighborsIteration(Graph, checkers) {
+  var notFound = checkers.notFound,
+      invalid = checkers.invalid;
+  var graph = new Graph({
+    multi: true
+  });
+  (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas', 'Martha', 'Roger', 'Catherine', 'Alone', 'Forever']);
+  graph.replaceNodeAttributes('John', {
+    age: 34
+  });
+  graph.replaceNodeAttributes('Martha', {
+    age: 35
+  });
+  graph.addDirectedEdgeWithKey('J->T', 'John', 'Thomas');
+  graph.addDirectedEdgeWithKey('J->M', 'John', 'Martha');
+  graph.addDirectedEdgeWithKey('C->J', 'Catherine', 'John');
+  graph.addUndirectedEdgeWithKey('M<->R', 'Martha', 'Roger');
+  graph.addUndirectedEdgeWithKey('M<->J', 'Martha', 'John');
+  graph.addUndirectedEdgeWithKey('J<->R', 'John', 'Roger');
+  graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+  var TEST_DATA = {
+    neighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Thomas', 'Martha', 'Roger']
+      }
+    },
+    inNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine']
+      }
+    },
+    outNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Thomas', 'Martha']
+      }
+    },
+    inboundNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Martha', 'Roger']
+      }
+    },
+    outboundNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Thomas', 'Martha', 'Roger']
+      }
+    },
+    directedNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Catherine', 'Thomas', 'Martha']
+      }
+    },
+    undirectedNeighbors: {
+      node: {
+        key: 'John',
+        neighbors: ['Martha', 'Roger']
+      }
+    }
+  };
+
+  function commonTests(name) {
+    return _defineProperty({}, '#.' + name, {
+      'it should throw when the node is not found.': function itShouldThrowWhenTheNodeIsNotFound() {
+        _assert["default"]["throws"](function () {
+          graph[name]('Test');
+        }, notFound());
+
+        if (~name.indexOf('count')) return;
+
+        _assert["default"]["throws"](function () {
+          graph[name]('Test', 'SecondTest');
+        }, notFound());
+      }
+    });
+  }
+
+  function specificTests(name, data) {
+    var _ref2;
+
+    var capitalized = name[0].toUpperCase() + name.slice(1, -1);
+    var forEachName = 'forEach' + capitalized;
+    var findName = 'find' + capitalized;
+    var iteratorName = name.slice(0, -1) + 'Entries';
+    var areName = 'are' + capitalized + 's';
+    var mapName = 'map' + capitalized + 's';
+    var filterName = 'filter' + capitalized + 's';
+    var reduceName = 'reduce' + capitalized + 's';
+    var someName = 'some' + capitalized;
+    var everyName = 'every' + capitalized;
+    return _ref2 = {}, _defineProperty(_ref2, '#.' + name, {
+      'it should return the correct neighbors array.': function itShouldReturnTheCorrectNeighborsArray() {
+        var neighbors = graph[name](data.node.key);
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors);
+
+        _assert["default"].deepStrictEqual(graph[name]('Alone'), []);
+      }
+    }), _defineProperty(_ref2, '#.' + forEachName, {
+      'it should be possible to iterate over neighbors using a callback.': function itShouldBePossibleToIterateOverNeighborsUsingACallback() {
+        var neighbors = [];
+        graph[forEachName](data.node.key, function (target, attrs) {
+          neighbors.push(target);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), attrs);
+
+          _assert["default"].strictEqual(graph[areName](data.node.key, target), true);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + mapName, {
+      'it should be possible to map neighbors using a callback.': function itShouldBePossibleToMapNeighborsUsingACallback() {
+        var result = graph[mapName](data.node.key, function (target) {
+          return target;
+        });
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + filterName, {
+      'it should be possible to filter neighbors using a callback.': function itShouldBePossibleToFilterNeighborsUsingACallback() {
+        var result = graph[filterName](data.node.key, function () {
+          return true;
+        });
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+
+        result = graph[filterName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].deepStrictEqual(result, []);
+      }
+    }), _defineProperty(_ref2, '#.' + reduceName, {
+      'it sould throw if not given an initial value.': function itSouldThrowIfNotGivenAnInitialValue() {
+        _assert["default"]["throws"](function () {
+          graph[reduceName]('node', function () {
+            return true;
+          });
+        }, invalid());
+      },
+      'it should be possible to reduce neighbors using a callback.': function itShouldBePossibleToReduceNeighborsUsingACallback() {
+        var result = graph[reduceName](data.node.key, function (acc, key) {
+          return acc.concat(key);
+        }, []);
+
+        _assert["default"].deepStrictEqual(result, data.node.neighbors);
+      }
+    }), _defineProperty(_ref2, '#.' + findName, {
+      'it should be possible to find neighbors.': function itShouldBePossibleToFindNeighbors() {
+        var neighbors = [];
+        var found = graph[findName](data.node.key, function (target, attrs) {
+          neighbors.push(target);
+
+          _assert["default"].deepStrictEqual(graph.getNodeAttributes(target), attrs);
+
+          _assert["default"].strictEqual(graph[areName](data.node.key, target), true);
+
+          return true;
+        });
+
+        _assert["default"].strictEqual(found, neighbors[0]);
+
+        _assert["default"].deepStrictEqual(neighbors, data.node.neighbors.slice(0, 1));
+
+        found = graph[findName](data.node.key, function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    }), _defineProperty(_ref2, '#.' + someName, {
+      'it should always return false on empty set.': function itShouldAlwaysReturnFalseOnEmptySet() {
+        var loneGraph = new Graph();
+        loneGraph.addNode('alone');
+
+        _assert["default"].strictEqual(loneGraph[someName]('alone', function () {
+          return true;
+        }), false);
+      },
+      'it should be possible to assert whether any neighbor matches a predicate.': function itShouldBePossibleToAssertWhetherAnyNeighborMatchesAPredicate() {
+        _assert["default"].strictEqual(graph[someName](data.node.key, function () {
+          return true;
+        }), data.node.neighbors.length > 0);
+      }
+    }), _defineProperty(_ref2, '#.' + everyName, {
+      'it should always return true on empty set.': function itShouldAlwaysReturnTrueOnEmptySet() {
+        var loneGraph = new Graph();
+        loneGraph.addNode('alone');
+
+        _assert["default"].strictEqual(loneGraph[everyName]('alone', function () {
+          return true;
+        }), true);
+      },
+      'it should be possible to assert whether any neighbor matches a predicate.': function itShouldBePossibleToAssertWhetherAnyNeighborMatchesAPredicate() {
+        _assert["default"].strictEqual(graph[everyName](data.node.key, function () {
+          return true;
+        }), data.node.neighbors.length > 0);
+      }
+    }), _defineProperty(_ref2, '#.' + iteratorName, {
+      'it should be possible to create an iterator over neighbors.': function itShouldBePossibleToCreateAnIteratorOverNeighbors() {
+        var iterator = graph[iteratorName](data.node.key);
+
+        _assert["default"].deepStrictEqual((0, _take["default"])(iterator), data.node.neighbors.map(function (neighbor) {
+          return {
+            neighbor: neighbor,
+            attributes: graph.getNodeAttributes(neighbor)
+          };
+        }));
+      }
+    }), _ref2;
+  }
+
+  var tests = {
+    Miscellaneous: {
+      'self loops should appear when using #.inNeighbors and should appear only once with #.neighbors.': function selfLoopsShouldAppearWhenUsingInNeighborsAndShouldAppearOnlyOnceWithNeighbors() {
+        var directed = new Graph({
+          type: 'directed'
+        });
+        directed.addNode('Lucy');
+        directed.addEdgeWithKey('test', 'Lucy', 'Lucy');
+
+        _assert["default"].deepStrictEqual(directed.inNeighbors('Lucy'), ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.inNeighborEntries('Lucy')).map(function (x) {
+          return x.neighbor;
+        }), ['Lucy']);
+
+        var neighbors = [];
+        directed.forEachInNeighbor('Lucy', function (neighbor) {
+          neighbors.push(neighbor);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(directed.neighbors('Lucy'), ['Lucy']);
+
+        neighbors = [];
+        directed.forEachNeighbor('Lucy', function (neighbor) {
+          neighbors.push(neighbor);
+        });
+
+        _assert["default"].deepStrictEqual(neighbors, ['Lucy']);
+
+        _assert["default"].deepStrictEqual(Array.from(directed.neighborEntries('Lucy')).map(function (x) {
+          return x.neighbor;
+        }), ['Lucy']);
+      }
+    }
+  }; // Common tests
+
+  METHODS.forEach(function (name) {
+    return (0, _helpers.deepMerge)(tests, commonTests(name));
+  }); // Specific tests
+
+  for (var name in TEST_DATA) {
+    (0, _helpers.deepMerge)(tests, specificTests(name, TEST_DATA[name]));
+  }
+
+  return tests;
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/nodes.js b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/nodes.js
new file mode 100644
index 0000000000000000000000000000000000000000..33298820d5db1d8504c4be075824ae5b51644fef
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/iteration/nodes.js
@@ -0,0 +1,248 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = nodesIteration;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("../helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Nodes Iteration Specs
+ * =================================
+ *
+ * Testing the nodes iteration-related methods of the graph.
+ */
+function nodesIteration(Graph, checkers) {
+  var invalid = checkers.invalid;
+  return {
+    '#.nodes': {
+      'it should return the list of nodes of the graph.': function itShouldReturnTheListOfNodesOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['one', 'two', 'three']);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['one', 'two', 'three']);
+      }
+    },
+    '#.forEachNode': {
+      'it should throw if given callback is not a function.': function itShouldThrowIfGivenCallbackIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.forEachNode(null);
+        }, invalid());
+      },
+      'it should be possible to iterate over nodes and their attributes.': function itShouldBePossibleToIterateOverNodesAndTheirAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Martha', {
+          age: 33
+        });
+        var count = 0;
+        graph.forEachNode(function (key, attributes) {
+          _assert["default"].strictEqual(key, count ? 'Martha' : 'John');
+
+          _assert["default"].deepStrictEqual(attributes, count ? {
+            age: 33
+          } : {
+            age: 34
+          });
+
+          count++;
+        });
+
+        _assert["default"].strictEqual(count, 2);
+      }
+    },
+    '#.findNode': {
+      'it should throw if given callback is not a function.': function itShouldThrowIfGivenCallbackIsNotAFunction() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.findNode(null);
+        }, invalid());
+      },
+      'it should be possible to find a node in the graph.': function itShouldBePossibleToFindANodeInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          age: 34
+        });
+        graph.addNode('Martha', {
+          age: 33
+        });
+        var count = 0;
+        var found = graph.findNode(function (key, attributes) {
+          _assert["default"].strictEqual(key, 'John');
+
+          _assert["default"].deepStrictEqual(attributes, {
+            age: 34
+          });
+
+          count++;
+          if (key === 'John') return true;
+        });
+
+        _assert["default"].strictEqual(found, 'John');
+
+        _assert["default"].strictEqual(count, 1);
+
+        found = graph.findNode(function () {
+          return false;
+        });
+
+        _assert["default"].strictEqual(found, undefined);
+      }
+    },
+    '#.mapNodes': {
+      'it should be possible to map nodes.': function itShouldBePossibleToMapNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        var result = graph.mapNodes(function (node, attr) {
+          return attr.weight * 2;
+        });
+
+        _assert["default"].deepStrictEqual(result, [4, 6]);
+      }
+    },
+    '#.someNode': {
+      'it should always return false on empty sets.': function itShouldAlwaysReturnFalseOnEmptySets() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.someNode(function () {
+          return true;
+        }), false);
+      },
+      'it should be possible to find if some node matches a predicate.': function itShouldBePossibleToFindIfSomeNodeMatchesAPredicate() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.someNode(function (node, attr) {
+          return attr.weight > 6;
+        }), false);
+
+        _assert["default"].strictEqual(graph.someNode(function (node, attr) {
+          return attr.weight > 2;
+        }), true);
+      }
+    },
+    '#.everyNode': {
+      'it should always return true on empty sets.': function itShouldAlwaysReturnTrueOnEmptySets() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.everyNode(function () {
+          return true;
+        }), true);
+      },
+      'it should be possible to find if all node matches a predicate.': function itShouldBePossibleToFindIfAllNodeMatchesAPredicate() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+
+        _assert["default"].strictEqual(graph.everyNode(function (node, attr) {
+          return attr.weight > 2;
+        }), false);
+
+        _assert["default"].strictEqual(graph.everyNode(function (node, attr) {
+          return attr.weight > 1;
+        }), true);
+      }
+    },
+    '#.filterNodes': {
+      'it should be possible to filter nodes.': function itShouldBePossibleToFilterNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        graph.addNode('three', {
+          weight: 4
+        });
+        var result = graph.filterNodes(function (node, _ref) {
+          var weight = _ref.weight;
+          return weight >= 3;
+        });
+
+        _assert["default"].deepStrictEqual(result, ['two', 'three']);
+      }
+    },
+    '#.reduceNodes': {
+      'it should throw if initial value is not given.': function itShouldThrowIfInitialValueIsNotGiven() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.reduceNodes(function (x, _, attr) {
+            return x + attr.weight;
+          });
+        }, invalid());
+      },
+      'it should be possible to reduce nodes.': function itShouldBePossibleToReduceNodes() {
+        var graph = new Graph();
+        graph.addNode('one', {
+          weight: 2
+        });
+        graph.addNode('two', {
+          weight: 3
+        });
+        graph.addNode('three', {
+          weight: 4
+        });
+        var result = graph.reduceNodes(function (x, _, attr) {
+          return x + attr.weight;
+        }, 0);
+
+        _assert["default"].strictEqual(result, 9);
+      }
+    },
+    '#.nodeEntries': {
+      'it should be possible to create a nodes iterator.': function itShouldBePossibleToCreateANodesIterator() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['one', 'two', 'three']);
+        graph.replaceNodeAttributes('two', {
+          hello: 'world'
+        });
+        var iterator = graph.nodeEntries();
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'one',
+          attributes: {}
+        });
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'two',
+          attributes: {
+            hello: 'world'
+          }
+        });
+
+        _assert["default"].deepStrictEqual(iterator.next().value, {
+          node: 'three',
+          attributes: {}
+        });
+
+        _assert["default"].strictEqual(iterator.next().done, true);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/known.js b/libs/shared/graph-layouts/node_modules/graphology/specs/known.js
new file mode 100644
index 0000000000000000000000000000000000000000..040ffb036d23726a0a45e93be2bada79bd7d2e28
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/known.js
@@ -0,0 +1,50 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = knownMethods;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Known Methods Specs
+ * ===============================
+ *
+ * Testing the known methods of the graph.
+ */
+function knownMethods(Graph) {
+  return {
+    '#.toJSON': {
+      'it should return the serialized graph.': function itShouldReturnTheSerializedGraph() {
+        var graph = new Graph({
+          multi: true
+        });
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Jack', 'Martha']);
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.addEdgeWithKey('J->J•1', 'John', 'Jack');
+        graph.addEdgeWithKey('J->J•2', 'John', 'Jack', {
+          weight: 2
+        });
+        graph.addEdgeWithKey('J->J•3', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•1', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•2', 'John', 'Jack', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph.toJSON(), graph["export"]());
+      }
+    },
+    '#.toString': {
+      'it should return "[object Graph]".': function itShouldReturnObjectGraph() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.toString(), '[object Graph]');
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/misc.js b/libs/shared/graph-layouts/node_modules/graphology/specs/misc.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0044b1a2f68ce6013b81d0aa74ee36af5e36b21
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/misc.js
@@ -0,0 +1,112 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = misc;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Misc Specs
+ * ======================
+ *
+ * Testing the miscellaneous things about the graph.
+ */
+function misc(Graph) {
+  return {
+    Structure: {
+      'a simple mixed graph can have A->B, B->A & A<->B': function aSimpleMixedGraphCanHaveABBAAB() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Audrey', 'Benjamin']);
+
+        _assert["default"].doesNotThrow(function () {
+          graph.addEdge('Audrey', 'Benjamin');
+          graph.addEdge('Benjamin', 'Audrey');
+          graph.addUndirectedEdge('Benjamin', 'Audrey');
+        });
+      },
+      'deleting the last edge between A & B should correctly clear neighbor index.': function deletingTheLastEdgeBetweenABShouldCorrectlyClearNeighborIndex() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.addNode('A');
+        graph.addNode('B');
+        graph.addEdge('A', 'B');
+        graph.addEdge('A', 'B');
+        graph.forEachEdge('A', function (edge) {
+          return graph.dropEdge(edge);
+        });
+
+        _assert["default"].deepStrictEqual(graph.neighbors('A'), []);
+
+        _assert["default"].deepStrictEqual(graph.neighbors('B'), []);
+      },
+      'exhaustive deletion use-cases should not break doubly-linked lists implementation of multigraph edge storage.': function exhaustiveDeletionUseCasesShouldNotBreakDoublyLinkedListsImplementationOfMultigraphEdgeStorage() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.mergeEdgeWithKey('1', 'A', 'B');
+        graph.mergeEdgeWithKey('2', 'A', 'B');
+        graph.mergeEdgeWithKey('3', 'A', 'B');
+        graph.mergeEdgeWithKey('4', 'A', 'B');
+        graph.dropEdge('1');
+        graph.dropEdge('2');
+        graph.dropEdge('3');
+        graph.dropEdge('4');
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), false);
+
+        graph.mergeEdgeWithKey('1', 'A', 'B');
+        graph.mergeEdgeWithKey('2', 'A', 'B');
+        graph.mergeEdgeWithKey('3', 'A', 'B');
+        graph.mergeEdgeWithKey('4', 'A', 'B');
+
+        _assert["default"].strictEqual(graph.size, 4);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), true);
+
+        graph.dropEdge('2');
+        graph.dropEdge('3');
+
+        _assert["default"].strictEqual(graph.size, 2);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), true);
+
+        graph.dropEdge('4');
+        graph.dropEdge('1');
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.areNeighbors('A', 'B'), false);
+      }
+    },
+    'Key coercion': {
+      'keys should be correctly coerced to strings.': function keysShouldBeCorrectlyCoercedToStrings() {
+        var graph = new Graph();
+        graph.addNode(1);
+        graph.addNode('2');
+
+        _assert["default"].strictEqual(graph.hasNode(1), true);
+
+        _assert["default"].strictEqual(graph.hasNode('1'), true);
+
+        _assert["default"].strictEqual(graph.hasNode(2), true);
+
+        _assert["default"].strictEqual(graph.hasNode('2'), true);
+
+        graph.addEdgeWithKey(3, 1, 2);
+
+        _assert["default"].strictEqual(graph.hasEdge(3), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('3'), true);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/mutation.js b/libs/shared/graph-layouts/node_modules/graphology/specs/mutation.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6a114c1b30abe04ba95646608ce0a06a5ee42a7
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/mutation.js
@@ -0,0 +1,883 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = mutation;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function mutation(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+  return {
+    '#.addNode': {
+      'it should throw if given attributes is not an object.': function itShouldThrowIfGivenAttributesIsNotAnObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.addNode('test', true);
+        }, invalid());
+      },
+      'it should throw if the given node already exist.': function itShouldThrowIfTheGivenNodeAlreadyExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addNode('Martha');
+        }, usage());
+      },
+      'it should return the added node.': function itShouldReturnTheAddedNode() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.addNode('John'), 'John');
+      }
+    },
+    '#.mergeNode': {
+      'it should add the node if it does not exist yet.': function itShouldAddTheNodeIfItDoesNotExistYet() {
+        var graph = new Graph();
+        graph.mergeNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should do nothing if the node already exists.': function itShouldDoNothingIfTheNodeAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.mergeNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should merge the attributes.': function itShouldMergeTheAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          eyes: 'blue'
+        });
+        graph.mergeNode('John', {
+          age: 15
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          eyes: 'blue',
+          age: 15
+        });
+      },
+      'it should coerce keys to string.': function itShouldCoerceKeysToString() {
+        var graph = new Graph();
+        graph.addNode(4);
+
+        _assert["default"].doesNotThrow(function () {
+          return graph.mergeNode(4);
+        });
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+
+        var _graph$mergeNode = graph.mergeNode('Jack'),
+            key = _graph$mergeNode[0],
+            wasAdded = _graph$mergeNode[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, true);
+
+        var _graph$mergeNode2 = graph.mergeNode('Jack');
+
+        key = _graph$mergeNode2[0];
+        wasAdded = _graph$mergeNode2[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, false);
+      }
+    },
+    '#.updateNode': {
+      'it should add the node if it does not exist yet.': function itShouldAddTheNodeIfItDoesNotExistYet() {
+        var graph = new Graph();
+        graph.updateNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should do nothing if the node already exists.': function itShouldDoNothingIfTheNodeAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.updateNode('John');
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+      },
+      'it should update the attributes.': function itShouldUpdateTheAttributes() {
+        var graph = new Graph();
+        graph.addNode('John', {
+          eyes: 'blue',
+          count: 1
+        });
+        graph.updateNode('John', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            count: attr.count + 1
+          });
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John']);
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          eyes: 'blue',
+          count: 2
+        });
+      },
+      'it should be possible to start from blank attributes.': function itShouldBePossibleToStartFromBlankAttributes() {
+        var graph = new Graph();
+        graph.updateNode('John', function () {
+          return {
+            count: 2
+          };
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {
+          count: 2
+        });
+      },
+      'it should coerce keys to string.': function itShouldCoerceKeysToString() {
+        var graph = new Graph();
+        graph.addNode(4);
+
+        _assert["default"].doesNotThrow(function () {
+          return graph.updateNode(4);
+        });
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+
+        var _graph$updateNode = graph.updateNode('Jack'),
+            key = _graph$updateNode[0],
+            wasAdded = _graph$updateNode[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, true);
+
+        var _graph$updateNode2 = graph.updateNode('Jack');
+
+        key = _graph$updateNode2[0];
+        wasAdded = _graph$updateNode2[1];
+
+        _assert["default"].strictEqual(key, 'Jack');
+
+        _assert["default"].strictEqual(wasAdded, false);
+      }
+    },
+    '#.addDirectedEdge': {
+      'it should throw if given attributes is not an object.': function itShouldThrowIfGivenAttributesIsNotAnObject() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('source', 'target', true);
+        }, invalid());
+      },
+      'it should throw if the graph is undirected.': function itShouldThrowIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('source', 'target');
+        }, usage());
+      },
+      'it should throw if either the source or the target does not exist.': function itShouldThrowIfEitherTheSourceOrTheTargetDoesNotExist() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Eric');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Martha', 'Eric');
+        }, notFound());
+      },
+      'it should throw if the edge is a loop and the graph does not allow it.': function itShouldThrowIfTheEdgeIsALoopAndTheGraphDoesNotAllowIt() {
+        var graph = new Graph({
+          allowSelfLoops: false
+        });
+        graph.addNode('Thomas');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Thomas');
+        }, usage());
+      },
+      'it should be possible to add self loops.': function itShouldBePossibleToAddSelfLoops() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        var loop = graph.addDirectedEdge('Thomas', 'Thomas');
+
+        _assert["default"].deepStrictEqual(graph.extremities(loop), ['Thomas', 'Thomas']);
+      },
+      'it should throw if the graph is not multi & we try to add twice the same edge.': function itShouldThrowIfTheGraphIsNotMultiWeTryToAddTwiceTheSameEdge() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addDirectedEdge('Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdge('Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+      },
+      "it should return the generated edge's key.": function itShouldReturnTheGeneratedEdgeSKey() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('Thomas', 'Martha');
+        (0, _assert["default"])(typeof edge === 'string' || typeof edge === 'number');
+        (0, _assert["default"])(!(edge instanceof Graph));
+      }
+    },
+    '#.addEdge': {
+      'it should add a directed edge if the graph is directed or mixed.': function itShouldAddADirectedEdgeIfTheGraphIsDirectedOrMixed() {
+        var graph = new Graph(),
+            directedGraph = new Graph({
+          type: 'directed'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var mixedEdge = graph.addEdge('John', 'Martha');
+        directedGraph.addNode('John');
+        directedGraph.addNode('Martha');
+        var directedEdge = directedGraph.addEdge('John', 'Martha');
+        (0, _assert["default"])(graph.isDirected(mixedEdge));
+        (0, _assert["default"])(directedGraph.isDirected(directedEdge));
+      },
+      'it should add an undirected edge if the graph is undirected.': function itShouldAddAnUndirectedEdgeIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addEdge('John', 'Martha');
+        (0, _assert["default"])(graph.isUndirected(edge));
+      }
+    },
+    '#.addDirectedEdgeWithKey': {
+      'it should throw if an edge with the same key already exists.': function itShouldThrowIfAnEdgeWithTheSameKeyAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addUndirectedEdgeWithKey('T->M', 'Thomas', 'Martha');
+        }, usage());
+      }
+    },
+    '#.addUndirectedEdgeWithKey': {
+      'it should throw if an edge with the same key already exists.': function itShouldThrowIfAnEdgeWithTheSameKeyAlreadyExists() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+        graph.addNode('Martha');
+        graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.addUndirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.addDirectedEdgeWithKey('T<->M', 'Thomas', 'Martha');
+        }, usage());
+      }
+    },
+    '#.addEdgeWithKey': {
+      'it should add a directed edge if the graph is directed or mixed.': function itShouldAddADirectedEdgeIfTheGraphIsDirectedOrMixed() {
+        var graph = new Graph(),
+            directedGraph = new Graph({
+          type: 'directed'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var mixedEdge = graph.addEdgeWithKey('J->M', 'John', 'Martha');
+        directedGraph.addNode('John');
+        directedGraph.addNode('Martha');
+        var directedEdge = directedGraph.addEdgeWithKey('J->M', 'John', 'Martha');
+        (0, _assert["default"])(graph.isDirected(mixedEdge));
+        (0, _assert["default"])(directedGraph.isDirected(directedEdge));
+      },
+      'it should add an undirected edge if the graph is undirected.': function itShouldAddAnUndirectedEdgeIfTheGraphIsUndirected() {
+        var graph = new Graph({
+          type: 'undirected'
+        });
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addEdgeWithKey('J<->M', 'John', 'Martha');
+        (0, _assert["default"])(graph.isUndirected(edge));
+      }
+    },
+    '#.mergeEdge': {
+      'it should add the edge if it does not yet exist.': function itShouldAddTheEdgeIfItDoesNotYetExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should do nothing if the edge already exists.': function itShouldDoNothingIfTheEdgeAlreadyExists() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha');
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should merge existing attributes if any.': function itShouldMergeExistingAttributesIfAny() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          type: 'KNOWS'
+        });
+        graph.mergeEdge('John', 'Martha', {
+          weight: 2
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          type: 'KNOWS',
+          weight: 2
+        });
+      },
+      'it should add missing nodes in the path.': function itShouldAddMissingNodesInThePath() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Martha']);
+      },
+      'it should throw in case of inconsistencies.': function itShouldThrowInCaseOfInconsistencies() {
+        var graph = new Graph();
+        graph.mergeEdgeWithKey('J->M', 'John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.mergeEdgeWithKey('J->M', 'John', 'Thomas');
+        }, usage());
+      },
+      'it should be able to merge undirected edges in both directions.': function itShouldBeAbleToMergeUndirectedEdgesInBothDirections() {
+        _assert["default"].doesNotThrow(function () {
+          var graph = new Graph();
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'John', 'Martha');
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'John', 'Martha');
+          graph.mergeUndirectedEdgeWithKey('J<->M', 'Martha', 'John');
+        }, usage());
+      },
+      'it should distinguish between typed edges.': function itShouldDistinguishBetweenTypedEdges() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'Martha', {
+          type: 'LIKES'
+        });
+        graph.mergeUndirectedEdge('John', 'Martha', {
+          weight: 34
+        });
+
+        _assert["default"].strictEqual(graph.size, 2);
+      },
+      'it should be possible to merge a self loop.': function itShouldBePossibleToMergeASelfLoop() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'John', {
+          type: 'IS'
+        });
+
+        _assert["default"].strictEqual(graph.order, 1);
+
+        _assert["default"].strictEqual(graph.size, 1);
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+        var info = graph.mergeEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), true, true, true]);
+
+        info = graph.mergeEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), false, false, false]);
+
+        graph.addNode('Mary');
+        info = graph.mergeEdge('Mary', 'Sue');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Mary', 'Sue'), true, false, true]);
+
+        info = graph.mergeEdge('Gwladys', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Gwladys', 'Mary'), true, true, false]);
+
+        graph.addNode('Quintin');
+        info = graph.mergeEdge('Quintin', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Quintin', 'Mary'), true, false, false]);
+      }
+    },
+    '#.updateEdge': {
+      'it should add the edge if it does not yet exist.': function itShouldAddTheEdgeIfItDoesNotYetExist() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should do nothing if the edge already exists.': function itShouldDoNothingIfTheEdgeAlreadyExists() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha');
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+      },
+      'it should be possible to start from blank attributes.': function itShouldBePossibleToStartFromBlankAttributes() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.updateEdge('John', 'Martha', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 3
+          });
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          weight: 3
+        });
+      },
+      'it should update existing attributes if any.': function itShouldUpdateExistingAttributesIfAny() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+        graph.addEdge('John', 'Martha', {
+          type: 'KNOWS'
+        });
+        graph.updateEdge('John', 'Martha', function (attr) {
+          return _objectSpread(_objectSpread({}, attr), {}, {
+            weight: 2
+          });
+        });
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Martha'), true);
+
+        _assert["default"].deepStrictEqual(graph.getEdgeAttributes('John', 'Martha'), {
+          type: 'KNOWS',
+          weight: 2
+        });
+      },
+      'it should add missing nodes in the path.': function itShouldAddMissingNodesInThePath() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Martha']);
+      },
+      'it should throw in case of inconsistencies.': function itShouldThrowInCaseOfInconsistencies() {
+        var graph = new Graph();
+        graph.updateEdgeWithKey('J->M', 'John', 'Martha');
+
+        _assert["default"]["throws"](function () {
+          graph.updateEdgeWithKey('J->M', 'John', 'Thomas');
+        }, usage());
+      },
+      'it should distinguish between typed edges.': function itShouldDistinguishBetweenTypedEdges() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'Martha', function () {
+          return {
+            type: 'LIKES'
+          };
+        });
+        graph.updateUndirectedEdge('John', 'Martha', function () {
+          return {
+            weight: 34
+          };
+        });
+
+        _assert["default"].strictEqual(graph.size, 2);
+      },
+      'it should be possible to merge a self loop.': function itShouldBePossibleToMergeASelfLoop() {
+        var graph = new Graph();
+        graph.updateEdge('John', 'John', function () {
+          return {
+            type: 'IS'
+          };
+        });
+
+        _assert["default"].strictEqual(graph.order, 1);
+
+        _assert["default"].strictEqual(graph.size, 1);
+      },
+      'it should return useful information.': function itShouldReturnUsefulInformation() {
+        var graph = new Graph();
+        var info = graph.updateEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), true, true, true]);
+
+        info = graph.updateEdge('John', 'Jack');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('John', 'Jack'), false, false, false]);
+
+        graph.addNode('Mary');
+        info = graph.updateEdge('Mary', 'Sue');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Mary', 'Sue'), true, false, true]);
+
+        info = graph.updateEdge('Gwladys', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Gwladys', 'Mary'), true, true, false]);
+
+        graph.addNode('Quintin');
+        info = graph.updateEdge('Quintin', 'Mary');
+
+        _assert["default"].deepStrictEqual(info, [graph.edge('Quintin', 'Mary'), true, false, false]);
+      }
+    },
+    '#.dropEdge': {
+      'it should throw if the edge or nodes in the path are not found in the graph.': function itShouldThrowIfTheEdgeOrNodesInThePathAreNotFoundInTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Martha']);
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('Test');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('Forever', 'Alone');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('John', 'Test');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.dropEdge('John', 'Martha');
+        }, notFound());
+      },
+      'it should correctly remove the given edge from the graph.': function itShouldCorrectlyRemoveTheGivenEdgeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        var edge = graph.addEdge('John', 'Margaret');
+        graph.dropEdge(edge);
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.degree('Margaret'), 0);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should be possible to remove an edge using source & target.': function itShouldBePossibleToRemoveAnEdgeUsingSourceTarget() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        graph.addEdge('John', 'Margaret');
+        graph.dropEdge('John', 'Margaret');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.degree('Margaret'), 0);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Margaret'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeEdge('John', 'John');
+        graph.dropEdge('John', 'John');
+
+        _assert["default"].deepStrictEqual(graph.edges(), []);
+
+        _assert["default"].deepStrictEqual(graph.edges('John'), []);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        var multiGraph = new Graph({
+          multi: true
+        });
+        multiGraph.mergeEdgeWithKey('j', 'John', 'John');
+        multiGraph.mergeEdgeWithKey('k', 'John', 'John');
+        multiGraph.dropEdge('j');
+
+        _assert["default"].deepStrictEqual(multiGraph.edges(), ['k']);
+
+        _assert["default"].deepStrictEqual(multiGraph.edges('John'), ['k']);
+
+        _assert["default"].strictEqual(multiGraph.size, 1);
+      }
+    },
+    '#.dropNode': {
+      'it should throw if the edge is not found in the graph.': function itShouldThrowIfTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.dropNode('Test');
+        }, notFound());
+      },
+      'it should correctly remove the given node from the graph.': function itShouldCorrectlyRemoveTheGivenNodeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Margaret']);
+        var edge = graph.addEdge('John', 'Margaret');
+        graph.mergeEdge('Jack', 'Trudy');
+        graph.dropNode('Margaret');
+
+        _assert["default"].strictEqual(graph.order, 3);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasNode('Margaret'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+
+        _assert["default"].strictEqual(graph.degree('John'), 0);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Margaret'), false);
+      },
+      'it should also work with mixed, multi graphs and self loops.': function itShouldAlsoWorkWithMixedMultiGraphsAndSelfLoops() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('B', 'A');
+        graph.mergeEdge('A', 'B');
+        graph.mergeEdge('A', 'A');
+        graph.mergeUndirectedEdge('A', 'B');
+        graph.mergeUndirectedEdge('A', 'B');
+        graph.mergeUndirectedEdge('A', 'A');
+        var copy = graph.copy();
+        graph.dropNode('B');
+
+        _assert["default"].strictEqual(graph.size, 2);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 1);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 1);
+
+        copy.dropNode('A');
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        _assert["default"].strictEqual(copy.directedSelfLoopCount, 0);
+
+        _assert["default"].strictEqual(copy.undirectedSelfLoopCount, 0);
+      },
+      'it should also coerce keys as strings.': function itShouldAlsoCoerceKeysAsStrings() {
+        function Key(name) {
+          this.name = name;
+        }
+
+        Key.prototype.toString = function () {
+          return this.name;
+        };
+
+        var graph = new Graph();
+        var key = new Key('test');
+        graph.addNode(key);
+        graph.dropNode(key);
+
+        _assert["default"].strictEqual(graph.order, 0);
+
+        _assert["default"].strictEqual(graph.hasNode(key), false);
+      }
+    },
+    '#.dropDirectedEdge': {
+      'it should throw if given incorrect arguments.': function itShouldThrowIfGivenIncorrectArguments() {
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true
+          });
+          graph.mergeEdge('a', 'b');
+          graph.dropDirectedEdge('a', 'b');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true
+          });
+          graph.mergeEdgeWithKey('1', 'a', 'b');
+          graph.dropDirectedEdge('1');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph();
+          graph.dropDirectedEdge('a', 'b');
+        }, notFound());
+      },
+      'it should correctly drop the relevant edge.': function itShouldCorrectlyDropTheRelevantEdge() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('a', 'b');
+        graph.mergeDirectedEdge('a', 'b');
+        graph.dropDirectedEdge('a', 'b');
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('a', 'b'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('a', 'b'), true);
+      }
+    },
+    '#.dropUndirectedEdge': {
+      'it should throw if given incorrect arguments.': function itShouldThrowIfGivenIncorrectArguments() {
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true,
+            type: 'undirected'
+          });
+          graph.mergeEdge('a', 'b');
+          graph.dropUndirectedEdge('a', 'b');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            multi: true,
+            type: 'undirected'
+          });
+          graph.mergeEdgeWithKey('1', 'a', 'b');
+          graph.dropUndirectedEdge('1');
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          graph.dropUndirectedEdge('a', 'b');
+        }, notFound());
+      },
+      'it should correctly drop the relevant edge.': function itShouldCorrectlyDropTheRelevantEdge() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('a', 'b');
+        graph.mergeDirectedEdge('a', 'b');
+        graph.dropUndirectedEdge('a', 'b');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('a', 'b'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('a', 'b'), true);
+      }
+    },
+    '#.clear': {
+      'it should empty the graph.': function itShouldEmptyTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+        graph.clear();
+
+        _assert["default"].strictEqual(graph.order, 0);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), false);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+      },
+      'it should be possible to use the graph normally afterwards.': function itShouldBePossibleToUseTheGraphNormallyAfterwards() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        graph.addEdge('Lindsay', 'Martha');
+        graph.clear();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), true);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), true);
+      }
+    },
+    '#.clearEdges': {
+      'it should drop every edge from the graph.': function itShouldDropEveryEdgeFromTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Lindsay', 'Martha']);
+        var edge = graph.addEdge('Lindsay', 'Martha');
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.order, 2);
+
+        _assert["default"].strictEqual(graph.size, 0);
+
+        _assert["default"].strictEqual(graph.hasNode('Lindsay'), true);
+
+        _assert["default"].strictEqual(graph.hasNode('Martha'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge(edge), false);
+      },
+      'it should properly reset instance counters.': function itShouldProperlyResetInstanceCounters() {
+        var graph = new Graph();
+        graph.mergeEdge(0, 1);
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        graph.mergeEdge(0, 1);
+        graph.clear();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      },
+      'it should properly clear node indices, regarding self loops notably.': function itShouldProperlyClearNodeIndicesRegardingSelfLoopsNotably() {
+        var graph = new Graph();
+        graph.mergeEdge(1, 1);
+
+        _assert["default"].strictEqual(graph.degree(1), 2);
+
+        graph.clearEdges();
+
+        _assert["default"].strictEqual(graph.degree(1), 0);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/properties.js b/libs/shared/graph-layouts/node_modules/graphology/specs/properties.js
new file mode 100644
index 0000000000000000000000000000000000000000..4babe3dc5dc23e69132ea1a0429124d920ccafff
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/properties.js
@@ -0,0 +1,214 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = properties;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
+
+var PROPERTIES = ['order', 'size', 'directedSize', 'undirectedSize', 'type', 'multi', 'allowSelfLoops', 'implementation', 'selfLoopCount', 'directedSelfLoopCount', 'undirectedSelfLoopCount'];
+
+function properties(Graph) {
+  return {
+    /**
+     * Regarding all properties.
+     */
+    misc: {
+      'all expected properties should be set.': function allExpectedPropertiesShouldBeSet() {
+        var graph = new Graph();
+        PROPERTIES.forEach(function (property) {
+          (0, _assert["default"])(property in graph, property);
+        });
+      },
+      'properties should be read-only.': function propertiesShouldBeReadOnly() {
+        var graph = new Graph(); // Attempting to mutate the properties
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"]["throws"](function () {
+            graph[property] = 'test';
+          }, TypeError);
+        });
+      }
+    },
+
+    /**
+     * Order.
+     */
+    '#.order': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.order, 0);
+      },
+      'adding nodes should increase order.': function addingNodesShouldIncreaseOrder() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+
+        _assert["default"].strictEqual(graph.order, 2);
+      }
+    },
+
+    /**
+     * Size.
+     */
+    '#.size': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.size, 0);
+      },
+      'adding & dropping edges should affect size.': function addingDroppingEdgesShouldAffectSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        graph.dropEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.size, 0);
+      }
+    },
+
+    /**
+     * Directed Size.
+     */
+    '#.directedSize': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      },
+      'adding & dropping edges should affect directed size.': function addingDroppingEdgesShouldAffectDirectedSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        var directedEdge = graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        var undirectedEdge = graph.addUndirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.directedSize, 1);
+
+        graph.dropEdge(directedEdge);
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+
+        graph.dropEdge(undirectedEdge);
+
+        _assert["default"].strictEqual(graph.directedSize, 0);
+      }
+    },
+
+    /**
+     * Undirected Size.
+     */
+    '#.undirectedSize': {
+      'it should be 0 if the graph is empty.': function itShouldBe0IfTheGraphIsEmpty() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+      },
+      'adding & dropping edges should affect undirected size.': function addingDroppingEdgesShouldAffectUndirectedSize() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Jack');
+        var directedEdge = graph.addDirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+
+        var undirectedEdge = graph.addUndirectedEdge('John', 'Jack');
+
+        _assert["default"].strictEqual(graph.undirectedSize, 1);
+
+        graph.dropEdge(directedEdge);
+
+        _assert["default"].strictEqual(graph.undirectedSize, 1);
+
+        graph.dropEdge(undirectedEdge);
+
+        _assert["default"].strictEqual(graph.undirectedSize, 0);
+      }
+    },
+
+    /**
+     * Multi.
+     */
+    '#.multi': {
+      'it should be false by default.': function itShouldBeFalseByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.multi, false);
+      }
+    },
+
+    /**
+     * Type.
+     */
+    '#.type': {
+      'it should be "mixed" by default.': function itShouldBeMixedByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.type, 'mixed');
+      }
+    },
+
+    /**
+     * Self loops.
+     */
+    '#.allowSelfLoops': {
+      'it should be true by default.': function itShouldBeTrueByDefault() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.allowSelfLoops, true);
+      }
+    },
+
+    /**
+     * Implementation.
+     */
+    '#.implementation': {
+      'it should exist and be a string.': function itShouldExistAndBeAString() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(_typeof(graph.implementation), 'string');
+      }
+    },
+
+    /**
+     * Self Loop Count.
+     */
+    '#.selfLoopCount': {
+      'it should exist and be correct.': function itShouldExistAndBeCorrect() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('John', 'John');
+        graph.mergeDirectedEdge('Lucy', 'Lucy');
+        graph.mergeUndirectedEdge('Joana', 'Joana');
+
+        _assert["default"].strictEqual(graph.selfLoopCount, 3);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 2);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 1);
+
+        graph.forEachEdge(function (edge) {
+          return graph.dropEdge(edge);
+        });
+
+        _assert["default"].strictEqual(graph.selfLoopCount, 0);
+
+        _assert["default"].strictEqual(graph.directedSelfLoopCount, 0);
+
+        _assert["default"].strictEqual(graph.undirectedSelfLoopCount, 0);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/read.js b/libs/shared/graph-layouts/node_modules/graphology/specs/read.js
new file mode 100644
index 0000000000000000000000000000000000000000..afe9f1c66fcdc4b23b4e813f1d80362be6125569
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/read.js
@@ -0,0 +1,909 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = read;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Read Specs
+ * ======================
+ *
+ * Testing the read methods of the graph.
+ */
+function read(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound,
+      usage = checkers.usage;
+  return {
+    '#.hasNode': {
+      'it should correctly return whether the given node is found in the graph.': function itShouldCorrectlyReturnWhetherTheGivenNodeIsFoundInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"].strictEqual(graph.hasNode('John'), false);
+
+        graph.addNode('John');
+
+        _assert["default"].strictEqual(graph.hasNode('John'), true);
+      }
+    },
+    '#.hasDirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasDirectedEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('M->C'), true);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('C<->J'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Martha', 'Catherine'), true);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Catherine', 'John'), false);
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('John', 'Catherine'), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), false);
+      }
+    },
+    '#.hasUndirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasUndirectedEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('M->C'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('C<->J'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Martha', 'Catherine'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Catherine', 'John'), true);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('John', 'Catherine'), true);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), true);
+      }
+    },
+    '#.hasEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.hasEdge(1, 2, 3);
+        }, invalid());
+      },
+      'it should correctly return whether a matching edge exists in the graph.': function itShouldCorrectlyReturnWhetherAMatchingEdgeExistsInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Martha');
+        graph.addNode('Catherine');
+        graph.addNode('John');
+        graph.addDirectedEdgeWithKey('M->C', 'Martha', 'Catherine');
+        graph.addUndirectedEdgeWithKey('C<->J', 'Catherine', 'John');
+
+        _assert["default"].strictEqual(graph.hasEdge('M->C'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('C<->J'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('test'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge('Martha', 'Catherine'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('Martha', 'Thomas'), false);
+
+        _assert["default"].strictEqual(graph.hasEdge('Catherine', 'John'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Catherine'), true);
+      },
+      'it should work properly with typed graphs.': function itShouldWorkProperlyWithTypedGraphs() {
+        var directedGraph = new Graph({
+          type: 'directed'
+        }),
+            undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        (0, _helpers.addNodesFrom)(directedGraph, [1, 2]);
+        (0, _helpers.addNodesFrom)(undirectedGraph, [1, 2]);
+
+        _assert["default"].strictEqual(directedGraph.hasEdge(1, 2), false);
+
+        _assert["default"].strictEqual(undirectedGraph.hasEdge(1, 2), false);
+      },
+      'it should work with self loops.': function itShouldWorkWithSelfLoops() {
+        var graph = new Graph();
+        graph.mergeUndirectedEdge('Lucy', 'Lucy');
+
+        _assert["default"].strictEqual(graph.hasDirectedEdge('Lucy', 'Lucy'), false);
+
+        _assert["default"].strictEqual(graph.hasUndirectedEdge('Lucy', 'Lucy'), true);
+
+        _assert["default"].strictEqual(graph.hasEdge('Lucy', 'Lucy'), true);
+      }
+    },
+    '#.directedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.directedEdge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.directedEdge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.directedEdge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.directedEdge('Lucy', 'Jack'), undefined);
+
+        _assert["default"].strictEqual(graph.directedEdge('Jack', 'Lucy'), 'J->L');
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Jack', 'Lucy');
+
+        _assert["default"].strictEqual(undirectedGraph.directedEdge('Jack', 'Lucy'), undefined);
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.directedEdge('John', 'John'), 'd');
+      }
+    },
+    '#.undirectedEdge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.undirectedEdge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.undirectedEdge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.undirectedEdge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('Lucy', 'Jack'), 'J<->L');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('Jack', 'Lucy'), 'J<->L');
+
+        var directedGraph = new Graph({
+          type: 'directed'
+        });
+        directedGraph.mergeEdge('Jack', 'Lucy');
+
+        _assert["default"].strictEqual(directedGraph.undirectedEdge('Jack', 'Lucy'), undefined);
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.undirectedEdge('John', 'John'), 'u');
+      }
+    },
+    '#.edge': {
+      'it should throw if invalid arguments are provided.': function itShouldThrowIfInvalidArgumentsAreProvided() {
+        var graph = new Graph(),
+            multiGraph = new Graph({
+          multi: true
+        });
+        graph.addNode('John');
+
+        _assert["default"]["throws"](function () {
+          multiGraph.edge(1, 2);
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          graph.edge('Jack', 'John');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.edge('John', 'Jack');
+        }, notFound());
+      },
+      'it should return the correct edge.': function itShouldReturnTheCorrectEdge() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Jack', 'Lucy']);
+        graph.addDirectedEdgeWithKey('J->L', 'Jack', 'Lucy');
+        graph.addUndirectedEdgeWithKey('J<->L', 'Jack', 'Lucy');
+
+        _assert["default"].strictEqual(graph.edge('Lucy', 'Jack'), 'J<->L');
+
+        _assert["default"].strictEqual(graph.edge('Jack', 'Lucy'), 'J->L');
+      },
+      'it should return the correct self loop.': function itShouldReturnTheCorrectSelfLoop() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addEdgeWithKey('d', 'John', 'John');
+        graph.addUndirectedEdgeWithKey('u', 'John', 'John');
+
+        _assert["default"].strictEqual(graph.edge('John', 'John'), 'd');
+      }
+    },
+    '#.areDirectedNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areDirectedNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areDirectedNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areDirectedNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areInNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areInNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areInNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areInNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areOutNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areOutNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areOutNeighbors('Martha', 'Mary'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areOutNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areOutboundNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areOutboundNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areOutboundNeighbors('Martha', 'Mary'), true);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areOutboundNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.areInboundNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areInboundNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areInboundNeighbors('Martha', 'Mary'), true);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areInboundNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.areUndirectedNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areUndirectedNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Mary', 'Joseph'), false);
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Joseph', 'Mary'), false);
+
+        _assert["default"].strictEqual(graph.areUndirectedNeighbors('Martha', 'Mary'), true);
+
+        var directedGraph = new Graph({
+          type: 'directed'
+        });
+        directedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(directedGraph.areUndirectedNeighbors('Mary', 'Martha'), false);
+      }
+    },
+    '#.areNeighbors': {
+      'it should throw if node is not in the graph.': function itShouldThrowIfNodeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.areNeighbors('source', 'target');
+        }, notFound());
+      },
+      'it should correctly return whether two nodes are neighbors.': function itShouldCorrectlyReturnWhetherTwoNodesAreNeighbors() {
+        var graph = new Graph();
+        graph.mergeDirectedEdge('Mary', 'Joseph');
+        graph.mergeUndirectedEdge('Martha', 'Mary');
+
+        _assert["default"].strictEqual(graph.areNeighbors('Mary', 'Joseph'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Joseph', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Martha', 'Mary'), true);
+
+        _assert["default"].strictEqual(graph.areNeighbors('Joseph', 'Martha'), false);
+
+        var undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        undirectedGraph.mergeEdge('Mary', 'Martha');
+
+        _assert["default"].strictEqual(undirectedGraph.areNeighbors('Mary', 'Martha'), true);
+      }
+    },
+    '#.source': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.source('test');
+        }, notFound());
+      },
+      'it should return the correct source.': function itShouldReturnTheCorrectSource() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.source(edge), 'John');
+      }
+    },
+    '#.target': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.target('test');
+        }, notFound());
+      },
+      'it should return the correct target.': function itShouldReturnTheCorrectTarget() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].strictEqual(graph.target(edge), 'Martha');
+      }
+    },
+    '#.extremities': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.extremities('test');
+        }, notFound());
+      },
+      'it should return the correct extremities.': function itShouldReturnTheCorrectExtremities() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Martha');
+        var edge = graph.addDirectedEdge('John', 'Martha');
+
+        _assert["default"].deepStrictEqual(graph.extremities(edge), ['John', 'Martha']);
+      }
+    },
+    '#.opposite': {
+      'it should throw if either the node or the edge is not found in the graph.': function itShouldThrowIfEitherTheNodeOrTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+        graph.addNode('Thomas');
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Jeremy', 'T->J');
+        }, notFound());
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Thomas', 'T->J');
+        }, notFound());
+      },
+      'it should throw if the node & the edge are not related.': function itShouldThrowIfTheNodeTheEdgeAreNotRelated() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Thomas', 'Isabella', 'Estelle']);
+        graph.addEdgeWithKey('I->E', 'Isabella', 'Estelle');
+
+        _assert["default"]["throws"](function () {
+          graph.opposite('Thomas', 'I->E');
+        }, notFound());
+      },
+      'it should return the correct node.': function itShouldReturnTheCorrectNode() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['Thomas', 'Estelle']);
+        var edge = graph.addEdge('Thomas', 'Estelle');
+
+        _assert["default"].strictEqual(graph.opposite('Thomas', edge), 'Estelle');
+      }
+    },
+    '#.hasExtremity': {
+      'it should throw if either the edge is not found in the graph.': function itShouldThrowIfEitherTheEdgeIsNotFoundInTheGraph() {
+        var graph = new Graph();
+        graph.mergeEdge('Thomas', 'Laura');
+
+        _assert["default"]["throws"](function () {
+          graph.hasExtremity('inexisting-edge', 'Thomas');
+        }, notFound());
+      },
+      'it should return the correct answer.': function itShouldReturnTheCorrectAnswer() {
+        var graph = new Graph();
+        graph.addNode('Jack');
+
+        var _graph$mergeEdge = graph.mergeEdge('Thomas', 'Estelle'),
+            edge = _graph$mergeEdge[0];
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Thomas'), true);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Estelle'), true);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Jack'), false);
+
+        _assert["default"].strictEqual(graph.hasExtremity(edge, 'Who?'), false);
+      }
+    },
+    '#.isDirected': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isDirected('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is directed or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsDirectedOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        graph.addNode('Suzan');
+        var directedEdge = graph.addDirectedEdge('John', 'Rachel'),
+            undirectedEdge = graph.addUndirectedEdge('Rachel', 'Suzan');
+
+        _assert["default"].strictEqual(graph.isDirected(directedEdge), true);
+
+        _assert["default"].strictEqual(graph.isDirected(undirectedEdge), false);
+      }
+    },
+    '#.isUndirected': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isUndirected('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is undirected or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsUndirectedOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        graph.addNode('Suzan');
+        var directedEdge = graph.addDirectedEdge('John', 'Rachel'),
+            undirectedEdge = graph.addUndirectedEdge('Rachel', 'Suzan');
+
+        _assert["default"].strictEqual(graph.isUndirected(directedEdge), false);
+
+        _assert["default"].strictEqual(graph.isUndirected(undirectedEdge), true);
+      }
+    },
+    '#.isSelfLoop': {
+      'it should throw if the edge is not in the graph.': function itShouldThrowIfTheEdgeIsNotInTheGraph() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph.isSelfLoop('test');
+        }, notFound());
+      },
+      'it should correctly return whether the edge is a self-loop or not.': function itShouldCorrectlyReturnWhetherTheEdgeIsASelfLoopOrNot() {
+        var graph = new Graph();
+        graph.addNode('John');
+        graph.addNode('Rachel');
+        var selfLoop = graph.addDirectedEdge('John', 'John'),
+            edge = graph.addUndirectedEdge('John', 'Rachel');
+
+        _assert["default"].strictEqual(graph.isSelfLoop(selfLoop), true);
+
+        _assert["default"].strictEqual(graph.isSelfLoop(edge), false);
+      }
+    },
+    Degree: {
+      '#.inDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.inDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct in degree.': function itShouldReturnTheCorrectInDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('William', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Sue'), 2);
+
+          graph.addDirectedEdge('Sue', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Sue'), 3);
+
+          _assert["default"].strictEqual(graph.inDegreeWithoutSelfLoops('Sue'), 2);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Helen'), 0);
+        }
+      },
+      '#.inboundDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.inboundDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct in degree.': function itShouldReturnTheCorrectInDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('William', 'Sue');
+          graph.addUndirectedEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Sue'), 3);
+
+          graph.addDirectedEdge('Sue', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Sue'), 4);
+
+          _assert["default"].strictEqual(graph.inboundDegreeWithoutSelfLoops('Sue'), 3);
+        },
+        'it should always the undirected degree in an undirected graph.': function itShouldAlwaysTheUndirectedDegreeInAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inboundDegree('Helen'), 1);
+        }
+      },
+      '#.outDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.outDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct out degree.': function itShouldReturnTheCorrectOutDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+
+          _assert["default"].strictEqual(graph.outDegree('Helen'), 2);
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.outDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.outDegreeWithoutSelfLoops('Helen'), 2);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outDegree('Sue'), 0);
+        }
+      },
+      '#.outboundDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.outboundDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct out degree.': function itShouldReturnTheCorrectOutDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addUndirectedEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Helen'), 3);
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Helen'), 4);
+
+          _assert["default"].strictEqual(graph.outboundDegreeWithoutSelfLoops('Helen'), 3);
+        },
+        'it should always the undirected degree in an undirected graph.': function itShouldAlwaysTheUndirectedDegreeInAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.outboundDegree('Sue'), 1);
+        }
+      },
+      '#.directedDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.directedDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct directed degree.': function itShouldReturnTheCorrectDirectedDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John', 'Martha']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addDirectedEdge('Martha', 'Helen');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), graph.inDegree('Helen') + graph.outDegree('Helen'));
+
+          graph.addDirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.directedDegree('Helen'), 5);
+
+          _assert["default"].strictEqual(graph.directedDegreeWithoutSelfLoops('Helen'), 3);
+        },
+        'it should always return 0 in an undirected graph.': function itShouldAlwaysReturn0InAnUndirectedGraph() {
+          var graph = new Graph({
+            type: 'undirected'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.inDegree('Helen'), 0);
+        }
+      },
+      '#.undirectedDegree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.undirectedDegree('Test');
+          }, notFound());
+        },
+        'it should return the correct undirected degree.': function itShouldReturnTheCorrectUndirectedDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 1);
+
+          graph.addUndirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 3);
+
+          _assert["default"].strictEqual(graph.undirectedDegreeWithoutSelfLoops('Helen'), 1);
+        },
+        'it should always return 0 in a directed graph.': function itShouldAlwaysReturn0InADirectedGraph() {
+          var graph = new Graph({
+            type: 'directed'
+          });
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue']);
+          graph.addEdge('Helen', 'Sue');
+
+          _assert["default"].strictEqual(graph.undirectedDegree('Helen'), 0);
+        }
+      },
+      '#.degree': {
+        'it should throw if the node is not found in the graph.': function itShouldThrowIfTheNodeIsNotFoundInTheGraph() {
+          var graph = new Graph();
+
+          _assert["default"]["throws"](function () {
+            graph.degree('Test');
+          }, notFound());
+        },
+        'it should return the correct degree.': function itShouldReturnTheCorrectDegree() {
+          var graph = new Graph();
+          (0, _helpers.addNodesFrom)(graph, ['Helen', 'Sue', 'William', 'John', 'Martha']);
+          graph.addDirectedEdge('Helen', 'Sue');
+          graph.addDirectedEdge('Helen', 'William');
+          graph.addDirectedEdge('Martha', 'Helen');
+          graph.addUndirectedEdge('Helen', 'John');
+
+          _assert["default"].strictEqual(graph.degree('Helen'), 4);
+
+          _assert["default"].strictEqual(graph.degree('Helen'), graph.directedDegree('Helen') + graph.undirectedDegree('Helen'));
+
+          graph.addUndirectedEdge('Helen', 'Helen');
+
+          _assert["default"].strictEqual(graph.degree('Helen'), 6);
+
+          _assert["default"].strictEqual(graph.degreeWithoutSelfLoops('Helen'), 4);
+        }
+      },
+      'it should also work with typed graphs.': function itShouldAlsoWorkWithTypedGraphs() {
+        var directedGraph = new Graph({
+          type: 'directed'
+        }),
+            undirectedGraph = new Graph({
+          type: 'undirected'
+        });
+        (0, _helpers.addNodesFrom)(directedGraph, [1, 2]);
+        (0, _helpers.addNodesFrom)(undirectedGraph, [1, 2]);
+
+        _assert["default"].strictEqual(directedGraph.degree(1), 0);
+
+        _assert["default"].strictEqual(undirectedGraph.degree(1), 0);
+
+        directedGraph.addDirectedEdge(1, 2);
+        undirectedGraph.addUndirectedEdge(1, 2);
+
+        _assert["default"].strictEqual(directedGraph.degree(1), 1);
+
+        _assert["default"].strictEqual(undirectedGraph.degree(1), 1);
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/serialization.js b/libs/shared/graph-layouts/node_modules/graphology/specs/serialization.js
new file mode 100644
index 0000000000000000000000000000000000000000..fc9318ae907039544a0aa110933e4c56ad84e946
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/serialization.js
@@ -0,0 +1,171 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = serialization;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Serializaton Specs
+ * ==============================
+ *
+ * Testing the serialization methods of the graph.
+ */
+function serialization(Graph, checkers) {
+  var invalid = checkers.invalid,
+      notFound = checkers.notFound;
+  return {
+    '#.export': {
+      'it should correctly return the serialized graph.': function itShouldCorrectlyReturnTheSerializedGraph() {
+        var graph = new Graph({
+          multi: true
+        });
+        graph.setAttribute('name', 'graph');
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Jack', 'Martha']);
+        graph.setNodeAttribute('John', 'age', 34);
+        graph.addEdgeWithKey('J->J•1', 'John', 'Jack');
+        graph.addEdgeWithKey('J->J•2', 'John', 'Jack', {
+          weight: 2
+        });
+        graph.addEdgeWithKey('J->J•3', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•1', 'John', 'Jack');
+        graph.addUndirectedEdgeWithKey('J<->J•2', 'John', 'Jack', {
+          weight: 3
+        });
+
+        _assert["default"].deepStrictEqual(graph["export"](), {
+          attributes: {
+            name: 'graph'
+          },
+          nodes: [{
+            key: 'John',
+            attributes: {
+              age: 34
+            }
+          }, {
+            key: 'Jack'
+          }, {
+            key: 'Martha'
+          }],
+          edges: [{
+            key: 'J->J•1',
+            source: 'John',
+            target: 'Jack'
+          }, {
+            key: 'J->J•2',
+            source: 'John',
+            target: 'Jack',
+            attributes: {
+              weight: 2
+            }
+          }, {
+            key: 'J->J•3',
+            source: 'John',
+            target: 'Jack'
+          }, {
+            key: 'J<->J•1',
+            source: 'John',
+            target: 'Jack',
+            undirected: true
+          }, {
+            key: 'J<->J•2',
+            source: 'John',
+            target: 'Jack',
+            attributes: {
+              weight: 3
+            },
+            undirected: true
+          }],
+          options: {
+            allowSelfLoops: true,
+            multi: true,
+            type: 'mixed'
+          }
+        });
+      }
+    },
+    '#.import': {
+      'it should throw if the given data is invalid.': function itShouldThrowIfTheGivenDataIsInvalid() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph["import"](true);
+        }, invalid());
+      },
+      'it should be possible to import a graph instance.': function itShouldBePossibleToImportAGraphInstance() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var other = new Graph();
+        other["import"](graph);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), other.nodes());
+
+        _assert["default"].deepStrictEqual(graph.edges(), other.edges());
+      },
+      'it should be possible to import a serialized graph.': function itShouldBePossibleToImportASerializedGraph() {
+        var graph = new Graph();
+        graph["import"]({
+          nodes: [{
+            key: 'John'
+          }, {
+            key: 'Thomas'
+          }],
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        });
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to import only edges when merging.': function itShouldBePossibleToImportOnlyEdgesWhenMerging() {
+        var graph = new Graph();
+        graph["import"]({
+          edges: [{
+            source: 'John',
+            target: 'Thomas'
+          }]
+        }, true);
+
+        _assert["default"].deepStrictEqual(graph.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(graph.size, 1);
+
+        _assert["default"].strictEqual(graph.hasEdge('John', 'Thomas'), true);
+      },
+      'it should be possible to import attributes.': function itShouldBePossibleToImportAttributes() {
+        var graph = new Graph();
+        graph["import"]({
+          attributes: {
+            name: 'graph'
+          }
+        });
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), {
+          name: 'graph'
+        });
+      },
+      'it should throw if nodes are absent, edges are present and we merge.': function itShouldThrowIfNodesAreAbsentEdgesArePresentAndWeMerge() {
+        var graph = new Graph();
+
+        _assert["default"]["throws"](function () {
+          graph["import"]({
+            edges: [{
+              source: '1',
+              target: '2'
+            }]
+          });
+        }, notFound());
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/graphology/specs/utils.js b/libs/shared/graph-layouts/node_modules/graphology/specs/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b7158a8a9d0b1236b83e7dff5a749cdb1cf299a
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/graphology/specs/utils.js
@@ -0,0 +1,249 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports["default"] = utils;
+
+var _assert = _interopRequireDefault(require("assert"));
+
+var _helpers = require("./helpers");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+/**
+ * Graphology Utils Specs
+ * =======================
+ *
+ * Testing the utils methods.
+ */
+var PROPERTIES = ['type', 'multi', 'map', 'selfLoops'];
+
+function utils(Graph, checkers) {
+  var usage = checkers.usage;
+  return {
+    '#.nullCopy': {
+      'it should create an null copy of the graph.': function itShouldCreateAnNullCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.nullCopy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), []);
+
+        _assert["default"].strictEqual(copy.order, 0);
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], copy[property]);
+        });
+      },
+      'it should be possible to pass options to merge.': function itShouldBePossibleToPassOptionsToMerge() {
+        var graph = new Graph({
+          type: 'directed'
+        });
+        var copy = graph.nullCopy({
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(copy.type, 'undirected');
+
+        _assert["default"]["throws"](function () {
+          copy.addDirectedEdge('one', 'two');
+        }, /addDirectedEdge/);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.nullCopy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      }
+    },
+    '#.emptyCopy': {
+      'it should create an empty copy of the graph.': function itShouldCreateAnEmptyCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.emptyCopy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), ['John', 'Thomas']);
+
+        _assert["default"].strictEqual(copy.order, 2);
+
+        _assert["default"].strictEqual(copy.size, 0);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], copy[property]);
+        });
+        copy.mergeEdge('Mary', 'Thomas');
+        copy.setNodeAttribute('John', 'age', 32);
+
+        _assert["default"].strictEqual(copy.order, 3);
+
+        _assert["default"].strictEqual(copy.size, 1);
+
+        _assert["default"].deepStrictEqual(copy.getNodeAttributes('John'), {
+          age: 32
+        });
+
+        _assert["default"].deepStrictEqual(graph.getNodeAttributes('John'), {});
+      },
+      'it should be possible to pass options to merge.': function itShouldBePossibleToPassOptionsToMerge() {
+        var graph = new Graph({
+          type: 'directed'
+        });
+        var copy = graph.emptyCopy({
+          type: 'undirected'
+        });
+
+        _assert["default"].strictEqual(copy.type, 'undirected');
+
+        _assert["default"]["throws"](function () {
+          copy.addDirectedEdge('one', 'two');
+        }, /addDirectedEdge/);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.emptyCopy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      }
+    },
+    '#.copy': {
+      'it should create a full copy of the graph.': function itShouldCreateAFullCopyOfTheGraph() {
+        var graph = new Graph();
+        (0, _helpers.addNodesFrom)(graph, ['John', 'Thomas']);
+        graph.addEdge('John', 'Thomas');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(copy.nodes(), graph.nodes());
+
+        _assert["default"].deepStrictEqual(copy.edges(), graph.edges());
+
+        _assert["default"].strictEqual(copy.order, 2);
+
+        _assert["default"].strictEqual(copy.size, 1);
+
+        PROPERTIES.forEach(function (property) {
+          _assert["default"].strictEqual(graph[property], graph[property]);
+        });
+      },
+      'it should not break when copying a graph with wrangled edge ids (issue #213).': function itShouldNotBreakWhenCopyingAGraphWithWrangledEdgeIdsIssue213() {
+        var graph = new Graph();
+        graph.addNode('n0');
+        graph.addNode('n1');
+        graph.addNode('n2');
+        graph.addNode('n3');
+        graph.addEdge('n0', 'n1');
+        graph.addEdge('n1', 'n2');
+        graph.addEdge('n2', 'n3');
+        graph.addEdge('n3', 'n0');
+
+        _assert["default"].doesNotThrow(function () {
+          graph.copy();
+        }); // Do surgery on second edge
+
+
+        var edgeToSplit = graph.edge('n1', 'n2');
+        var newNode = 'n12';
+        graph.addNode(newNode);
+        graph.dropEdge('n1', 'n2');
+        graph.addEdge('n1', newNode);
+        graph.addEdgeWithKey(edgeToSplit, newNode, 'n2');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(new Set(graph.nodes()), new Set(copy.nodes()));
+
+        _assert["default"].deepStrictEqual(new Set(graph.edges()), new Set(copy.edges()));
+
+        _assert["default"].notStrictEqual(graph.getNodeAttributes('n1'), copy.getNodeAttributes('n1'));
+
+        _assert["default"].doesNotThrow(function () {
+          copy.addEdge('n0', 'n12');
+        });
+      },
+      'it should not break on adversarial inputs.': function itShouldNotBreakOnAdversarialInputs() {
+        var graph = new Graph();
+        graph.mergeEdge(0, 1);
+        graph.mergeEdge(1, 2);
+        graph.mergeEdge(2, 0);
+        var copy = graph.copy();
+        copy.mergeEdge(3, 4);
+        var serializedCopy = Graph.from(graph["export"]());
+
+        _assert["default"].doesNotThrow(function () {
+          serializedCopy.mergeEdge(3, 4);
+        });
+
+        _assert["default"].strictEqual(serializedCopy.size, 4);
+      },
+      'copying the graph should conserve its attributes.': function copyingTheGraphShouldConserveItsAttributes() {
+        var graph = new Graph();
+        graph.setAttribute('title', 'The Graph');
+        var copy = graph.copy();
+
+        _assert["default"].deepStrictEqual(graph.getAttributes(), copy.getAttributes());
+      },
+      'it should be possible to upgrade a graph.': function itShouldBePossibleToUpgradeAGraph() {
+        var strict = new Graph({
+          type: 'directed',
+          allowSelfLoops: false
+        });
+        var loose = new Graph({
+          multi: true
+        });
+
+        _assert["default"].strictEqual(strict.copy({
+          type: 'directed'
+        }).type, 'directed');
+
+        _assert["default"].strictEqual(strict.copy({
+          multi: false
+        }).multi, false);
+
+        _assert["default"].strictEqual(strict.copy({
+          allowSelfLoops: false
+        }).allowSelfLoops, false);
+
+        _assert["default"].strictEqual(strict.copy({
+          type: 'mixed'
+        }).type, 'mixed');
+
+        _assert["default"].strictEqual(strict.copy({
+          multi: true
+        }).multi, true);
+
+        _assert["default"].strictEqual(strict.copy({
+          allowSelfLoops: true
+        }).allowSelfLoops, true);
+
+        _assert["default"]["throws"](function () {
+          strict.copy({
+            type: 'undirected'
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            type: 'undirected'
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            multi: false
+          });
+        }, usage());
+
+        _assert["default"]["throws"](function () {
+          loose.copy({
+            allowSelfLoops: false
+          });
+        }, usage());
+      }
+    }
+  };
+}
\ No newline at end of file
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/LICENSE.txt b/libs/shared/graph-layouts/node_modules/obliterator/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df12ee58e0ff4fa313b5bc24e694918ccc1b363a
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-2021 Guillaume Plique (Yomguithereal)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/README.md b/libs/shared/graph-layouts/node_modules/obliterator/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8f3fc6656314b07a90c3fa644395b72d59f9db64
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/README.md
@@ -0,0 +1,415 @@
+[![Build Status](https://github.com/Yomguithereal/obliterator/workflows/Tests/badge.svg)](https://github.com/Yomguithereal/obliterator/actions)
+
+# Obliterator
+
+Obliterator is a dead simple JavaScript/TypeScript library providing miscellaneous higher-order iterator/iterable functions such as combining two or more iterators into a single one.
+
+Note that when possible, `obliterator` also consider sequences such as arrays, strings etc. as valid iterables (although they are not proper ES6 iterables values), for convenience.
+
+# Installation
+
+```
+npm install --save obliterator
+```
+
+Note that `obliterator` comes along with its TypeScript declarations.
+
+# Usage
+
+## Summary
+
+_Classes_
+
+- [Iterator](#iterator)
+
+_Functions_
+
+- [chain](#chain)
+- [combinations](#combinations)
+- [consume](#consume)
+- [every](#every)
+- [filter](#filter)
+- [find](#find)
+- [forEach](#foreach)
+- [forEachWithNullKeys](#foreachwithnullkeys)
+- [includes](#includes)
+- [iter](#iter)
+- [map](#map)
+- [match](#match)
+- [permutations](#permutations)
+- [powerSet](#powerSet)
+- [some](#some)
+- [split](#split)
+- [take](#take)
+
+## Iterator
+
+A handy Iterator class easily usable with ES2015's `for ... of` loop constructs & spread operator.
+
+```js
+import Iterator from 'obliterator/iterator';
+// Or
+import {Iterator} from 'obliterator';
+
+const iterator = new Iterator(function () {
+  // Define what the `next` function does
+  return {done: false, value: 34};
+});
+
+// Checking that the given value is an iterator (native or else)
+Iterator.is(value);
+
+// Creating an empty iterator
+const emptyIterator = Iterator.empty();
+
+// Creating a simple iterator from a single value
+const simpleIterator = Iterator.of(34);
+
+// Creating a simple iterator from multiple values
+const multipleIterator = Iterator.of(1, 2, 3);
+```
+
+## chain
+
+Variadic function chaining all the given iterable-like values.
+
+```js
+import chain from 'obliterator/chain';
+// Or
+import {chain} from 'obliterator';
+
+const set1 = new Set('a');
+const set2 = new Set('bc');
+
+const chained = chain(set1.values(), set2);
+
+chained.next();
+>>> {done: false, value: 'a'}
+chained.next();
+>>> {done: false, value: 'b'}
+```
+
+## combinations
+
+Returns an iterator of combinations of the given array and of the given size.
+
+Note that for performance reasons, the yielded combination is always the same object.
+
+```js
+import combinations from 'obliterator/combinations';
+// Or
+import {combinations} from 'obliterator';
+
+const iterator = combinations(['A', 'B', 'C', 'D'], 2);
+
+iterator.next().value;
+>>> ['A', 'B']
+iterator.next().value;
+>>> ['A', 'C']
+```
+
+## consume
+
+Function consuming the given iterator fully or for n steps.
+
+```js
+import consume from 'obliterator/consume';
+// Or
+import {consume} from 'obliterator';
+
+const set = new Set([1, 2, 3]);
+
+// Consuming the whole iterator
+let iterator = set.values();
+consume(iterator);
+iterator.next().done >>> true;
+
+// Consuming n steps
+let iterator = set.values();
+consume(iterator, 2);
+iterator.next().value >>> 3;
+```
+
+## every
+
+Function returning whether all items of an iterable-like match the given predicate function.
+
+```js
+import every from 'obliterator/every';
+// Or
+import {every} from 'obliterator';
+
+every([2, 4, 6], n => n % 2 === 0);
+>>> true
+
+every([1, 2, 3], n => n % 2 === 0);
+>>> false
+```
+
+## filter
+
+Function returning an iterator filtering another one's values using the given predicate function.
+
+```js
+import filter from 'obliterator/filter';
+// Or
+import {filter} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const even = x => x % 2 === 0;
+
+const iterator = filter(set.values(), even);
+
+iterator.next().value >>> 2;
+iterator.next().value >>> 4;
+```
+
+## find
+
+Function returning the next item matching given predicate function in an iterable-like.
+
+```js
+import find from 'obliterator/find';
+// Or
+import {find} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const even = x => x % 2 === 0;
+
+const values = set.values();
+
+find(values, even);
+>>> 2
+
+find(values, even);
+>>> 4
+
+find(values, even);
+>>> undefined
+```
+
+## forEach
+
+Function able to iterate over almost any JavaScript iterable value using a callback.
+
+Supported values range from arrays, typed arrays, sets, maps, objects, strings, arguments, iterators, arbitrary iterables etc.
+
+```js
+import forEach from 'obliterator/foreach';
+// Or
+import {forEach} from 'obliterator';
+
+const set = new Set(['apple', 'banana']);
+
+forEach(set.values(), (value, i) => {
+  console.log(i, value);
+});
+
+// Iterating over a string
+forEach('abc', (char, i) => ...);
+
+// Iterating over a map
+forEach(map, (value, key) => ...);
+```
+
+## forEachWithNullKeys
+
+Variant of [forEach](#foreach) one can use to iterate over mixed values but with the twist that iterables without proper keys (lists, sets etc.), will yield `null` instead of an index key.
+
+Supported values range from arrays, typed arrays, sets, maps, objects, strings, arguments, iterators, arbitrary iterables etc.
+
+```js
+import {forEachWithNullKeys} from 'obliterator/foreach';
+
+const set = new Set(['apple', 'banana']);
+
+forEach(set, (value, key) => {
+  console.log(key, value);
+});
+>>> null, 'apple'
+>>> null, 'banana'
+```
+
+## includes
+
+Function returning whether the given value can be found in given iterable-like.
+
+```js
+import {includes} from 'obliterator';
+// Or
+import includes from 'obliterator/includes';
+
+includes([1, 2, 3], 3);
+>>> true;
+
+includes('test', 'a');
+>>> false;
+```
+
+## iter
+
+Function casting any iterable-like value to a proper iterator. Will throw an error if the given value cannot be cast as an iterator.
+
+```js
+import {iter} from 'obliterator';
+// Or
+import iter from 'obliterator/iter';
+
+iter('test');
+iter(new Set([1, 2, 3]));
+
+// This will throw:
+iter(null);
+```
+
+## map
+
+Function returning an iterator mapping another one's values using the given function.
+
+```js
+import map from 'obliterator/map';
+// Or
+import {map} from 'obliterator';
+
+const set = new Set([1, 2, 3, 4, 5]);
+
+const triple = x => x * 3;
+
+const iterator = map(set.values(), triple);
+
+iterator.next().value >>> 3;
+iterator.next().value >>> 6;
+```
+
+## match
+
+Function returning an iterator over the matches of a given regex applied to the target string.
+
+```js
+import match from 'obliterator/match';
+// Or
+import {match} from 'obliterator';
+
+const iterator = match(/t/, 'test');
+
+iterator.next().value.index >>> 0;
+iterator.next().value.index >>> 3;
+```
+
+## permutations
+
+Returns an iterator of permutations of the given array and of the given size.
+
+Note that for performance reasons, the yielded permutation is always the same object.
+
+```js
+import permutations from 'obliterator/permutations';
+// Or
+import {permutations} from 'obliterator';
+
+let iterator = permutations([1, 2, 3]);
+
+iterator.next().value
+>>> [1, 2, 3]
+iterator.next().value
+>>> [1, 3, 2]
+
+iterator = permutations(['A', 'B', 'C', 'D'], 2);
+
+iterator.next().value;
+>>> ['A', 'B']
+iterator.next().value;
+>>> ['A', 'C']
+```
+
+## powerSet
+
+Returns an iterator of sets composing the power set of the given array.
+
+```js
+import powerSet from 'obliterator/power-set';
+// Or
+import {powerSet} from 'obliterator';
+
+const iterator = powerSet(['A', 'B', 'C']);
+
+iterator.next().value;
+>>> []
+iterator.next().value;
+>>> ['A']
+```
+
+## some
+
+Returns whether the given iterable-like has some item matching the given predicate function.
+
+```js
+import some from 'obliterator/some';
+// Or
+import {some} from 'obliterator';
+
+some(new Set([1, 2, 3]), n => n % 2 === 0);
+>>> true
+
+some('test', c => c === 'a');
+>>> false
+```
+
+## split
+
+Returns an iterator over the splits of the target string, according to the given RegExp pattern.
+
+```js
+import split from 'obliterator/split';
+// Or
+import {split} from 'obliterator';
+
+const iterator = split(/;/g, 'hello;world;super');
+
+iterator.next().value;
+>>> 'hello'
+iterator.next().value;
+>>> 'world'
+```
+
+## take
+
+Function taking values from given iterator and returning them in an array.
+
+```js
+import take from 'obliterator/take';
+// Or
+import {take} from 'obliterator';
+
+const set = new Set([1, 2, 3]);
+
+// To take n values from the iterator
+take(set.values(), 2);
+>>> [1, 2]
+
+// To convert the full iterator into an array
+take(set.values());
+>>> [1, 2, 3]
+```
+
+# Contribution
+
+Contributions are obviously welcome. Please be sure to lint the code & add the relevant unit tests before submitting any PR.
+
+```
+git clone git@github.com:Yomguithereal/obliterator.git
+cd obliterator
+npm install
+
+# To lint the code
+npm run lint
+
+# To run the unit tests
+npm test
+```
+
+# License
+
+[MIT](LICENSE.txt)
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/chain.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/chain.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cc4c78db9a19d49256e93ba0d9ae811cd51635d
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/chain.d.ts
@@ -0,0 +1,5 @@
+import type {IntoInterator} from './types';
+
+export default function chain<T>(
+  ...iterables: IntoInterator<T>[]
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/chain.js b/libs/shared/graph-layouts/node_modules/obliterator/chain.js
new file mode 100644
index 0000000000000000000000000000000000000000..00eb68dced0dd4e5253f5bbee1e96e5090acb509
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/chain.js
@@ -0,0 +1,46 @@
+/**
+ * Obliterator Chain Function
+ * ===========================
+ *
+ * Variadic function combining the given iterables.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Chain.
+ *
+ * @param  {...Iterator} iterables - Target iterables.
+ * @return {Iterator}
+ */
+module.exports = function chain() {
+  var iterables = arguments;
+  var current = null;
+  var i = -1;
+
+  /* eslint-disable no-constant-condition */
+  return new Iterator(function next() {
+    var step = null;
+
+    do {
+      if (current === null) {
+        i++;
+
+        if (i >= iterables.length) return {done: true};
+
+        current = iter(iterables[i]);
+      }
+
+      step = current.next();
+
+      if (step.done === true) {
+        current = null;
+        continue;
+      }
+
+      break;
+    } while (true);
+
+    return step;
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/combinations.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/combinations.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..901a0eaa9bfba7e92b6521f2d500fc42cff9438b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/combinations.d.ts
@@ -0,0 +1,4 @@
+export default function combinations<T>(
+  array: Array<T>,
+  r: number
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/combinations.js b/libs/shared/graph-layouts/node_modules/obliterator/combinations.js
new file mode 100644
index 0000000000000000000000000000000000000000..c60099d328b9badc21a5099b4fbc26219bd31f00
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/combinations.js
@@ -0,0 +1,76 @@
+/**
+ * Obliterator Combinations Function
+ * ==================================
+ *
+ * Iterator returning combinations of the given array.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Helper mapping indices to items.
+ */
+function indicesToItems(target, items, indices, r) {
+  for (var i = 0; i < r; i++) target[i] = items[indices[i]];
+}
+
+/**
+ * Combinations.
+ *
+ * @param  {array}    array - Target array.
+ * @param  {number}   r     - Size of the subsequences.
+ * @return {Iterator}
+ */
+module.exports = function combinations(array, r) {
+  if (!Array.isArray(array))
+    throw new Error(
+      'obliterator/combinations: first argument should be an array.'
+    );
+
+  var n = array.length;
+
+  if (typeof r !== 'number')
+    throw new Error(
+      'obliterator/combinations: second argument should be omitted or a number.'
+    );
+
+  if (r > n)
+    throw new Error(
+      'obliterator/combinations: the size of the subsequences should not exceed the length of the array.'
+    );
+
+  if (r === n) return Iterator.of(array.slice());
+
+  var indices = new Array(r),
+    subsequence = new Array(r),
+    first = true,
+    i;
+
+  for (i = 0; i < r; i++) indices[i] = i;
+
+  return new Iterator(function next() {
+    if (first) {
+      first = false;
+
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    if (indices[r - 1]++ < n - 1) {
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    i = r - 2;
+
+    while (i >= 0 && indices[i] >= n - (r - i)) --i;
+
+    if (i < 0) return {done: true};
+
+    indices[i]++;
+
+    while (++i < r) indices[i] = indices[i - 1] + 1;
+
+    indicesToItems(subsequence, array, indices, r);
+    return {value: subsequence, done: false};
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/consume.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/consume.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16812ee0ec670149dec8046f62f638e8715a5619
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/consume.d.ts
@@ -0,0 +1 @@
+export default function consume<T>(iterator: Iterator<T>, steps?: number): void;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/consume.js b/libs/shared/graph-layouts/node_modules/obliterator/consume.js
new file mode 100644
index 0000000000000000000000000000000000000000..1fa7007490deca7579d442a462bf1b0ce9828266
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/consume.js
@@ -0,0 +1,29 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Consume Function
+ * =============================
+ *
+ * Function consuming the given iterator for n or every steps.
+ */
+
+/**
+ * Consume.
+ *
+ * @param  {Iterator} iterator - Target iterator.
+ * @param  {number}   [steps]  - Optional steps.
+ */
+module.exports = function consume(iterator, steps) {
+  var step,
+    l = arguments.length > 1 ? steps : Infinity,
+    i = 0;
+
+  while (true) {
+    if (i === l) return;
+
+    step = iterator.next();
+
+    if (step.done) return;
+
+    i++;
+  }
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/every.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/every.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..361fd48e24e01e03cebae14c544c31ddcef0a288
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/every.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function every<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): boolean;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/every.js b/libs/shared/graph-layouts/node_modules/obliterator/every.js
new file mode 100644
index 0000000000000000000000000000000000000000..f2d296c9426c6ce58b88c841057da8b76884c06c
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/every.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Every Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning whether all
+ * its items match the given predicate.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Every.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function every(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (!predicate(step.value)) return false;
+  }
+
+  return true;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/filter.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/filter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91982950c2f56bdc143e6dca7a857736cff4f6eb
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/filter.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function filter<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/filter.js b/libs/shared/graph-layouts/node_modules/obliterator/filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..b17945e0eba88a6f8a3f9a8fc8e5de8488ac911f
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/filter.js
@@ -0,0 +1,28 @@
+/**
+ * Obliterator Filter Function
+ * ===========================
+ *
+ * Function returning a iterator filtering the given iterator.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Filter.
+ *
+ * @param  {Iterable} target    - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {Iterator}
+ */
+module.exports = function filter(target, predicate) {
+  var iterator = iter(target);
+  var step;
+
+  return new Iterator(function () {
+    do {
+      step = iterator.next();
+    } while (!step.done && !predicate(step.value));
+
+    return step;
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/find.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/find.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3dea30c89a36c675f63dbbc4d0ff89b520251d2
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/find.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function find<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): T | undefined;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/find.js b/libs/shared/graph-layouts/node_modules/obliterator/find.js
new file mode 100644
index 0000000000000000000000000000000000000000..d7641cebf1b57238034d8e5ad9178743d632b63e
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/find.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Find Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning the first item
+ * matching the given predicate.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Find.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function find(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (predicate(step.value)) return step.value;
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b30738264f40293497a2b67f1f8d657fa9943c3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.d.ts
@@ -0,0 +1,29 @@
+import type {Sequence} from './types';
+
+interface ForEachTrait<K, V> {
+  forEach(callback: (value: V, key: K, self: this) => void): void;
+}
+
+interface PlainObject<T> {
+  [key: string]: T;
+}
+
+export default function forEachWithNullKeys<V>(
+  iterable: Set<V>,
+  callback: (value: V, key: null) => void
+): void;
+
+export default function forEachWithNullKeys<K, V>(
+  iterable: ForEachTrait<K, V>,
+  callback: (value: V, key: K) => void
+): void;
+
+export default function forEachWithNullKeys<T>(
+  iterable: Iterator<T> | Iterable<T> | Sequence<T>,
+  callback: (item: T, key: null) => void
+): void;
+
+export default function forEachWithNullKeys<T>(
+  object: PlainObject<T>,
+  callback: (value: T, key: string) => void
+): void;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.js b/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.js
new file mode 100644
index 0000000000000000000000000000000000000000..97135add134c8529fcd1d1885b07626cd48255f5
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/foreach-with-null-keys.js
@@ -0,0 +1,83 @@
+/**
+ * Obliterator ForEachWithNullKeys Function
+ * =========================================
+ *
+ * Helper function used to easily iterate over mixed values.
+ */
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+/**
+ * Same function as the `forEach` but will yield `null` when the target
+ * does not have keys.
+ *
+ * @param  {any}      iterable - Iterable value.
+ * @param  {function} callback - Callback function.
+ */
+module.exports = function forEachWithNullKeys(iterable, callback) {
+  var iterator, k, i, l, s;
+
+  if (!iterable)
+    throw new Error('obliterator/forEachWithNullKeys: invalid iterable.');
+
+  if (typeof callback !== 'function')
+    throw new Error('obliterator/forEachWithNullKeys: expecting a callback.');
+
+  // The target is an array or a string or function arguments
+  if (
+    Array.isArray(iterable) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(iterable)) ||
+    typeof iterable === 'string' ||
+    iterable.toString() === '[object Arguments]'
+  ) {
+    for (i = 0, l = iterable.length; i < l; i++) callback(iterable[i], null);
+    return;
+  }
+
+  // The target is a Set
+  if (iterable instanceof Set) {
+    iterable.forEach(function (value) {
+      callback(value, null);
+    });
+    return;
+  }
+
+  // The target has a #.forEach method
+  if (typeof iterable.forEach === 'function') {
+    iterable.forEach(callback);
+    return;
+  }
+
+  // The target is iterable
+  if (
+    SYMBOL_SUPPORT &&
+    Symbol.iterator in iterable &&
+    typeof iterable.next !== 'function'
+  ) {
+    iterable = iterable[Symbol.iterator]();
+  }
+
+  // The target is an iterator
+  if (typeof iterable.next === 'function') {
+    iterator = iterable;
+    i = 0;
+
+    while (((s = iterator.next()), s.done !== true)) {
+      callback(s.value, null);
+      i++;
+    }
+
+    return;
+  }
+
+  // The target is a plain object
+  for (k in iterable) {
+    if (iterable.hasOwnProperty(k)) {
+      callback(iterable[k], k);
+    }
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/foreach.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/foreach.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db0bac77e612ed92e8a7bac4b82c87656280fdf9
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/foreach.d.ts
@@ -0,0 +1,24 @@
+import type {Sequence} from './types';
+
+interface ForEachTrait<K, V> {
+  forEach(callback: (value: V, key: K, self: this) => void): void;
+}
+
+interface PlainObject<T> {
+  [key: string]: T;
+}
+
+export default function forEach<K, V>(
+  iterable: ForEachTrait<K, V>,
+  callback: (value: V, key: K) => void
+): void;
+
+export default function forEach<T>(
+  iterable: Iterator<T> | Iterable<T> | Sequence<T>,
+  callback: (item: T, index: number) => void
+): void;
+
+export default function forEach<T>(
+  object: PlainObject<T>,
+  callback: (value: T, key: string) => void
+): void;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/foreach.js b/libs/shared/graph-layouts/node_modules/obliterator/foreach.js
new file mode 100644
index 0000000000000000000000000000000000000000..84af94fe97a3f4b65368e024ac15dc396c33a70b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/foreach.js
@@ -0,0 +1,73 @@
+/**
+ * Obliterator ForEach Function
+ * =============================
+ *
+ * Helper function used to easily iterate over mixed values.
+ */
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+/**
+ * Function able to iterate over almost any iterable JS value.
+ *
+ * @param  {any}      iterable - Iterable value.
+ * @param  {function} callback - Callback function.
+ */
+module.exports = function forEach(iterable, callback) {
+  var iterator, k, i, l, s;
+
+  if (!iterable) throw new Error('obliterator/forEach: invalid iterable.');
+
+  if (typeof callback !== 'function')
+    throw new Error('obliterator/forEach: expecting a callback.');
+
+  // The target is an array or a string or function arguments
+  if (
+    Array.isArray(iterable) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(iterable)) ||
+    typeof iterable === 'string' ||
+    iterable.toString() === '[object Arguments]'
+  ) {
+    for (i = 0, l = iterable.length; i < l; i++) callback(iterable[i], i);
+    return;
+  }
+
+  // The target has a #.forEach method
+  if (typeof iterable.forEach === 'function') {
+    iterable.forEach(callback);
+    return;
+  }
+
+  // The target is iterable
+  if (
+    SYMBOL_SUPPORT &&
+    Symbol.iterator in iterable &&
+    typeof iterable.next !== 'function'
+  ) {
+    iterable = iterable[Symbol.iterator]();
+  }
+
+  // The target is an iterator
+  if (typeof iterable.next === 'function') {
+    iterator = iterable;
+    i = 0;
+
+    while (((s = iterator.next()), s.done !== true)) {
+      callback(s.value, i);
+      i++;
+    }
+
+    return;
+  }
+
+  // The target is a plain object
+  for (k in iterable) {
+    if (iterable.hasOwnProperty(k)) {
+      callback(iterable[k], k);
+    }
+  }
+
+  return;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/includes.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/includes.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1a8f9de2b39fc55cc9d46ab5b6c78fc8bbc27dce
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/includes.d.ts
@@ -0,0 +1,6 @@
+import type {IntoInterator} from './types';
+
+export default function includes<T>(
+  target: IntoInterator<T>,
+  value: T
+): boolean;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/includes.js b/libs/shared/graph-layouts/node_modules/obliterator/includes.js
new file mode 100644
index 0000000000000000000000000000000000000000..01c788b577c5f259ae1b3aa27ef9bd599f69a79c
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/includes.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Includes Function
+ * ==============================
+ *
+ * Function taking an iterable and returning whether the given item can be
+ * found in it.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Includes.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} value     - Searched value.
+ * @return {boolean}
+ */
+module.exports = function includes(iterable, value) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (step.value === value) return true;
+  }
+
+  return false;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/index.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a0371419f4d643b59eb9aec194a9786ac7e3966
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/index.d.ts
@@ -0,0 +1,20 @@
+export {default as Iterator} from './iterator';
+export {default as iter} from './iter';
+export {default as chain} from './chain';
+export {default as combinations} from './combinations';
+export {default as consume} from './consume';
+export {default as every} from './every';
+export {default as filter} from './filter';
+export {default as find} from './find';
+export {default as forEach} from './foreach';
+export {default as forEachWithNullKeys} from './foreach-with-null-keys';
+export {default as includes} from './includes';
+export {default as map} from './map';
+export {default as match} from './match';
+export {default as permutations} from './permutations';
+export {default as powerSet} from './power-set';
+export {default as range} from './range';
+export {default as some} from './some';
+export {default as split} from './split';
+export {default as take} from './take';
+export {default as takeInto} from './take-into';
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/index.js b/libs/shared/graph-layouts/node_modules/obliterator/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a55393a5d32a88c1fb794d90cafed28e8ccef64c
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/index.js
@@ -0,0 +1,26 @@
+/**
+ * Obliterator Library Endpoint
+ * =============================
+ *
+ * Exporting the library's functions.
+ */
+exports.Iterator = require('./iterator.js');
+exports.iter = require('./iter.js');
+exports.chain = require('./chain.js');
+exports.combinations = require('./combinations.js');
+exports.consume = require('./consume.js');
+exports.every = require('./every.js');
+exports.filter = require('./filter.js');
+exports.find = require('./find.js');
+exports.forEach = require('./foreach.js');
+exports.forEachWithNullKeys = require('./foreach-with-null-keys.js');
+exports.includes = require('./includes.js');
+exports.map = require('./map.js');
+exports.match = require('./match.js');
+exports.permutations = require('./permutations.js');
+exports.powerSet = require('./power-set.js');
+exports.range = require('./range.js');
+exports.some = require('./some.js');
+exports.split = require('./split.js');
+exports.take = require('./take.js');
+exports.takeInto = require('./take-into.js');
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/iter.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/iter.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..55377454d702e299afd045bcd4718cf2ea47fc23
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/iter.d.ts
@@ -0,0 +1 @@
+export default function iter<T>(target: unknown): Iterator<T>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/iter.js b/libs/shared/graph-layouts/node_modules/obliterator/iter.js
new file mode 100644
index 0000000000000000000000000000000000000000..12ab82f305a1ba077ddaaa616f3e653ac5ff5d85
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/iter.js
@@ -0,0 +1,46 @@
+/**
+ * Obliterator Iter Function
+ * ==========================
+ *
+ * Function coercing values to an iterator. It can be quite useful when needing
+ * to handle iterables and iterators the same way.
+ */
+var Iterator = require('./iterator.js');
+var support = require('./support.js');
+
+var ARRAY_BUFFER_SUPPORT = support.ARRAY_BUFFER_SUPPORT;
+var SYMBOL_SUPPORT = support.SYMBOL_SUPPORT;
+
+function iterOrNull(target) {
+  // Indexed sequence
+  if (
+    typeof target === 'string' ||
+    Array.isArray(target) ||
+    (ARRAY_BUFFER_SUPPORT && ArrayBuffer.isView(target))
+  )
+    return Iterator.fromSequence(target);
+
+  // Invalid value
+  if (typeof target !== 'object' || target === null) return null;
+
+  // Iterable
+  if (SYMBOL_SUPPORT && typeof target[Symbol.iterator] === 'function')
+    return target[Symbol.iterator]();
+
+  // Iterator duck-typing
+  if (typeof target.next === 'function') return target;
+
+  // Invalid object
+  return null;
+}
+
+module.exports = function iter(target) {
+  var iterator = iterOrNull(target);
+
+  if (!iterator)
+    throw new Error(
+      'obliterator: target is not iterable nor a valid iterator.'
+    );
+
+  return iterator;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/iterator.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/iterator.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8b10451a6be5f03adeea2c38e92f08f8109121a5
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/iterator.d.ts
@@ -0,0 +1,18 @@
+import type {Sequence} from './types';
+
+type NextFunction<V> = () => IteratorResult<V>;
+
+export default class ObliteratorIterator<V> implements IterableIterator<V> {
+  // Constructor
+  constructor(next: NextFunction<V>);
+
+  // Well-known methods
+  next(): IteratorResult<V>;
+  [Symbol.iterator](): IterableIterator<V>;
+
+  // Static methods
+  static of<T>(...args: T[]): ObliteratorIterator<T>;
+  static empty<T>(): ObliteratorIterator<T>;
+  static is(value: any): boolean;
+  static fromSequence<T>(sequence: Sequence<T>): ObliteratorIterator<T>;
+}
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/iterator.js b/libs/shared/graph-layouts/node_modules/obliterator/iterator.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa241892460e89232c0e5e155845eaa2a537e0b9
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/iterator.js
@@ -0,0 +1,96 @@
+/**
+ * Obliterator Iterator Class
+ * ===========================
+ *
+ * Simple class representing the library's iterators.
+ */
+
+/**
+ * Iterator class.
+ *
+ * @constructor
+ * @param {function} next - Next function.
+ */
+function Iterator(next) {
+  if (typeof next !== 'function')
+    throw new Error('obliterator/iterator: expecting a function!');
+
+  this.next = next;
+}
+
+/**
+ * If symbols are supported, we add `next` to `Symbol.iterator`.
+ */
+if (typeof Symbol !== 'undefined')
+  Iterator.prototype[Symbol.iterator] = function () {
+    return this;
+  };
+
+/**
+ * Returning an iterator of the given values.
+ *
+ * @param  {any...} values - Values.
+ * @return {Iterator}
+ */
+Iterator.of = function () {
+  var args = arguments,
+    l = args.length,
+    i = 0;
+
+  return new Iterator(function () {
+    if (i >= l) return {done: true};
+
+    return {done: false, value: args[i++]};
+  });
+};
+
+/**
+ * Returning an empty iterator.
+ *
+ * @return {Iterator}
+ */
+Iterator.empty = function () {
+  var iterator = new Iterator(function () {
+    return {done: true};
+  });
+
+  return iterator;
+};
+
+/**
+ * Returning an iterator over the given indexed sequence.
+ *
+ * @param  {string|Array} sequence - Target sequence.
+ * @return {Iterator}
+ */
+Iterator.fromSequence = function (sequence) {
+  var i = 0,
+    l = sequence.length;
+
+  return new Iterator(function () {
+    if (i >= l) return {done: true};
+
+    return {done: false, value: sequence[i++]};
+  });
+};
+
+/**
+ * Returning whether the given value is an iterator.
+ *
+ * @param  {any} value - Value.
+ * @return {boolean}
+ */
+Iterator.is = function (value) {
+  if (value instanceof Iterator) return true;
+
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    typeof value.next === 'function'
+  );
+};
+
+/**
+ * Exporting.
+ */
+module.exports = Iterator;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/map.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/map.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf087aae593614b1435bd1e7d24f362dad11d0d0
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/map.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type MapFunction<S, T> = (item: S) => T;
+
+export default function map<S, T>(
+  target: IntoInterator<S>,
+  predicate: MapFunction<S, T>
+): IterableIterator<T>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/map.js b/libs/shared/graph-layouts/node_modules/obliterator/map.js
new file mode 100644
index 0000000000000000000000000000000000000000..9efb94100fb98d35d257be42e45d4ed37d55a3cb
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/map.js
@@ -0,0 +1,29 @@
+/**
+ * Obliterator Map Function
+ * ===========================
+ *
+ * Function returning a iterator mapping the given iterator's values.
+ */
+var Iterator = require('./iterator.js');
+var iter = require('./iter.js');
+
+/**
+ * Map.
+ *
+ * @param  {Iterator} target - Target iterable.
+ * @param  {function} mapper - Map function.
+ * @return {Iterator}
+ */
+module.exports = function map(target, mapper) {
+  var iterator = iter(target);
+
+  return new Iterator(function next() {
+    var step = iterator.next();
+
+    if (step.done) return step;
+
+    return {
+      value: mapper(step.value)
+    };
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/match.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/match.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3ff382efdd69836f89c84530f8fda3f210a0ce4e
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/match.d.ts
@@ -0,0 +1,4 @@
+export default function match(
+  pattern: RegExp,
+  string: string
+): IterableIterator<string>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/match.js b/libs/shared/graph-layouts/node_modules/obliterator/match.js
new file mode 100644
index 0000000000000000000000000000000000000000..86c17fce555004920e6e1dd70b62c52daf761263
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/match.js
@@ -0,0 +1,43 @@
+/**
+ * Obliterator Match Function
+ * ===========================
+ *
+ * Function returning an iterator over the matches of the given regex on the
+ * target string.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Match.
+ *
+ * @param  {RegExp}   pattern - Regular expression to use.
+ * @param  {string}   string  - Target string.
+ * @return {Iterator}
+ */
+module.exports = function match(pattern, string) {
+  var executed = false;
+
+  if (!(pattern instanceof RegExp))
+    throw new Error(
+      'obliterator/match: invalid pattern. Expecting a regular expression.'
+    );
+
+  if (typeof string !== 'string')
+    throw new Error('obliterator/match: invalid target. Expecting a string.');
+
+  return new Iterator(function () {
+    if (executed && !pattern.global) {
+      pattern.lastIndex = 0;
+      return {done: true};
+    }
+
+    executed = true;
+
+    var m = pattern.exec(string);
+
+    if (m) return {value: m};
+
+    pattern.lastIndex = 0;
+    return {done: true};
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/package.json b/libs/shared/graph-layouts/node_modules/obliterator/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c36cb964f8985191f503bcfd3b9de44eb42ff770
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/package.json
@@ -0,0 +1,46 @@
+{
+  "name": "obliterator",
+  "version": "2.0.2",
+  "description": "Higher order iterator library for JavaScript/TypeScript.",
+  "main": "index.js",
+  "types": "index.d.ts",
+  "scripts": {
+    "lint": "eslint *.js",
+    "prepublishOnly": "npm run lint && npm test",
+    "prettier": "prettier --write '*.js' '*.ts'",
+    "test": "mocha test.js && npm run test:types",
+    "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns ./test-types.ts"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/yomguithereal/obliterator.git"
+  },
+  "keywords": [
+    "iterator"
+  ],
+  "author": {
+    "name": "Guillaume Plique",
+    "url": "http://github.com/Yomguithereal"
+  },
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/yomguithereal/obliterator/issues"
+  },
+  "homepage": "https://github.com/yomguithereal/obliterator#readme",
+  "devDependencies": {
+    "@yomguithereal/eslint-config": "^4.4.0",
+    "@yomguithereal/prettier-config": "^1.2.0",
+    "eslint": "^8.2.0",
+    "eslint-config-prettier": "^8.3.0",
+    "mocha": "^9.1.3",
+    "prettier": "^2.4.1",
+    "typescript": "^4.4.4"
+  },
+  "eslintConfig": {
+    "extends": [
+      "@yomguithereal/eslint-config",
+      "eslint-config-prettier"
+    ]
+  },
+  "prettier": "@yomguithereal/prettier-config"
+}
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/permutations.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/permutations.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e8ef37b668b6261dd04e2845ea7161ccb3caf38
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/permutations.d.ts
@@ -0,0 +1,4 @@
+export default function permutations<T>(
+  array: Array<T>,
+  r: number
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/permutations.js b/libs/shared/graph-layouts/node_modules/obliterator/permutations.js
new file mode 100644
index 0000000000000000000000000000000000000000..3410ca42c33dfc2a86720f913f4d8665659914dc
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/permutations.js
@@ -0,0 +1,94 @@
+/**
+ * Obliterator Permutations Function
+ * ==================================
+ *
+ * Iterator returning permutations of the given array.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Helper mapping indices to items.
+ */
+function indicesToItems(target, items, indices, r) {
+  for (var i = 0; i < r; i++) target[i] = items[indices[i]];
+}
+
+/**
+ * Permutations.
+ *
+ * @param  {array}    array - Target array.
+ * @param  {number}   r     - Size of the subsequences.
+ * @return {Iterator}
+ */
+module.exports = function permutations(array, r) {
+  if (!Array.isArray(array))
+    throw new Error(
+      'obliterator/permutations: first argument should be an array.'
+    );
+
+  var n = array.length;
+
+  if (arguments.length < 2) r = n;
+
+  if (typeof r !== 'number')
+    throw new Error(
+      'obliterator/permutations: second argument should be omitted or a number.'
+    );
+
+  if (r > n)
+    throw new Error(
+      'obliterator/permutations: the size of the subsequences should not exceed the length of the array.'
+    );
+
+  var indices = new Uint32Array(n),
+    subsequence = new Array(r),
+    cycles = new Uint32Array(r),
+    first = true,
+    i;
+
+  for (i = 0; i < n; i++) {
+    indices[i] = i;
+
+    if (i < r) cycles[i] = n - i;
+  }
+
+  i = r;
+
+  return new Iterator(function next() {
+    if (first) {
+      first = false;
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+
+    var tmp, j;
+
+    i--;
+
+    if (i < 0) return {done: true};
+
+    cycles[i]--;
+
+    if (cycles[i] === 0) {
+      tmp = indices[i];
+
+      for (j = i; j < n - 1; j++) indices[j] = indices[j + 1];
+
+      indices[n - 1] = tmp;
+
+      cycles[i] = n - i;
+      return next();
+    } else {
+      j = cycles[i];
+      tmp = indices[i];
+
+      indices[i] = indices[n - j];
+      indices[n - j] = tmp;
+
+      i = r;
+
+      indicesToItems(subsequence, array, indices, r);
+      return {value: subsequence, done: false};
+    }
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/power-set.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/power-set.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..254094ad73496279c862503e50510543b2f8def3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/power-set.d.ts
@@ -0,0 +1,3 @@
+export default function powerSet<T>(
+  array: Array<T>
+): IterableIterator<Array<T>>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/power-set.js b/libs/shared/graph-layouts/node_modules/obliterator/power-set.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd63f9574e15253d4c603e57b0d8df19d89ce39b
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/power-set.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Power Set Function
+ * ===============================
+ *
+ * Iterator returning the power set of the given array.
+ */
+var Iterator = require('./iterator.js'),
+  combinations = require('./combinations.js'),
+  chain = require('./chain.js');
+
+/**
+ * Power set.
+ *
+ * @param  {array}    array - Target array.
+ * @return {Iterator}
+ */
+module.exports = function powerSet(array) {
+  var n = array.length;
+
+  var iterators = new Array(n + 1);
+
+  iterators[0] = Iterator.of([]);
+
+  for (var i = 1; i < n + 1; i++) iterators[i] = combinations(array, i);
+
+  return chain.apply(null, iterators);
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/range.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/range.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bff37072dda3f70bfa59b6c8743bb17638d314c3
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/range.d.ts
@@ -0,0 +1,10 @@
+export default function range(end: number): IterableIterator<number>;
+export default function range(
+  start: number,
+  end: number
+): IterableIterator<number>;
+export default function range(
+  start: number,
+  end: number,
+  step: number
+): IterableIterator<number>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/range.js b/libs/shared/graph-layouts/node_modules/obliterator/range.js
new file mode 100644
index 0000000000000000000000000000000000000000..b00f9f02baa2e72bf075d86dac59ac180ee7e7f4
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/range.js
@@ -0,0 +1,44 @@
+/**
+ * Obliterator Range Function
+ * ===========================
+ *
+ * Function returning a range iterator.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Range.
+ *
+ * @param  {number} start - Start.
+ * @param  {number} end   - End.
+ * @param  {number} step  - Step.
+ * @return {Iterator}
+ */
+module.exports = function range(start, end, step) {
+  if (arguments.length === 1) {
+    end = start;
+    start = 0;
+  }
+
+  if (arguments.length < 3) step = 1;
+
+  var i = start;
+
+  var iterator = new Iterator(function () {
+    if (i < end) {
+      var value = i;
+
+      i += step;
+
+      return {value: value, done: false};
+    }
+
+    return {done: true};
+  });
+
+  iterator.start = start;
+  iterator.end = end;
+  iterator.step = step;
+
+  return iterator;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/some.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/some.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cccd35f13461d4ee8a0bc3b0f7ae9c37823da20f
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/some.d.ts
@@ -0,0 +1,8 @@
+import type {IntoInterator} from './types';
+
+type PredicateFunction<T> = (item: T) => boolean;
+
+export default function some<T>(
+  target: IntoInterator<T>,
+  predicate: PredicateFunction<T>
+): boolean;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/some.js b/libs/shared/graph-layouts/node_modules/obliterator/some.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7923ee45b938275fa9d612c2e348cd04418b430
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/some.js
@@ -0,0 +1,27 @@
+/**
+ * Obliterator Some Function
+ * ==========================
+ *
+ * Function taking an iterable and a predicate and returning whether a
+ * matching item can be found.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Some.
+ *
+ * @param  {Iterable} iterable  - Target iterable.
+ * @param  {function} predicate - Predicate function.
+ * @return {boolean}
+ */
+module.exports = function some(iterable, predicate) {
+  var iterator = iter(iterable);
+
+  var step;
+
+  while (((step = iterator.next()), !step.done)) {
+    if (predicate(step.value)) return true;
+  }
+
+  return false;
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/split.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/split.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f54145b482debf402c4cfe2a48754da3a590395
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/split.d.ts
@@ -0,0 +1,4 @@
+export default function split(
+  pattern: RegExp,
+  string: string
+): IterableIterator<string>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/split.js b/libs/shared/graph-layouts/node_modules/obliterator/split.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a765c0e9b18e97cd335a921861c1c1d3fcc1f25
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/split.js
@@ -0,0 +1,68 @@
+/**
+ * Obliterator Split Function
+ * ===========================
+ *
+ * Function returning an iterator over the pieces of a regex split.
+ */
+var Iterator = require('./iterator.js');
+
+/**
+ * Function used to make the given pattern global.
+ *
+ * @param  {RegExp} pattern - Regular expression to make global.
+ * @return {RegExp}
+ */
+function makeGlobal(pattern) {
+  var flags = 'g';
+
+  if (pattern.multiline) flags += 'm';
+  if (pattern.ignoreCase) flags += 'i';
+  if (pattern.sticky) flags += 'y';
+  if (pattern.unicode) flags += 'u';
+
+  return new RegExp(pattern.source, flags);
+}
+
+/**
+ * Split.
+ *
+ * @param  {RegExp}   pattern - Regular expression to use.
+ * @param  {string}   string  - Target string.
+ * @return {Iterator}
+ */
+module.exports = function split(pattern, string) {
+  if (!(pattern instanceof RegExp))
+    throw new Error(
+      'obliterator/split: invalid pattern. Expecting a regular expression.'
+    );
+
+  if (typeof string !== 'string')
+    throw new Error('obliterator/split: invalid target. Expecting a string.');
+
+  // NOTE: cloning the pattern has a performance cost but side effects for not
+  // doing so might be worse.
+  pattern = makeGlobal(pattern);
+
+  var consumed = false,
+    current = 0;
+
+  return new Iterator(function () {
+    if (consumed) return {done: true};
+
+    var match = pattern.exec(string),
+      value,
+      length;
+
+    if (match) {
+      length = match.index + match[0].length;
+
+      value = string.slice(current, match.index);
+      current = length;
+    } else {
+      consumed = true;
+      value = string.slice(current);
+    }
+
+    return {value: value, done: false};
+  });
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/support.js b/libs/shared/graph-layouts/node_modules/obliterator/support.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c00649aea5d906ab34fbb382fd37e7411ffc314
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/support.js
@@ -0,0 +1,2 @@
+exports.ARRAY_BUFFER_SUPPORT = typeof ArrayBuffer !== 'undefined';
+exports.SYMBOL_SUPPORT = typeof Symbol !== 'undefined';
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/take-into.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/take-into.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb1d4dbe16c98e2bf10dad362ccafa6a8a6e508a
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/take-into.d.ts
@@ -0,0 +1,9 @@
+import type {IntoInterator} from './types';
+
+// Requires a resolution of https://github.com/microsoft/TypeScript/issues/1213
+// export default function takeInto<C<~>, T>(ArrayClass: new <T>(n: number) => C<T>, iterator: Iterator<T>, n: number): C<T>;
+export default function takeInto<T>(
+  ArrayClass: new <T>(arrayLength: number) => T[],
+  iterator: IntoInterator<T>,
+  n: number
+): T[];
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/take-into.js b/libs/shared/graph-layouts/node_modules/obliterator/take-into.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc2db1875d6a959e1b262c88ee22188abe2c8b16
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/take-into.js
@@ -0,0 +1,39 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Take Into Function
+ * ===============================
+ *
+ * Same as the take function but enables the user to select an array class
+ * in which to insert the retrieved values.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Take Into.
+ *
+ * @param  {function} ArrayClass - Array class to use.
+ * @param  {Iterable} iterable   - Target iterable.
+ * @param  {number}   n          - Number of items to take.
+ * @return {array}
+ */
+module.exports = function takeInto(ArrayClass, iterable, n) {
+  var array = new ArrayClass(n),
+    step,
+    i = 0;
+
+  var iterator = iter(iterable);
+
+  while (true) {
+    if (i === n) return array;
+
+    step = iterator.next();
+
+    if (step.done) {
+      if (i !== n) return array.slice(0, i);
+
+      return array;
+    }
+
+    array[i++] = step.value;
+  }
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/take.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/take.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..346084841dd5a3cf0bea789556830f1a09985160
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/take.d.ts
@@ -0,0 +1,6 @@
+import type {IntoInterator} from './types';
+
+export default function take<T>(
+  iterator: IntoInterator<T>,
+  n: number
+): Array<T>;
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/take.js b/libs/shared/graph-layouts/node_modules/obliterator/take.js
new file mode 100644
index 0000000000000000000000000000000000000000..defe775aa642640ab5496c99d8bacecb11defa61
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/take.js
@@ -0,0 +1,39 @@
+/* eslint no-constant-condition: 0 */
+/**
+ * Obliterator Take Function
+ * ==========================
+ *
+ * Function taking n or every value of the given iterator and returns them
+ * into an array.
+ */
+var iter = require('./iter.js');
+
+/**
+ * Take.
+ *
+ * @param  {Iterable} iterable - Target iterable.
+ * @param  {number}   [n]      - Optional number of items to take.
+ * @return {array}
+ */
+module.exports = function take(iterable, n) {
+  var l = arguments.length > 1 ? n : Infinity,
+    array = l !== Infinity ? new Array(l) : [],
+    step,
+    i = 0;
+
+  var iterator = iter(iterable);
+
+  while (true) {
+    if (i === l) return array;
+
+    step = iterator.next();
+
+    if (step.done) {
+      if (i !== n) array.length = i;
+
+      return array;
+    }
+
+    array[i++] = step.value;
+  }
+};
diff --git a/libs/shared/graph-layouts/node_modules/obliterator/types.d.ts b/libs/shared/graph-layouts/node_modules/obliterator/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be9cca50e67f1bfdebce4d015837394535f8358e
--- /dev/null
+++ b/libs/shared/graph-layouts/node_modules/obliterator/types.d.ts
@@ -0,0 +1,7 @@
+export interface Sequence<T> {
+  length: number;
+  slice(from: number, to?: number): Sequence<T>;
+  [index: number]: T;
+}
+
+export type IntoInterator<T> = Iterable<T> | Iterator<T> | Sequence<T>;
diff --git a/libs/shared/graph-layouts/package.json b/libs/shared/graph-layouts/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..0833b5d4c7b18c4636761f5c156246034544e7c7
--- /dev/null
+++ b/libs/shared/graph-layouts/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "@graphpolaris/graph-layouts",
+  "version": "0.0.1",
+  "type": "commonjs",
+  "dependencies": {
+    "graphology": "^0.24.1"
+  }
+}
diff --git a/libs/shared/graph-layouts/project.json b/libs/shared/graph-layouts/project.json
new file mode 100644
index 0000000000000000000000000000000000000000..3f2eab4033dac9d777a80f17800b7d1b0d4730fd
--- /dev/null
+++ b/libs/shared/graph-layouts/project.json
@@ -0,0 +1,33 @@
+{
+  "root": "libs/shared/graph-layouts",
+  "sourceRoot": "libs/shared/graph-layouts/src",
+  "projectType": "library",
+  "targets": {
+    "build": {
+      "executor": "@nrwl/js:tsc",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/libs/shared/graph-layouts",
+        "main": "libs/shared/graph-layouts/src/index.ts",
+        "tsConfig": "libs/shared/graph-layouts/tsconfig.lib.json",
+        "assets": ["libs/shared/graph-layouts/*.md"]
+      }
+    },
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["libs/shared/graph-layouts/**/*.ts"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/libs/shared/graph-layouts"],
+      "options": {
+        "jestConfig": "libs/shared/graph-layouts/jest.config.js",
+        "passWithNoTests": true
+      }
+    }
+  },
+  "tags": []
+}
diff --git a/libs/shared/graph-layouts/src/index.ts b/libs/shared/graph-layouts/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..61989a30a087417515bdca0736d2609edaa7ae21
--- /dev/null
+++ b/libs/shared/graph-layouts/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/shared-graph-layouts';
diff --git a/libs/shared/graph-layouts/src/lib/cytoscape-layouts.ts b/libs/shared/graph-layouts/src/lib/cytoscape-layouts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..325377b8fe541507a00785962dee9fb9c3cc3ef5
--- /dev/null
+++ b/libs/shared/graph-layouts/src/lib/cytoscape-layouts.ts
@@ -0,0 +1,54 @@
+import { Layout } from './layout';
+import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase';
+
+export type CytoscapeProvider = 'Cytoscape';
+
+export type CytoscapeLayoutAlgorithms =
+  | `${CytoscapeProvider}_coupe`
+  | `${CytoscapeProvider}_i4`;
+
+/**
+ * This is a ConcreteCreator
+ */
+export class CytoscapeFactory
+  implements ILayoutFactory<CytoscapeLayoutAlgorithms>
+{
+  createLayout(LayoutAlgorithm: CytoscapeLayoutAlgorithms): Cytoscape | null {
+    switch (LayoutAlgorithm) {
+      case 'Cytoscape_coupe':
+        return new CytoscapeCoupe();
+      case 'Cytoscape_i4':
+        return new CytoscapeI4();
+      default:
+        return null;
+    }
+  }
+}
+
+export abstract class Cytoscape extends Layout<CytoscapeProvider> {
+  constructor(public override algorithm: LayoutAlgorithm<CytoscapeProvider>) {
+    super('Cytoscape', algorithm);
+  }
+
+  public specialCytoscapeFunction() {
+    console.log('Only Cytoscape Layouts can do this.');
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+class CytoscapeI4 extends Cytoscape {
+  constructor() {
+    super('Cytoscape_i4');
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+class CytoscapeCoupe extends Cytoscape {
+  constructor() {
+    super('Cytoscape_coupe');
+  }
+}
diff --git a/libs/shared/graph-layouts/src/lib/graphology-layouts.ts b/libs/shared/graph-layouts/src/lib/graphology-layouts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e609bbe54c01f15985b7f9341f40b44afd56452
--- /dev/null
+++ b/libs/shared/graph-layouts/src/lib/graphology-layouts.ts
@@ -0,0 +1,170 @@
+import Graph from 'graphology';
+import { circular, random } from 'graphology-layout';
+import forceAtlas2, {
+  ForceAtlas2Settings,
+} from 'graphology-layout-forceatlas2';
+import noverlap from 'graphology-layout-noverlap';
+import { RandomLayoutOptions } from 'graphology-layout/random';
+import { NoverlapSettings } from 'graphology-library/layout-noverlap';
+import { Attributes } from 'graphology-types';
+import { Layout } from './layout';
+import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase';
+
+export type GraphologyProvider = 'Graphology';
+
+export type GraphologyLayoutAlgorithms =
+  | `${GraphologyProvider}_circular`
+  | `${GraphologyProvider}_random`
+  | `${GraphologyProvider}_noverlap`
+  | `${GraphologyProvider}_forceAtlas2`;
+
+/**
+ * This is the Graphology Constructor for the main layouts available at
+ * https://graphology.github.io/
+ */
+export class GraphologyFactory
+  implements ILayoutFactory<GraphologyLayoutAlgorithms>
+{
+  createLayout(layoutAlgorithm: GraphologyLayoutAlgorithms): Graphology | null {
+    switch (layoutAlgorithm) {
+      case 'Graphology_random':
+        return new GraphologyRandom();
+      case 'Graphology_circular':
+        return new GraphologyCircular();
+      case 'Graphology_noverlap':
+        return new GraphologyNoverlap();
+      case 'Graphology_forceAtlas2':
+        return new GraphologyForceAtlas2();
+      default:
+        return null;
+    }
+  }
+}
+
+export abstract class Graphology extends Layout<GraphologyProvider> {
+  height: number = 100;
+  width: number = 100;
+  constructor(public override algorithm: LayoutAlgorithm<GraphologyProvider>) {
+    super('Graphology', algorithm);
+    this.setDimensions(100, 200);
+  }
+
+  public specialGraphologyFunction() {
+    // graph.forEachNode((node, attr) => {
+    //   graph.setNodeAttribute(node, 'x', 0);
+    //   graph.setNodeAttribute(node, 'y', 0);
+    // });
+  }
+
+  public setDimensions(width = 100, height = 100) {
+    this.width = width;
+    this.height = height;
+  }
+}
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyCircular extends Graphology {
+  constructor() {
+    super('Graphology_circular');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // To directly assign the positions to the nodes:
+    circular.assign(graph, {
+      scale: 100,
+    });
+  }
+}
+
+const DEFAULT_RANDOM_SETTINGS: RandomLayoutOptions = {
+  scale: 250,
+};
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyRandom extends Graphology {
+  constructor() {
+    super('Graphology_random');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // const positions = random(graph);
+
+    // To directly assign the positions to the nodes:
+    random.assign(graph, DEFAULT_RANDOM_SETTINGS);
+  }
+}
+
+const DEFAULT_NOVERLAP_SETTINGS: NoverlapSettings = {
+  margin: 40,
+  ratio: 40,
+  // gridSize: 50,
+
+  // gridSize ?number 20: number of grid cells horizontally and vertically subdivising the graph’s space. This is used as an optimization scheme. Set it to 1 and you will have O(n²) time complexity, which can sometimes perform better with very few nodes.
+  // margin ?number 5: margin to keep between nodes.
+  // expansion ?number 1.1: percentage of current space that nodes could attempt to move outside of.
+  // ratio ?number 1.0: ratio scaling node sizes.
+  // speed ?number 3: dampening factor that will slow down node movements to ease the overall process.
+};
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyNoverlap extends Graphology {
+  constructor() {
+    super('Graphology_noverlap');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // // // To directly assign the positions to the nodes:
+    noverlap.assign(graph, {
+      maxIterations: 5000,
+      settings: DEFAULT_NOVERLAP_SETTINGS,
+    });
+  }
+}
+
+const DEFAULT_FORCEATLAS2_SETTINGS: ForceAtlas2Settings = {
+  gravity: 10,
+  adjustSizes: true,
+  linLogMode: true,
+
+  // adjustSizes ?boolean false: should the node’s sizes be taken into account?
+  // barnesHutOptimize ?boolean false: whether to use the Barnes-Hut approximation to compute repulsion in O(n*log(n)) rather than default O(n^2), n being the number of nodes.
+  // barnesHutTheta ?number 0.5: Barnes-Hut approximation theta parameter.
+  // edgeWeightInfluence ?number 1: influence of the edge’s weights on the layout. To consider edge weight, don’t forget to pass weighted as true when applying the synchronous layout or when instantiating the worker.
+  // gravity ?number 1: strength of the layout’s gravity.
+  // linLogMode ?boolean false: whether to use Noack’s LinLog model.
+  // outboundAttractionDistribution ?boolean false
+  // scalingRatio ?number 1
+  // slowDown ?number 1
+  // strongGravityMode ?boolean false
+};
+
+/**
+ * This is a ConcreteProduct
+ */
+export class GraphologyForceAtlas2 extends Graphology {
+  constructor() {
+    super('Graphology_forceAtlas2');
+  }
+
+  public override layout(
+    graph: Graph<Attributes, Attributes, Attributes>
+  ): void {
+    // To directly assign the positions to the nodes:
+
+    forceAtlas2.assign(graph, {
+      iterations: 500000,
+      settings: DEFAULT_FORCEATLAS2_SETTINGS,
+    });
+  }
+}
diff --git a/libs/shared/graph-layouts/src/lib/layout-creator-usecase.spec.ts b/libs/shared/graph-layouts/src/lib/layout-creator-usecase.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..95e16ec4d055879d7e3c16a5260c06afa945f67f
--- /dev/null
+++ b/libs/shared/graph-layouts/src/lib/layout-creator-usecase.spec.ts
@@ -0,0 +1,140 @@
+import {
+  movieSchemaRaw,
+  northWindSchemaRaw,
+  simpleSchemaRaw,
+  twitterSchemaRaw,
+} from '@graphpolaris/shared/mock-data';
+// import Graph, { MultiGraph } from 'graphology';
+
+// import ladder from 'graphology-generators/classic/ladder';
+// import { LayoutFactory } from './layout-creator-usecase';
+
+// /**
+//  * @jest-environment jsdom
+//  */
+// describe('LayoutFactory', () => {
+//   /**
+//    * @jest-environment jsdom
+//    */
+//   it('should work with noverlap from graphology ', () => {
+//     const graph = new MultiGraph();
+
+//     // Adding some nodes
+//     // graph.addNode('John', { x: 0, y: 0, width: 200, height: 200 });
+//     // graph.addNode('Martha', { x: 0, y: 0 });
+//     graph.addNode('John');
+//     graph.addNode('Martha');
+
+//     // Adding an edge
+//     graph.addEdge('John', 'Martha');
+
+//     const layoutFactory = new LayoutFactory();
+//     const layoutAlgorithm = layoutFactory.createLayout(
+//       'Graphology_forceAtlas2'
+//     );
+//     layoutAlgorithm?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       console.log(node, attr);
+//     });
+//   });
+
+//   test('should work with noverlap from graphology on generated graph', () => {
+//     // Creating a ladder graph
+//     const graph = ladder(Graph, 10);
+
+//     graph.forEachNode((node, attr) => {
+//       graph.setNodeAttribute(node, 'x', 0);
+//       graph.setNodeAttribute(node, 'y', 0);
+//     });
+
+//     const layoutFactory = new LayoutFactory();
+
+//     const layout = layoutFactory.createLayout('Graphology_noverlap');
+//     layout?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       console.log(node, attr);
+//     });
+//   });
+
+//   test('should work with random from graphology on generated graph', () => {
+//     // Creating a ladder graph
+//     const graph = ladder(Graph, 10);
+
+//     graph.forEachNode((node, attr) => {
+//       graph.setNodeAttribute(node, 'x', 0);
+//       graph.setNodeAttribute(node, 'y', 0);
+//     });
+
+//     const layoutFactory = new LayoutFactory();
+
+//     const layout = layoutFactory.createLayout('Graphology_random');
+//     layout?.setDimensions(100, 100);
+//     layout?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       console.log(node, attr);
+//     });
+//   });
+
+//   test('should work with circular from graphology on generated graph', () => {
+//     // Creating a ladder graph
+//     const graph = ladder(Graph, 100);
+
+//     graph.forEachNode((node, attr) => {
+//       graph.setNodeAttribute(node, 'x', 0);
+//       graph.setNodeAttribute(node, 'y', 0);
+//     });
+
+//     const layoutFactory = new LayoutFactory();
+
+//     const layout = layoutFactory.createLayout('Graphology_circular');
+//     layout?.setDimensions(100, 100);
+//     layout?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       console.log(node, attr);
+//     });
+//   });
+
+//   test('should work with circular from graphology on generated graph', () => {
+//     // Creating a ladder graph
+//     const graph = ladder(Graph, 100);
+
+//     graph.forEachNode((node, attr) => {
+//       graph.setNodeAttribute(node, 'x', 0);
+//       graph.setNodeAttribute(node, 'y', 0);
+//     });
+
+//     const layoutFactory = new LayoutFactory();
+//     const layout = layoutFactory.createLayout('Graphology_forceAtlas2');
+//     layout?.setDimensions(100, 100);
+//     layout?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       console.log(node, attr);
+//     });
+//   });
+
+//   test('should add x,y for graphology layouts if not existing', () => {
+//     // console.log(Object.keys(AllLayoutAlgorithms))
+
+//     const graph = ladder(Graph, 100);
+
+//     graph.forEachNode((node, attr) => {
+//       expect(graph.getNodeAttribute(node, 'x')).toBeUndefined();
+//       expect(graph.getNodeAttribute(node, 'y')).toBeUndefined();
+//     });
+
+//     const layoutFactory = new LayoutFactory();
+//     const layout = layoutFactory.createLayout('Graphology_forceAtlas2');
+//     layout?.setDimensions(100, 100);
+//     layout?.layout(graph);
+
+//     graph.forEachNode((node, attr) => {
+//       expect(graph.getNodeAttribute(node, 'x')).toBeDefined();
+//       expect(graph.getNodeAttribute(node, 'y')).toBeDefined();
+//     });
+//   });
+// });
diff --git a/libs/shared/graph-layouts/src/lib/layout-creator-usecase.ts b/libs/shared/graph-layouts/src/lib/layout-creator-usecase.ts
new file mode 100644
index 0000000000000000000000000000000000000000..930200eea101f2b6c6c977a3705f5986ef2d864d
--- /dev/null
+++ b/libs/shared/graph-layouts/src/lib/layout-creator-usecase.ts
@@ -0,0 +1,77 @@
+import {
+  Cytoscape,
+  CytoscapeFactory,
+  CytoscapeLayoutAlgorithms,
+  CytoscapeProvider,
+} from './cytoscape-layouts';
+import {
+  Graphology,
+  GraphologyFactory,
+  GraphologyLayoutAlgorithms,
+  GraphologyProvider,
+} from './graphology-layouts';
+import { Layout } from './layout';
+
+export type Providers = GraphologyProvider | CytoscapeProvider;
+export type LayoutAlgorithm<Provider extends Providers> =
+  `${Provider}_${string}`;
+
+export type AllLayoutAlgorithms =
+  | GraphologyLayoutAlgorithms
+  | CytoscapeLayoutAlgorithms;
+
+export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> =
+  Algorithm extends GraphologyLayoutAlgorithms
+    ? Graphology
+    : Algorithm extends CytoscapeLayoutAlgorithms
+    ? Cytoscape
+    : Cytoscape | Graphology;
+
+export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> {
+  createLayout: (
+    Algorithm: Algorithm
+  ) => AlgorithmToLayoutProvider<Algorithm> | null;
+}
+
+/**
+ * This is our Creator
+ */
+export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> {
+  private graphologyFactory = new GraphologyFactory();
+  private cytoscapeFactory = new CytoscapeFactory();
+
+  private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>(
+    LayoutAlgorithm: AllLayoutAlgorithms,
+    startsWith: string
+  ): LayoutAlgorithm is Algorithm {
+    return LayoutAlgorithm.startsWith(startsWith);
+  }
+
+  createLayout<Algorithm extends AllLayoutAlgorithms>(
+    layoutAlgorithm: Algorithm
+  ): AlgorithmToLayoutProvider<Algorithm> | null {
+    if (
+      this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>(
+        layoutAlgorithm,
+        'Graphology'
+      )
+    ) {
+      return this.graphologyFactory.createLayout(
+        layoutAlgorithm
+      ) as AlgorithmToLayoutProvider<Algorithm>;
+    }
+
+    if (
+      this.isSpecificAlgorithm<CytoscapeLayoutAlgorithms>(
+        layoutAlgorithm,
+        'Cytoscape'
+      )
+    ) {
+      return this.cytoscapeFactory.createLayout(
+        layoutAlgorithm
+      ) as AlgorithmToLayoutProvider<Algorithm>;
+    }
+
+    return null;
+  }
+}
diff --git a/libs/shared/graph-layouts/src/lib/layout.ts b/libs/shared/graph-layouts/src/lib/layout.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06f4da35e0cc20e2a4190fe6335029e5eede0aba
--- /dev/null
+++ b/libs/shared/graph-layouts/src/lib/layout.ts
@@ -0,0 +1,21 @@
+import Graph from 'graphology';
+import { Providers, LayoutAlgorithm } from './layout-creator-usecase';
+
+/**
+ * This is our Product
+ */
+
+export abstract class Layout<provider extends Providers> {
+  constructor(
+    public provider: provider,
+    public algorithm: LayoutAlgorithm<provider>
+  ) {
+    console.log(
+      `Created the following Layout: ${provider} - ${this.algorithm}`
+    );
+  }
+
+  public layout(graph: Graph) {
+    console.log(`${this.provider} [${this.algorithm}] layouting now`);
+  }
+}
diff --git a/libs/shared/graph-layouts/tsconfig.json b/libs/shared/graph-layouts/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..47e7fa5392f983364d98061882d5716f8b484d0a
--- /dev/null
+++ b/libs/shared/graph-layouts/tsconfig.json
@@ -0,0 +1,23 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "CommonJS",
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "composite": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/libs/shared/graph-layouts/tsconfig.lib.json b/libs/shared/graph-layouts/tsconfig.lib.json
new file mode 100644
index 0000000000000000000000000000000000000000..efdd77fbf5b34f06e8efa8ad8bc87e11a3c1e9af
--- /dev/null
+++ b/libs/shared/graph-layouts/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "declaration": true,
+    "types": []
+  },
+  "include": ["**/*.ts"],
+  "exclude": ["**/*.spec.ts"]
+}
diff --git a/libs/shared/graph-layouts/tsconfig.spec.json b/libs/shared/graph-layouts/tsconfig.spec.json
new file mode 100644
index 0000000000000000000000000000000000000000..cdccf0f190c758616ec64312b01b3f9b35f49d44
--- /dev/null
+++ b/libs/shared/graph-layouts/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"],
+    "composite": true
+  },
+  "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/libs/shared/graph-layouts/yarn.lock b/libs/shared/graph-layouts/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..8992e29d763b5d5cab935263986c6dce4d5770ae
--- /dev/null
+++ b/libs/shared/graph-layouts/yarn.lock
@@ -0,0 +1,21 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+events@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+graphology@^0.24.1:
+  version "0.24.1"
+  resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.24.1.tgz#035e452e294b01168cf5c85d5dd0a4b7e4837d87"
+  integrity sha512-6lNz1PNTAe9Q6ioHKrXu0Lp047sgvOoHa4qmP/8mnJWCGv2iIZPQkuHPUb2/OWDWCqHpw2hKgJLJ55X/66xmHg==
+  dependencies:
+    events "^3.3.0"
+    obliterator "^2.0.2"
+
+obliterator@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21"
+  integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg==
diff --git a/libs/shared/mock-data/src/index.ts b/libs/shared/mock-data/src/index.ts
index 56efd8b1107de967cc6a20114df6dcea57ac2e93..ab66e18ef08fcda54d1db1bfb65b8119697452dc 100644
--- a/libs/shared/mock-data/src/index.ts
+++ b/libs/shared/mock-data/src/index.ts
@@ -1,4 +1,4 @@
-export * from './schema/simple';
-export * from './schema/moviesSchema';
-export * from './schema/northwindSchema';
-export * from './schema/twitterSchema';
+export * from './schema/simpleRaw';
+export * from './schema/moviesSchemaRaw';
+export * from './schema/northwindSchemaRaw';
+export * from './schema/twitterSchemaRaw';
diff --git a/libs/shared/mock-data/src/schema/mock-data.spec.ts b/libs/shared/mock-data/src/schema/mock-data.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f86e6d316a9867914bc99061442e2b3f8f56e854
--- /dev/null
+++ b/libs/shared/mock-data/src/schema/mock-data.spec.ts
@@ -0,0 +1,55 @@
+import Graph from 'graphology';
+import { movieSchemaRaw, movieSchema } from './moviesSchemaRaw';
+import { northWindSchema, northwindSchemaRaw } from './northwindSchemaRaw';
+import { simpleSchema, simpleSchemaRaw } from './simpleRaw';
+import { twitterSchema, twitterSchemaRaw } from './twitterSchemaRaw';
+
+describe('MockData Tests', () => {
+  it('should have raw data available movie', () => {
+    const graph = movieSchemaRaw;
+    expect(graph);
+  });
+
+  it('should have raw data available northwind', () => {
+    const graph = northwindSchemaRaw;
+    expect(graph);
+  });
+
+  it('should have raw data available simpleSchemaRaw', () => {
+    const graph = simpleSchemaRaw;
+    expect(graph);
+  });
+
+  it('should have raw data available twitterSchemaRaw', () => {
+    const graph = twitterSchemaRaw;
+    expect(graph);
+  });
+
+  it('should have data available as graphology model movie', () => {
+    const graph = movieSchema;
+    expect(graph);
+
+    expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy();
+  });
+
+  it('should have data available as graphology model northwind', () => {
+    const graph = northWindSchema;
+    expect(graph);
+
+    expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy();
+  });
+
+  it('should have data available as graphology model simpleSchemaRaw', () => {
+    const graph = simpleSchema;
+    expect(graph);
+
+    expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy();
+  });
+
+  it('should have data available as graphology model twitterSchemaRaw', () => {
+    const graph = twitterSchema;
+    expect(graph);
+
+    expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy();
+  });
+});
diff --git a/libs/shared/mock-data/src/schema/moviesSchema.ts b/libs/shared/mock-data/src/schema/moviesSchema.ts
deleted file mode 100644
index 67bdc012962a1c69de55d8b3bd90b8866e5de90d..0000000000000000000000000000000000000000
--- a/libs/shared/mock-data/src/schema/moviesSchema.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-export const movieSchema = {
-	"nodes": [
-		{
-			"name": "Movie",
-			"attributes": [
-				{
-					"name": "tagline",
-					"type": "string"
-				},
-				{
-					"name": "title",
-					"type": "string"
-				},
-				{
-					"name": "released",
-					"type": "int"
-				},
-				{
-					"name": "votes",
-					"type": "int"
-				}
-			]
-		},
-		{
-			"name": "Person",
-			"attributes": [
-				{
-					"name": "born",
-					"type": "int"
-				},
-				{
-					"name": "name",
-					"type": "string"
-				}
-			]
-		}
-	],
-	"edges": [
-		{
-			"name": "ACTED_IN",
-			"collection": "ACTED_IN",
-			"from": "Person",
-			"to": "Movie",
-			"attributes": [
-				{
-					"name": "roles",
-					"type": "string"
-				}
-			]
-		},
-		{
-			"name": "REVIEWED",
-			"collection": "REVIEWED",
-			"from": "Person",
-			"to": "Movie",
-			"attributes": [
-				{
-					"name": "summary",
-					"type": "string"
-				},
-				{
-					"name": "rating",
-					"type": "int"
-				}
-			]
-		},
-		{
-			"name": "PRODUCED",
-			"collection": "PRODUCED",
-			"from": "Person",
-			"to": "Movie",
-			"attributes": []
-		},
-		{
-			"name": "WROTE",
-			"collection": "WROTE",
-			"from": "Person",
-			"to": "Movie",
-			"attributes": []
-		},
-		{
-			"name": "FOLLOWS",
-			"collection": "FOLLOWS",
-			"from": "Person",
-			"to": "Person",
-			"attributes": []
-		},
-		{
-			"name": "DIRECTED",
-			"collection": "DIRECTED",
-			"from": "Person",
-			"to": "Movie",
-			"attributes": []
-		}
-	]
-}
\ No newline at end of file
diff --git a/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts b/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts
new file mode 100644
index 0000000000000000000000000000000000000000..762bce33c65af64ba2bdac7928697224afd5d651
--- /dev/null
+++ b/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts
@@ -0,0 +1,103 @@
+import { SchemaUtils } from '@graphpolaris/schema-utils';
+import { SchemaFromBackend } from 'libs/shared/models/src';
+
+export const movieSchemaRaw = {
+  nodes: [
+    {
+      name: 'Movie',
+      attributes: [
+        {
+          name: 'tagline',
+          type: 'string',
+        },
+        {
+          name: 'title',
+          type: 'string',
+        },
+        {
+          name: 'released',
+          type: 'int',
+        },
+        {
+          name: 'votes',
+          type: 'int',
+        },
+      ],
+    },
+    {
+      name: 'Person',
+      attributes: [
+        {
+          name: 'born',
+          type: 'int',
+        },
+        {
+          name: 'name',
+          type: 'string',
+        },
+      ],
+    },
+  ],
+  edges: [
+    {
+      name: 'ACTED_IN',
+      collection: 'ACTED_IN',
+      from: 'Person',
+      to: 'Movie',
+      attributes: [
+        {
+          name: 'roles',
+          type: 'string',
+        },
+      ],
+    },
+    {
+      name: 'REVIEWED',
+      collection: 'REVIEWED',
+      from: 'Person',
+      to: 'Movie',
+      attributes: [
+        {
+          name: 'summary',
+          type: 'string',
+        },
+        {
+          name: 'rating',
+          type: 'int',
+        },
+      ],
+    },
+    {
+      name: 'PRODUCED',
+      collection: 'PRODUCED',
+      from: 'Person',
+      to: 'Movie',
+      attributes: [],
+    },
+    {
+      name: 'WROTE',
+      collection: 'WROTE',
+      from: 'Person',
+      to: 'Movie',
+      attributes: [],
+    },
+    {
+      name: 'FOLLOWS',
+      collection: 'FOLLOWS',
+      from: 'Person',
+      to: 'Person',
+      attributes: [],
+    },
+    {
+      name: 'DIRECTED',
+      collection: 'DIRECTED',
+      from: 'Person',
+      to: 'Movie',
+      attributes: [],
+    },
+  ],
+};
+
+export const movieSchema = SchemaUtils.ParseSchemaFromBackend(
+  movieSchemaRaw as SchemaFromBackend
+);
diff --git a/libs/shared/mock-data/src/schema/northwindSchema.ts b/libs/shared/mock-data/src/schema/northwindSchema.ts
deleted file mode 100644
index 8cf2e965595dcfdc1374b3e573c124812468a31c..0000000000000000000000000000000000000000
--- a/libs/shared/mock-data/src/schema/northwindSchema.ts
+++ /dev/null
@@ -1,284 +0,0 @@
-export const northWindSchema = {
-	"nodes": [
-		{
-			"name": "Order",
-			"attributes": [
-				{
-					"name": "customerID",
-					"type": "string"
-				},
-				{
-					"name": "shipCity",
-					"type": "string"
-				},
-				{
-					"name": "orderID",
-					"type": "string"
-				},
-				{
-					"name": "freight",
-					"type": "string"
-				},
-				{
-					"name": "requiredDate",
-					"type": "string"
-				},
-				{
-					"name": "employeeID",
-					"type": "string"
-				},
-				{
-					"name": "shipName",
-					"type": "string"
-				},
-				{
-					"name": "shipPostalCode",
-					"type": "string"
-				},
-				{
-					"name": "orderDate",
-					"type": "string"
-				},
-				{
-					"name": "shipRegion",
-					"type": "string"
-				},
-				{
-					"name": "shipCountry",
-					"type": "string"
-				},
-				{
-					"name": "shippedDate",
-					"type": "string"
-				},
-				{
-					"name": "shipVia",
-					"type": "string"
-				},
-				{
-					"name": "shipAddress",
-					"type": "string"
-				}
-			]
-		},
-		{
-			"name": "Category",
-			"attributes": [
-				{
-					"name": "categoryID",
-					"type": "string"
-				},
-				{
-					"name": "description",
-					"type": "string"
-				},
-				{
-					"name": "categoryName",
-					"type": "string"
-				},
-				{
-					"name": "picture",
-					"type": "string"
-				}
-			]
-		},
-		{
-			"name": "Customer",
-			"attributes": [
-				{
-					"name": "country",
-					"type": "string"
-				},
-				{
-					"name": "address",
-					"type": "string"
-				},
-				{
-					"name": "contactTitle",
-					"type": "string"
-				},
-				{
-					"name": "city",
-					"type": "string"
-				},
-				{
-					"name": "phone",
-					"type": "string"
-				},
-				{
-					"name": "contactName",
-					"type": "string"
-				},
-				{
-					"name": "postalCode",
-					"type": "string"
-				},
-				{
-					"name": "companyName",
-					"type": "string"
-				},
-				{
-					"name": "fax",
-					"type": "string"
-				},
-				{
-					"name": "region",
-					"type": "string"
-				},
-				{
-					"name": "customerID",
-					"type": "string"
-				}
-			]
-		},
-		{
-			"name": "Product",
-			"attributes": [
-				{
-					"name": "reorderLevel",
-					"type": "int"
-				},
-				{
-					"name": "unitsInStock",
-					"type": "int"
-				},
-				{
-					"name": "unitPrice",
-					"type": "float"
-				},
-				{
-					"name": "supplierID",
-					"type": "string"
-				},
-				{
-					"name": "productID",
-					"type": "string"
-				},
-				{
-					"name": "discontinued",
-					"type": "bool"
-				},
-				{
-					"name": "quantityPerUnit",
-					"type": "string"
-				},
-				{
-					"name": "categoryID",
-					"type": "string"
-				},
-				{
-					"name": "unitsOnOrder",
-					"type": "int"
-				},
-				{
-					"name": "productName",
-					"type": "string"
-				}
-			]
-		},
-		{
-			"name": "Supplier",
-			"attributes": [
-				{
-					"name": "supplierID",
-					"type": "string"
-				},
-				{
-					"name": "country",
-					"type": "string"
-				},
-				{
-					"name": "address",
-					"type": "string"
-				},
-				{
-					"name": "contactTitle",
-					"type": "string"
-				},
-				{
-					"name": "city",
-					"type": "string"
-				},
-				{
-					"name": "phone",
-					"type": "string"
-				},
-				{
-					"name": "contactName",
-					"type": "string"
-				},
-				{
-					"name": "postalCode",
-					"type": "string"
-				},
-				{
-					"name": "companyName",
-					"type": "string"
-				},
-				{
-					"name": "fax",
-					"type": "string"
-				},
-				{
-					"name": "region",
-					"type": "string"
-				},
-				{
-					"name": "homePage",
-					"type": "string"
-				}
-			]
-		}
-	],
-	"edges": [
-		{
-			"name": "ORDERS",
-			"collection": "ORDERS",
-			"from": "Order",
-			"to": "Product",
-			"attributes": [
-				{
-					"name": "unitPrice",
-					"type": "string"
-				},
-				{
-					"name": "productID",
-					"type": "string"
-				},
-				{
-					"name": "orderID",
-					"type": "string"
-				},
-				{
-					"name": "discount",
-					"type": "string"
-				},
-				{
-					"name": "quantity",
-					"type": "int"
-				}
-			]
-		},
-		{
-			"name": "PART_OF",
-			"collection": "PART_OF",
-			"from": "Product",
-			"to": "Category",
-			"attributes": []
-		},
-		{
-			"name": "SUPPLIES",
-			"collection": "SUPPLIES",
-			"from": "Supplier",
-			"to": "Product",
-			"attributes": []
-		},
-		{
-			"name": "PURCHASED",
-			"collection": "PURCHASED",
-			"from": "Customer",
-			"to": "Order",
-			"attributes": []
-		}
-	]
-}
\ No newline at end of file
diff --git a/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts b/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts
new file mode 100644
index 0000000000000000000000000000000000000000..59f248f5fbd9117a1021cc1852100e24c02f85ec
--- /dev/null
+++ b/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts
@@ -0,0 +1,291 @@
+import { SchemaUtils } from '@graphpolaris/schema-utils';
+import { SchemaFromBackend } from 'libs/shared/models/src';
+
+export const northwindSchemaRaw = {
+  nodes: [
+    {
+      name: 'Order',
+      attributes: [
+        {
+          name: 'customerID',
+          type: 'string',
+        },
+        {
+          name: 'shipCity',
+          type: 'string',
+        },
+        {
+          name: 'orderID',
+          type: 'string',
+        },
+        {
+          name: 'freight',
+          type: 'string',
+        },
+        {
+          name: 'requiredDate',
+          type: 'string',
+        },
+        {
+          name: 'employeeID',
+          type: 'string',
+        },
+        {
+          name: 'shipName',
+          type: 'string',
+        },
+        {
+          name: 'shipPostalCode',
+          type: 'string',
+        },
+        {
+          name: 'orderDate',
+          type: 'string',
+        },
+        {
+          name: 'shipRegion',
+          type: 'string',
+        },
+        {
+          name: 'shipCountry',
+          type: 'string',
+        },
+        {
+          name: 'shippedDate',
+          type: 'string',
+        },
+        {
+          name: 'shipVia',
+          type: 'string',
+        },
+        {
+          name: 'shipAddress',
+          type: 'string',
+        },
+      ],
+    },
+    {
+      name: 'Category',
+      attributes: [
+        {
+          name: 'categoryID',
+          type: 'string',
+        },
+        {
+          name: 'description',
+          type: 'string',
+        },
+        {
+          name: 'categoryName',
+          type: 'string',
+        },
+        {
+          name: 'picture',
+          type: 'string',
+        },
+      ],
+    },
+    {
+      name: 'Customer',
+      attributes: [
+        {
+          name: 'country',
+          type: 'string',
+        },
+        {
+          name: 'address',
+          type: 'string',
+        },
+        {
+          name: 'contactTitle',
+          type: 'string',
+        },
+        {
+          name: 'city',
+          type: 'string',
+        },
+        {
+          name: 'phone',
+          type: 'string',
+        },
+        {
+          name: 'contactName',
+          type: 'string',
+        },
+        {
+          name: 'postalCode',
+          type: 'string',
+        },
+        {
+          name: 'companyName',
+          type: 'string',
+        },
+        {
+          name: 'fax',
+          type: 'string',
+        },
+        {
+          name: 'region',
+          type: 'string',
+        },
+        {
+          name: 'customerID',
+          type: 'string',
+        },
+      ],
+    },
+    {
+      name: 'Product',
+      attributes: [
+        {
+          name: 'reorderLevel',
+          type: 'int',
+        },
+        {
+          name: 'unitsInStock',
+          type: 'int',
+        },
+        {
+          name: 'unitPrice',
+          type: 'float',
+        },
+        {
+          name: 'supplierID',
+          type: 'string',
+        },
+        {
+          name: 'productID',
+          type: 'string',
+        },
+        {
+          name: 'discontinued',
+          type: 'bool',
+        },
+        {
+          name: 'quantityPerUnit',
+          type: 'string',
+        },
+        {
+          name: 'categoryID',
+          type: 'string',
+        },
+        {
+          name: 'unitsOnOrder',
+          type: 'int',
+        },
+        {
+          name: 'productName',
+          type: 'string',
+        },
+      ],
+    },
+    {
+      name: 'Supplier',
+      attributes: [
+        {
+          name: 'supplierID',
+          type: 'string',
+        },
+        {
+          name: 'country',
+          type: 'string',
+        },
+        {
+          name: 'address',
+          type: 'string',
+        },
+        {
+          name: 'contactTitle',
+          type: 'string',
+        },
+        {
+          name: 'city',
+          type: 'string',
+        },
+        {
+          name: 'phone',
+          type: 'string',
+        },
+        {
+          name: 'contactName',
+          type: 'string',
+        },
+        {
+          name: 'postalCode',
+          type: 'string',
+        },
+        {
+          name: 'companyName',
+          type: 'string',
+        },
+        {
+          name: 'fax',
+          type: 'string',
+        },
+        {
+          name: 'region',
+          type: 'string',
+        },
+        {
+          name: 'homePage',
+          type: 'string',
+        },
+      ],
+    },
+  ],
+  edges: [
+    {
+      name: 'ORDERS',
+      collection: 'ORDERS',
+      from: 'Order',
+      to: 'Product',
+      attributes: [
+        {
+          name: 'unitPrice',
+          type: 'string',
+        },
+        {
+          name: 'productID',
+          type: 'string',
+        },
+        {
+          name: 'orderID',
+          type: 'string',
+        },
+        {
+          name: 'discount',
+          type: 'string',
+        },
+        {
+          name: 'quantity',
+          type: 'int',
+        },
+      ],
+    },
+    {
+      name: 'PART_OF',
+      collection: 'PART_OF',
+      from: 'Product',
+      to: 'Category',
+      attributes: [],
+    },
+    {
+      name: 'SUPPLIES',
+      collection: 'SUPPLIES',
+      from: 'Supplier',
+      to: 'Product',
+      attributes: [],
+    },
+    {
+      name: 'PURCHASED',
+      collection: 'PURCHASED',
+      from: 'Customer',
+      to: 'Order',
+      attributes: [],
+    },
+  ],
+};
+
+export const northWindSchema = SchemaUtils.ParseSchemaFromBackend(
+  northwindSchemaRaw as SchemaFromBackend
+);
diff --git a/libs/shared/mock-data/src/schema/simple.ts b/libs/shared/mock-data/src/schema/simple.ts
deleted file mode 100644
index 13c5a91e08a4f6341c5be1255f1428d8e69c677e..0000000000000000000000000000000000000000
--- a/libs/shared/mock-data/src/schema/simple.ts
+++ /dev/null
@@ -1,101 +0,0 @@
- export const simpleSchema = {
-  "nodes": [
-    {
-      "name": "Thijs",
-      "attributes": []
-    },
-    {
-      "name": "Airport",
-      "attributes": [
-        { "name": "city", "type": "string" },
-        { "name": "vip", "type": "bool" },
-        { "name": "state", "type": "string" }
-      ]
-    },
-    {
-      "name": "Airport2",
-      "attributes": [
-        { "name": "city", "type": "string" },
-        { "name": "vip", "type": "bool" },
-        { "name": "state", "type": "string" }
-      ]
-    },
-    {
-      "name": "Plane",
-      "attributes": [
-        { "name": "type", "type": "string" },
-        { "name": "maxFuelCapacity", "type": "int" }
-      ]
-    },
-    { "name": "Staff", "attributes": [] }
-  ],
-  "edges": [
-    {
-      "name": "Airport2:Airport",
-      "from": "Airport2",
-      "to": "Airport",
-      "collection": "flights",
-      "attributes": [
-        { "name": "arrivalTime", "type": "int" },
-        { "name": "departureTime", "type": "int" }
-      ]
-    },
-    {
-      "name": "Airport:Staff",
-      "from": "Airport",
-      "to": "Staff",
-      "collection": "flights",
-      "attributes": [{ "name": "salary", "type": "int" }]
-    },
-    {
-      "name": "Plane:Airport",
-      "from": "Plane",
-      "to": "Airport",
-      "collection": "flights",
-      "attributes": []
-    },
-    {
-      "name": "Airport:Thijs",
-      "from": "Airport",
-      "to": "Thijs",
-      "collection": "flights",
-      "attributes": [{ "name": "hallo", "type": "string" }]
-    },
-    {
-      "name": "Thijs:Airport",
-      "from": "Thijs",
-      "to": "Airport",
-      "collection": "flights",
-      "attributes": [{ "name": "hallo", "type": "string" }]
-    },
-    {
-      "name": "Staff:Plane",
-      "from": "Staff",
-      "to": "Plane",
-      "collection": "flights",
-      "attributes": [{ "name": "hallo", "type": "string" }]
-    },
-    {
-      "name": "Staff:Airport2",
-      "from": "Staff",
-      "to": "Airport2",
-      "collection": "flights",
-      "attributes": [{ "name": "hallo", "type": "string" }]
-    },
-    {
-      "name": "Airport2:Plane",
-      "from": "Airport2",
-      "to": "Plane",
-      "collection": "flights",
-      "attributes": [{ "name": "hallo", "type": "string" }]
-    },
-
-    {
-      "name": "Airport:Airport",
-      "from": "Airport",
-      "to": "Airport",
-      "collection": "flights",
-      "attributes": [{ "name": "test", "type": "string" }]
-    }
-  ]
-}
diff --git a/libs/shared/mock-data/src/schema/simpleRaw.ts b/libs/shared/mock-data/src/schema/simpleRaw.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e8fee0ca1eea766231f802433ee7f52ecd7b273d
--- /dev/null
+++ b/libs/shared/mock-data/src/schema/simpleRaw.ts
@@ -0,0 +1,108 @@
+import { SchemaUtils } from '@graphpolaris/schema-utils';
+import { SchemaFromBackend } from '@graphpolaris/models';
+
+export const simpleSchemaRaw = {
+  nodes: [
+    {
+      name: 'Thijs',
+      attributes: [],
+    },
+    {
+      name: 'Airport',
+      attributes: [
+        { name: 'city', type: 'string' },
+        { name: 'vip', type: 'bool' },
+        { name: 'state', type: 'string' },
+      ],
+    },
+    {
+      name: 'Airport2',
+      attributes: [
+        { name: 'city', type: 'string' },
+        { name: 'vip', type: 'bool' },
+        { name: 'state', type: 'string' },
+      ],
+    },
+    {
+      name: 'Plane',
+      attributes: [
+        { name: 'type', type: 'string' },
+        { name: 'maxFuelCapacity', type: 'int' },
+      ],
+    },
+    { name: 'Staff', attributes: [] },
+  ],
+  edges: [
+    {
+      name: 'Airport2:Airport',
+      from: 'Airport2',
+      to: 'Airport',
+      collection: 'flights',
+      attributes: [
+        { name: 'arrivalTime', type: 'int' },
+        { name: 'departureTime', type: 'int' },
+      ],
+    },
+    {
+      name: 'Airport:Staff',
+      from: 'Airport',
+      to: 'Staff',
+      collection: 'flights',
+      attributes: [{ name: 'salary', type: 'int' }],
+    },
+    {
+      name: 'Plane:Airport',
+      from: 'Plane',
+      to: 'Airport',
+      collection: 'flights',
+      attributes: [],
+    },
+    {
+      name: 'Airport:Thijs',
+      from: 'Airport',
+      to: 'Thijs',
+      collection: 'flights',
+      attributes: [{ name: 'hallo', type: 'string' }],
+    },
+    {
+      name: 'Thijs:Airport',
+      from: 'Thijs',
+      to: 'Airport',
+      collection: 'flights',
+      attributes: [{ name: 'hallo', type: 'string' }],
+    },
+    {
+      name: 'Staff:Plane',
+      from: 'Staff',
+      to: 'Plane',
+      collection: 'flights',
+      attributes: [{ name: 'hallo', type: 'string' }],
+    },
+    {
+      name: 'Staff:Airport2',
+      from: 'Staff',
+      to: 'Airport2',
+      collection: 'flights',
+      attributes: [{ name: 'hallo', type: 'string' }],
+    },
+    {
+      name: 'Airport2:Plane',
+      from: 'Airport2',
+      to: 'Plane',
+      collection: 'flights',
+      attributes: [{ name: 'hallo', type: 'string' }],
+    },
+
+    {
+      name: 'Airport:Airport',
+      from: 'Airport',
+      to: 'Airport',
+      collection: 'flights',
+      attributes: [{ name: 'test', type: 'string' }],
+    },
+  ],
+};
+
+export const simpleSchema = SchemaUtils.ParseSchemaFromBackend(
+  simpleSchemaRaw as SchemaFromBackend
+);
diff --git a/libs/shared/mock-data/src/schema/twitterSchema.ts b/libs/shared/mock-data/src/schema/twitterSchemaRaw.ts
similarity index 95%
rename from libs/shared/mock-data/src/schema/twitterSchema.ts
rename to libs/shared/mock-data/src/schema/twitterSchemaRaw.ts
index 98a73b9ab4ee84b24275a65c647d26e15612e7cb..ba922222c3fa2d9600f1318649ae59c7d0033f90 100644
--- a/libs/shared/mock-data/src/schema/twitterSchema.ts
+++ b/libs/shared/mock-data/src/schema/twitterSchemaRaw.ts
@@ -1,4 +1,7 @@
-export const twitterSchema = {
+import { SchemaUtils } from '@graphpolaris/schema-utils';
+import { SchemaFromBackend } from 'libs/shared/models/src';
+
+export const twitterSchemaRaw = {
   nodes: [
     {
       name: 'Me',
@@ -311,3 +314,7 @@ export const twitterSchema = {
     },
   ],
 };
+
+export const twitterSchema = SchemaUtils.ParseSchemaFromBackend(
+  twitterSchemaRaw as SchemaFromBackend
+);
diff --git a/libs/shared/mock-data/tsconfig.spec.json b/libs/shared/mock-data/tsconfig.spec.json
index d8716fecfa3b7929f162b71e7a966c579a63c071..4afc999ad429eea63e777308527c4e8f629e4198 100644
--- a/libs/shared/mock-data/tsconfig.spec.json
+++ b/libs/shared/mock-data/tsconfig.spec.json
@@ -3,7 +3,8 @@
   "compilerOptions": {
     "outDir": "../../../dist/out-tsc",
     "module": "commonjs",
-    "types": ["jest", "node"]
+    "types": ["jest", "node"],
+    "composite": true
   },
   "include": [
     "**/*.test.ts",
diff --git a/libs/shared/models/.babelrc b/libs/shared/models/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe
--- /dev/null
+++ b/libs/shared/models/.babelrc
@@ -0,0 +1,12 @@
+{
+  "presets": [
+    [
+      "@nrwl/react/babel",
+      {
+        "runtime": "automatic",
+        "useBuiltIns": "usage"
+      }
+    ]
+  ],
+  "plugins": []
+}
diff --git a/libs/shared/models/.eslintrc.json b/libs/shared/models/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2
--- /dev/null
+++ b/libs/shared/models/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/libs/shared/models/README.md b/libs/shared/models/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ec693074dd1c5faa437707045ccc5396f8cf6544
--- /dev/null
+++ b/libs/shared/models/README.md
@@ -0,0 +1,7 @@
+# shared-models
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-models` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/models/jest.config.js b/libs/shared/models/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..67188b1bbb706872be6261eb30de2be67e54167d
--- /dev/null
+++ b/libs/shared/models/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  displayName: 'shared-models',
+  preset: '../../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]sx?$': 'babel-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../../coverage/libs/shared/models',
+};
diff --git a/libs/shared/models/package.json b/libs/shared/models/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..19f478f8b79a11a627b55297a3d2cd190048977f
--- /dev/null
+++ b/libs/shared/models/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "@graphpolaris/models",
+  "version": "0.0.1"
+}
diff --git a/libs/shared/models/project.json b/libs/shared/models/project.json
new file mode 100644
index 0000000000000000000000000000000000000000..16ea9e35534ac1536ac3ddc08b0ddfde33691e94
--- /dev/null
+++ b/libs/shared/models/project.json
@@ -0,0 +1,43 @@
+{
+  "root": "libs/shared/models",
+  "sourceRoot": "libs/shared/models/src",
+  "projectType": "library",
+  "tags": [],
+  "targets": {
+    "build": {
+      "executor": "@nrwl/web:rollup",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/libs/shared/models",
+        "tsConfig": "libs/shared/models/tsconfig.lib.json",
+        "project": "libs/shared/models/package.json",
+        "entryFile": "libs/shared/models/src/index.ts",
+        "external": ["react/jsx-runtime"],
+        "rollupConfig": "@nrwl/react/plugins/bundle-rollup",
+        "compiler": "babel",
+        "assets": [
+          {
+            "glob": "libs/shared/models/README.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["libs/shared/models/**/*.{ts,tsx,js,jsx}"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/libs/shared/models"],
+      "options": {
+        "jestConfig": "libs/shared/models/jest.config.js",
+        "passWithNoTests": true
+      }
+    }
+  }
+}
diff --git a/libs/shared/models/src/index.ts b/libs/shared/models/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..11a865736e6ed5d85963d39e8b33bc7803c1df0f
--- /dev/null
+++ b/libs/shared/models/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/shared-models';
diff --git a/libs/shared/models/src/lib/shared-models.spec.ts b/libs/shared/models/src/lib/shared-models.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08a29a089432208f807f885f6069ae25f8b6201e
--- /dev/null
+++ b/libs/shared/models/src/lib/shared-models.spec.ts
@@ -0,0 +1,7 @@
+
+
+describe('SharedModels', () => {
+  it('should render successfully', () => {
+    expect(true).toBeTruthy();
+  });
+});
diff --git a/libs/shared/models/src/lib/shared-models.ts b/libs/shared/models/src/lib/shared-models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b54d87d4913348e49d02d2531e0eb018b9397b16
--- /dev/null
+++ b/libs/shared/models/src/lib/shared-models.ts
@@ -0,0 +1,27 @@
+/*************** schema format from the backend *************** */
+/** Schema type, consist of nodes and edges */
+export type SchemaFromBackend = {
+  edges: Edge[];
+  nodes: Node[];
+};
+
+/** Attribute type, consist of a name */
+export type Attribute = {
+  name: string;
+  type: 'string' | 'int' | 'bool' | 'float';
+};
+
+/** Node type, consist of a name and a list of attributes */
+export type Node = {
+  name: string;
+  attributes: Attribute[];
+};
+
+/** Edge type, consist of a name, start point, end point and a list of attributes */
+export type Edge = {
+  name: string;
+  to: string;
+  from: string;
+  collection: string;
+  attributes: Attribute[];
+};
\ No newline at end of file
diff --git a/libs/shared/models/tsconfig.json b/libs/shared/models/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..4b421814593c06b901b07a205f079cc08998a1e0
--- /dev/null
+++ b/libs/shared/models/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/libs/shared/models/tsconfig.lib.json b/libs/shared/models/tsconfig.lib.json
new file mode 100644
index 0000000000000000000000000000000000000000..36549a100ee959618b742d13c61bd6b40af0f53a
--- /dev/null
+++ b/libs/shared/models/tsconfig.lib.json
@@ -0,0 +1,24 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "declaration": true,
+    "types": [],
+    "composite": true
+  },
+  "files": [
+    "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+    "../../../node_modules/@nrwl/react/typings/image.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+    "**/*.spec.tsx",
+    "**/*.test.tsx",
+    "**/*.spec.js",
+    "**/*.test.js",
+    "**/*.spec.jsx",
+    "**/*.test.jsx"
+  ],
+  "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/shared/models/tsconfig.spec.json b/libs/shared/models/tsconfig.spec.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071
--- /dev/null
+++ b/libs/shared/models/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"]
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/libs/shared/schema-utils/.babelrc b/libs/shared/schema-utils/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe
--- /dev/null
+++ b/libs/shared/schema-utils/.babelrc
@@ -0,0 +1,12 @@
+{
+  "presets": [
+    [
+      "@nrwl/react/babel",
+      {
+        "runtime": "automatic",
+        "useBuiltIns": "usage"
+      }
+    ]
+  ],
+  "plugins": []
+}
diff --git a/libs/shared/schema-utils/.eslintrc.json b/libs/shared/schema-utils/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2
--- /dev/null
+++ b/libs/shared/schema-utils/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/libs/shared/schema-utils/README.md b/libs/shared/schema-utils/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..99b8629120ccc1f0dafb2ac461ff40de9d45f654
--- /dev/null
+++ b/libs/shared/schema-utils/README.md
@@ -0,0 +1,7 @@
+# shared-schema-utils
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-schema-utils` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/schema-utils/jest.config.js b/libs/shared/schema-utils/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..e351d1abc92c54e097dfd6e58aa1610a9f33c846
--- /dev/null
+++ b/libs/shared/schema-utils/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  displayName: 'shared-schema-utils',
+  preset: '../../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]sx?$': 'babel-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../../coverage/libs/shared/schema-utils',
+};
diff --git a/libs/shared/schema-utils/package.json b/libs/shared/schema-utils/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..72dadd855d67b38ba862216d0180ffe6626ceabc
--- /dev/null
+++ b/libs/shared/schema-utils/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "@graphpolaris/schema-utils",
+  "version": "0.0.1"
+}
diff --git a/libs/shared/schema-utils/project.json b/libs/shared/schema-utils/project.json
new file mode 100644
index 0000000000000000000000000000000000000000..2414f349be5dbc92606704d842ba58a33e9240c2
--- /dev/null
+++ b/libs/shared/schema-utils/project.json
@@ -0,0 +1,43 @@
+{
+  "root": "libs/shared/schema-utils",
+  "sourceRoot": "libs/shared/schema-utils/src",
+  "projectType": "library",
+  "tags": [],
+  "targets": {
+    "build": {
+      "executor": "@nrwl/web:rollup",
+      "outputs": ["{options.outputPath}"],
+      "options": {
+        "outputPath": "dist/libs/shared/schema-utils",
+        "tsConfig": "libs/shared/schema-utils/tsconfig.lib.json",
+        "project": "libs/shared/schema-utils/package.json",
+        "entryFile": "libs/shared/schema-utils/src/index.ts",
+        "external": ["react/jsx-runtime"],
+        "rollupConfig": "@nrwl/react/plugins/bundle-rollup",
+        "compiler": "babel",
+        "assets": [
+          {
+            "glob": "libs/shared/schema-utils/README.md",
+            "input": ".",
+            "output": "."
+          }
+        ]
+      }
+    },
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["libs/shared/schema-utils/**/*.{ts,tsx,js,jsx}"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/libs/shared/schema-utils"],
+      "options": {
+        "jestConfig": "libs/shared/schema-utils/jest.config.js",
+        "passWithNoTests": true
+      }
+    }
+  }
+}
diff --git a/libs/shared/schema-utils/src/index.ts b/libs/shared/schema-utils/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..70bd0a41b24658ef1f5a4223f3172bc62f69aa25
--- /dev/null
+++ b/libs/shared/schema-utils/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/schema-utils';
diff --git a/libs/shared/schema-utils/src/lib/schema-utils.spec.ts b/libs/shared/schema-utils/src/lib/schema-utils.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21f5c4cda1dd36bf007886829531eb8b8a3ff978
--- /dev/null
+++ b/libs/shared/schema-utils/src/lib/schema-utils.spec.ts
@@ -0,0 +1,10 @@
+import { SchemaFromBackend } from "@graphpolaris/shared/data-access/store";
+import { SchemaUtils } from "./schema-utils";
+
+describe('Schema Utils', () => {
+    it('should expose a class SchemaUtils', () => {
+        const clazz = new SchemaUtils()
+      expect(clazz);
+    });
+  });
+  
\ No newline at end of file
diff --git a/libs/shared/schema-utils/src/lib/schema-utils.ts b/libs/shared/schema-utils/src/lib/schema-utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0ca15baedb7cdbe011e12d9eaf688c0fa23a73ed
--- /dev/null
+++ b/libs/shared/schema-utils/src/lib/schema-utils.ts
@@ -0,0 +1,34 @@
+import { SchemaFromBackend, Node, Edge } from '@graphpolaris/models';
+import Graph, { MultiGraph } from 'graphology';
+
+export class SchemaUtils {
+  public static ParseSchemaFromBackend(
+    schemaFromBackend: SchemaFromBackend
+  ): Graph {
+    const { nodes, edges } = schemaFromBackend;
+    // Instantiate a directed graph that allows self loops and parallel edges
+    const schemaGraph = new MultiGraph({ allowSelfLoops: true });
+    // console.log('parsing schema');
+    // The graph schema needs a node for each node AND edge. These need then be connected
+
+    nodes.forEach((node: Node) => {
+      schemaGraph.addNode(node.name, {
+        name: node.name,
+        attributes: node.attributes,
+        x: 0,
+        y: 0,
+      });
+    });
+
+    // The name of the edge will be name + from + to, since edge names are not unique
+    edges.forEach((edge: Edge) => {
+      const edgeID = [edge.name, '_', edge.from, edge.to].join(''); //ensure that all interpreted as string
+
+      // This node is the actual edge
+      schemaGraph.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, {
+        attribute: edge.attributes,
+      });
+    });
+    return schemaGraph;
+  }
+}
diff --git a/libs/shared/schema-utils/tsconfig.json b/libs/shared/schema-utils/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..4b421814593c06b901b07a205f079cc08998a1e0
--- /dev/null
+++ b/libs/shared/schema-utils/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/libs/shared/schema-utils/tsconfig.lib.json b/libs/shared/schema-utils/tsconfig.lib.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c1ac43e8efdd93ba3faef900cc1ececbf0a8eb0
--- /dev/null
+++ b/libs/shared/schema-utils/tsconfig.lib.json
@@ -0,0 +1,22 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "types": ["node"]
+  },
+  "files": [
+    "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+    "../../../node_modules/@nrwl/react/typings/image.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+    "**/*.spec.tsx",
+    "**/*.test.tsx",
+    "**/*.spec.js",
+    "**/*.test.js",
+    "**/*.spec.jsx",
+    "**/*.test.jsx"
+  ],
+  "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/shared/schema-utils/tsconfig.spec.json b/libs/shared/schema-utils/tsconfig.spec.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071
--- /dev/null
+++ b/libs/shared/schema-utils/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"]
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/package.json b/package.json
index a400f214030d9d29db1fe2ee14c9df99e50f7092..910a0f4083e87cea15b91f10f3475dcdcde43e50 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "start": "nx serve",
     "build": "nx build",
     "test": "nx test",
+    "test:all": "nx affected:test --all --codeCoverage --skip-nx-cache",
     "prepare": "husky install"
   },
   "private": true,
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 60c712ba8ea4a6e08006a81ef94e842d38de4510..8b5d935370d498584076e2c0efefb5bf3bde15f1 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,6 +15,9 @@
     "skipDefaultLibCheck": true,
     "baseUrl": ".",
     "paths": {
+      "@graphpolaris/graph-layout": ["libs/shared/graph-layout/src/index.ts"],
+      "@graphpolaris/models": ["libs/shared/models/src/index.ts"],
+      "@graphpolaris/schema-utils": ["libs/shared/schema-utils/src/index.ts"],
       "@graphpolaris/schema/schema-usecases": [
         "libs/schema/schema-usecases/src/index.ts"
       ],
diff --git a/workspace.json b/workspace.json
index 97e8805f921e5c0d0d72f6684fca76a56c42603d..97f3ebb03c09ac4d723108dc2e4e1647dd284244 100644
--- a/workspace.json
+++ b/workspace.json
@@ -2,11 +2,14 @@
   "version": 2,
   "projects": {
     "schema-schema-usecases": "libs/schema/schema-usecases",
+    "shared-data-access-authorization": "libs/shared/data-access/authorization",
     "shared-data-access-store": "libs/shared/data-access/store",
     "shared-data-access-theme": "libs/shared/data-access/theme",
+    "shared-graph-layout": "libs/shared/graph-layout",
     "shared-mock-data": "libs/shared/mock-data",
+    "shared-models": "libs/shared/models",
+    "shared-schema-utils": "libs/shared/schema-utils",
     "web-graphpolaris": "apps/web-graphpolaris",
-    "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e",
-    "shared-data-access-authorization": "libs/shared/data-access/authorization"
+    "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e"
   }
-}
\ No newline at end of file
+}