diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000000000000000000000000000000000..fe7bb8528ce296deafe951cc56d1161436ba5d72
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+  // Use IntelliSense to learn about possible attributes.
+  // Hover to view descriptions of existing attributes.
+  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "type": "chrome",
+      "request": "launch",
+      "name": "Launch Chrome against localhost",
+      "url": "http://localhost:6007",
+      "webRoot": "${workspaceFolder}"
+    }
+  ]
+}
diff --git a/libs/shared/lib/data-access/api/query.ts b/libs/shared/lib/data-access/api/query.ts
index 36aeeb0b9a8baa49c8b5a455ee963d9747a0edab..461ab0d04269027c36728e8ac02d9c6b9e95a302 100644
--- a/libs/shared/lib/data-access/api/query.ts
+++ b/libs/shared/lib/data-access/api/query.ts
@@ -1,6 +1,6 @@
 // All database related API calls
 
-import { BackendQueryFormat } from "../../querybuilder/query-utils/BackendQueryFormat";
+import { BackendQueryFormat } from "../../querybuilder/model/BackendQueryFormat";
 import { useAuthorizationCache, useSessionCache } from "../store";
 
 export const useQueryAPI = (domain: string) => {
diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
index 68ed9621859db5fa677c6a9641fee6dad01ae86a..1835b16975232ce3a318310ba34bb7c08da85f77 100644
--- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
+++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
@@ -8,6 +8,7 @@ export interface GraphQueryResultFromBackend {
   }[];
 
   edges: {
+    id: string;
     attributes: { [key: string]: unknown };
     from: string;
     to: string;
@@ -27,6 +28,7 @@ export interface Edge {
   attributes: { [key: string]: unknown };
   from: string;
   to: string;
+  id?: string;
   /* type: string; */
 }
 
@@ -62,7 +64,7 @@ export const graphQueryResultSlice = createSlice({
       // Collect all the different nodetypes in the result
       const nodeTypes: string[] = [];
       action.payload.nodes.forEach((node) => {
-        // Note: works only for arangodb
+        // TODO FIXME!! Note: works only for arangodb
         const nodeType = node.id.split('/')[0];
         if (!nodeTypes.includes(nodeType)) nodeTypes.push(nodeType);
       });
diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts
index 26488a2dac2ccfcc473330595dd8ab3be8c99a30..61f6a4a9e9504c7a1f86a7a7f1ed211bcb240c25 100644
--- a/libs/shared/lib/data-access/store/querybuilderSlice.ts
+++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts
@@ -3,14 +3,14 @@ import type { RootState } from './store';
 import { MultiGraph } from 'graphology';
 import { Attributes, SerializedGraph } from 'graphology-types';
 import {
+  QueryMultiGraphology,
   QueryMultiGraph,
-  QueryMultiGraphExport,
-} from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
+} from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 import { json } from 'd3';
 
 // Define the initial state using that type
 export const initialState = {
-  graphologySerialized: new QueryMultiGraph().export(),
+  graphologySerialized: new QueryMultiGraphology().export(),
   // schemaLayout: 'Graphology_noverlap',
 };
 
@@ -23,15 +23,15 @@ export const querybuilderSlice = createSlice({
       state,
       action: PayloadAction<SerializedGraph<Attributes, Attributes, Attributes>>
     ) => {
-      state.graphologySerialized = QueryMultiGraph.from(
+      state.graphologySerialized = QueryMultiGraphology.from(
         action.payload
-      ).export() as QueryMultiGraphExport;
+      ).export() as QueryMultiGraph;
     },
     updateQBAttributeOperator: (
       state,
       action: PayloadAction<{ id: string; operator: string }>
     ) => {
-      const graph = QueryMultiGraph.from(state.graphologySerialized);
+      const graph = QueryMultiGraphology.from(state.graphologySerialized);
       graph.setNodeAttribute(
         action.payload.id,
         'operator',
@@ -43,12 +43,12 @@ export const querybuilderSlice = createSlice({
       state,
       action: PayloadAction<{ id: string; value: string }>
     ) => {
-      const graph = QueryMultiGraph.from(state.graphologySerialized);
+      const graph = QueryMultiGraphology.from(state.graphologySerialized);
       graph.setNodeAttribute(action.payload.id, 'value', action.payload.value);
       state.graphologySerialized = graph.export();
     },
     clearQB: (state) => {
-      state.graphologySerialized = new QueryMultiGraph().export();
+      state.graphologySerialized = new QueryMultiGraphology().export();
     },
 
     // addQuerybuilderNode: (
@@ -75,10 +75,10 @@ export const {
 /** Select the querybuilder nodes in serialized fromat */
 export const selectQuerybuilderGraphology = (
   state: RootState
-): QueryMultiGraph => {
+): QueryMultiGraphology => {
   // This is really weird but for some reason all the attributes appeared as read-only otherwise
 
-  let ret = new QueryMultiGraph();
+  let ret = new QueryMultiGraphology();
   ret.import(MultiGraph.from(state.querybuilder.graphologySerialized).export());
   return ret;
 };
@@ -86,9 +86,9 @@ export const selectQuerybuilderGraphology = (
 /** Select the querybuilder nodes and convert it to a graphology object */
 export const selectQuerybuilderGraph = (
   state: RootState
-): QueryMultiGraphExport => {
+): QueryMultiGraph => {
   // This is really weird but for some reason all the attributes appeared as read-only otherwise
-  return state.querybuilder.graphologySerialized as QueryMultiGraphExport;
+  return state.querybuilder.graphologySerialized as QueryMultiGraph;
 };
 
 /** Select the querybuilder nodes and convert it to a graphology object */
diff --git a/libs/shared/lib/data-access/store/schemaSlice.spec.ts b/libs/shared/lib/data-access/store/schemaSlice.spec.ts
index 1cbc52aeb5cd2abf3671109a5edb617a587eadac..7d37c3860179f7233f705e8a19ed447e07a0317d 100644
--- a/libs/shared/lib/data-access/store/schemaSlice.spec.ts
+++ b/libs/shared/lib/data-access/store/schemaSlice.spec.ts
@@ -1,13 +1,4 @@
-import Graph from 'graphology';
-import AbstractGraph, { DirectedGraph, MultiGraph } from 'graphology';
-import { useSchemaGraphology } from '..';
-import reducer, {
-  schemaGraphology,
-  setSchema,
-  initialState,
-} from './schemaSlice';
-// import { deleteBook, updateBook, addNewBook } from '../redux/bookSlice';
-import { store } from './store';
+import { MultiGraph } from 'graphology';
 import { assert, describe, expect, it } from 'vitest';
 
 describe('SchemaSlice Tests', () => {
@@ -32,22 +23,22 @@ describe('SchemaSlice Tests', () => {
     expect(graphReloaded).toStrictEqual(graph);
   });
 
-  it('get the initial state', () => {
-    expect(initialState);
-  });
+  // it('get the initial state', () => {
+  //   expect(initialState);
+  // });
 
-  it('should return the initial state', () => {
-    const state = store.getState();
+  // it('should return the initial state', () => {
+  //   const state = store.getState();
 
-    const schema = state.schema;
-    expect(schema);
+  //   const schema = state.schema;
+  //   expect(schema);
 
-    const graph = MultiGraph.from(schema.graphologySerialized);
+  //   const graph = MultiGraph.from(schema.graphologySerialized);
 
-    // console.log(graph);
-    // console.log(initialState);
-    expect(graph);
-  });
+  //   // console.log(graph);
+  //   // console.log(initialState);
+  //   expect(graph);
+  // });
 
   //   it('should handle a todo being added to an empty list', () => {
   //     let state = store.getState().schema;
diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts
index 14a5e8446cf8dfd4916d9f807474775a9074b121..beccab8e2ea1e4584b0c2e89ac99ea2b07cd30da 100644
--- a/libs/shared/lib/data-access/store/schemaSlice.ts
+++ b/libs/shared/lib/data-access/store/schemaSlice.ts
@@ -1,17 +1,17 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import Graph, { MultiGraph } from 'graphology';
-import { SerializedGraph } from 'graphology-types';
-import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend';
 import type { RootState } from './store';
 import { AllLayoutAlgorithms, CytoscapeLayoutAlgorithms } from '@graphpolaris/shared/lib/graph-layout';
-import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
 import { SchemaUtils } from '../../schema/schema-utils';
+import { SchemaFromBackend, SchemaGraph, SchemaGraphology } from '../../schema';
 
 /**************************************************************** */
 
 // Define the initial state using that type
-export const initialState = {
-  graphologySerialized: new MultiGraph().export(),
+export const initialState: {
+  graphologySerialized: SchemaGraph;
+  layoutName: AllLayoutAlgorithms;
+} = {
+  graphologySerialized: new SchemaGraphology().export(),
   // layoutName: 'Cytoscape_fcose', 
   layoutName: CytoscapeLayoutAlgorithms.KLAY as AllLayoutAlgorithms,
 };
@@ -21,7 +21,7 @@ export const schemaSlice = createSlice({
   // `createSlice` will infer the state type from the `initialState` argument
   initialState,
   reducers: {
-    setSchema: (state, action: PayloadAction<SerializedGraph>) => {
+    setSchema: (state, action: PayloadAction<SchemaGraph>) => {
       console.log('setSchema', action);
       state.graphologySerialized = action.payload;
     },
@@ -80,16 +80,15 @@ export const { readInSchemaFromBackend, setSchema } = schemaSlice.actions;
  * */
 export const schemaGraphology = (state: RootState) => {
   // This is really weird but for some reason all the attributes appeared as read-only otherwise
-  let ret = new MultiGraph();
-  ret.import(MultiGraph.from(state.schema.graphologySerialized).export());
+  let ret = new SchemaGraphology();
+  ret.import(SchemaGraphology.from(state.schema.graphologySerialized).export());
   return ret;
 };
 
 /**
  * Select the schema
  * */
-export const schemaGraph = (state: RootState) => {
-  // This is really weird but for some reason all the attributes appeared as read-only otherwise
+export const schemaGraph = (state: RootState): SchemaGraph => {
   return state.schema.graphologySerialized;
 };
 
diff --git a/libs/shared/lib/mock-data/query-result/bigMockQueryResults.ts b/libs/shared/lib/mock-data/query-result/bigMockQueryResults.ts
index 66d8770d4c89f89b497438bd222247a3dd6164f5..49bef38b7c9bc1d4e0c86bf77a1182e1d44cea30 100644
--- a/libs/shared/lib/mock-data/query-result/bigMockQueryResults.ts
+++ b/libs/shared/lib/mock-data/query-result/bigMockQueryResults.ts
@@ -10,7 +10,7 @@
  * See testing plan for more details.*/
 
 export const bigMockQueryResults = {
-  links: [
+  edges: [
     {
       from: 'airports/DFW',
       id: 'flights/268402',
diff --git a/libs/shared/lib/mock-data/query-result/mockLargeQueryResults.ts b/libs/shared/lib/mock-data/query-result/mockLargeQueryResults.ts
index d21140f2d78f12f8b7cf705a9ffc039b109d23de..34044c6807392ffe4ed09f0024065eaf5ebcf077 100644
--- a/libs/shared/lib/mock-data/query-result/mockLargeQueryResults.ts
+++ b/libs/shared/lib/mock-data/query-result/mockLargeQueryResults.ts
@@ -4,6 +4,8 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 
+import { GraphQueryResultFromBackend } from "../../data-access/store/graphQueryResultSlice";
+
 export const mockLargeQueryResults: GraphQueryResultFromBackend = {
   "edges": [
     {
diff --git a/libs/shared/lib/mock-data/query-result/smallFlightsQueryResults.ts b/libs/shared/lib/mock-data/query-result/smallFlightsQueryResults.ts
index 1fcaff929504104d7ff0d997645459b44ded283d..0d58608d9cc1fc7aa896c72fdd68510164f9d377 100644
--- a/libs/shared/lib/mock-data/query-result/smallFlightsQueryResults.ts
+++ b/libs/shared/lib/mock-data/query-result/smallFlightsQueryResults.ts
@@ -10,7 +10,7 @@
  * See testing plan for more details.*/
 
 export const smallFlightsQueryResults = {
-  links: [
+  edges: [
     {
       from: 'airports/JFK',
       to: 'airports/SFO',
diff --git a/libs/shared/lib/mock-data/schema/moviesSchemaRaw.ts b/libs/shared/lib/mock-data/schema/moviesSchemaRaw.ts
index 4e37819de673c5cc01299200b28a69bee9c7a985..7857f98c0a9a569a13bddbc571a3b558acc6f7e6 100644
--- a/libs/shared/lib/mock-data/schema/moviesSchemaRaw.ts
+++ b/libs/shared/lib/mock-data/schema/moviesSchemaRaw.ts
@@ -1,5 +1,5 @@
 import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils';
-import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend';
+import { SchemaFromBackend } from '../../schema';
 
 export const movieSchemaRaw: SchemaFromBackend = {
   nodes: [
diff --git a/libs/shared/lib/mock-data/schema/northwindSchemaRaw.ts b/libs/shared/lib/mock-data/schema/northwindSchemaRaw.ts
index 10ae8344faeed35952221afb56db78aab0307c3c..529dbfa0496dfaa78880bf29b5900a2660760f69 100644
--- a/libs/shared/lib/mock-data/schema/northwindSchemaRaw.ts
+++ b/libs/shared/lib/mock-data/schema/northwindSchemaRaw.ts
@@ -1,5 +1,5 @@
 import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils';
-import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend';
+import { SchemaFromBackend } from '../../schema';
 
 export const northwindSchemaRaw: SchemaFromBackend = {
   nodes: [
diff --git a/libs/shared/lib/mock-data/schema/simpleAirportRaw.ts b/libs/shared/lib/mock-data/schema/simpleAirportRaw.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8524c549d1451709392292257d04661ab2b4ed4
--- /dev/null
+++ b/libs/shared/lib/mock-data/schema/simpleAirportRaw.ts
@@ -0,0 +1,43 @@
+import { SchemaFromBackend } from '../../schema';
+import { SchemaUtils } from '../../schema/schema-utils';
+
+export const simpleSchemaAirportRaw: SchemaFromBackend = {
+  nodes: [
+    {
+      name: 'airports',
+      attributes: [
+        { name: 'city', type: 'string' },
+        { name: 'country', type: 'string' },
+        { name: 'lat', type: 'float' },
+        { name: 'long', type: 'float' },
+        { name: 'name', type: 'string' },
+        { name: 'state', type: 'string' },
+        { name: 'vip', type: 'bool' },
+      ],
+    }
+  ],
+  edges: [
+    {
+      name: 'flights',
+      from: 'airports',
+      to: 'airports',
+      collection: 'flights',
+      attributes: [
+        { name: 'ArrTime', type: 'int' },
+        { name: 'ArrTimeUTC', type: 'string' },
+        { name: 'Day', type: 'int' },
+        { name: 'DayOfWeek', type: 'int' },
+        { name: 'DepTime', type: 'int' },
+        { name: 'DepTimeUTC', type: 'string' },
+        { name: 'Distance', type: 'int' },
+        { name: 'FlightNum', type: 'int' },
+        { name: 'Month', type: 'int' },
+        { name: 'TailNum', type: 'string' },
+        { name: 'UniqueCarrier', type: 'string' },
+        { name: 'Year', type: 'int' },
+      ],
+    }
+  ],
+};
+
+export const simpleSchemaAirport = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw);
diff --git a/libs/shared/lib/mock-data/schema/simpleRaw.ts b/libs/shared/lib/mock-data/schema/simpleRaw.ts
index df6855fffe7bba7f000a6df17a2e641d81db3090..5444e1f57b27731fedc78c3c3d33a1ff13029199 100644
--- a/libs/shared/lib/mock-data/schema/simpleRaw.ts
+++ b/libs/shared/lib/mock-data/schema/simpleRaw.ts
@@ -1,5 +1,5 @@
-import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils';
-import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend';
+import { SchemaFromBackend } from "../../schema";
+import { SchemaUtils } from "../../schema/schema-utils";
 
 export const simpleSchemaRaw: SchemaFromBackend = {
   nodes: [
@@ -103,4 +103,4 @@ export const simpleSchemaRaw: SchemaFromBackend = {
   ],
 };
 
-export const simpleSchema = SchemaUtils.schemaBackend2Graphology(simpleSchemaRaw);
+export const simpleSchema = SchemaUtils.schemaBackend2Graphology(simpleSchemaRaw);
\ No newline at end of file
diff --git a/libs/shared/lib/model/backend/index.ts b/libs/shared/lib/model/backend/index.ts
deleted file mode 100644
index 5d5aa13bd15aa8a201d524ee65b6102f5bebea1a..0000000000000000000000000000000000000000
--- a/libs/shared/lib/model/backend/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './schema';
\ No newline at end of file
diff --git a/libs/shared/lib/model/general.ts b/libs/shared/lib/model/general.ts
deleted file mode 100644
index b57417b67fdaaa702fb2aea66660e6bfec57be80..0000000000000000000000000000000000000000
--- a/libs/shared/lib/model/general.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Edge } from "reactflow";
-
-/**
- * Point that has an x and y coordinate
- */
-export interface Point {
-    x: number;
-    y: number;
-}
-
-/**
- * Bounding box described by a top left and bottom right coordinate
- */
-export interface BoundingBox {
-    topLeft: Point;
-    bottomRight: Point;
-}
-
-/**
- * List of schema elements for react flow
- */
-export type SchemaElements = {
-    nodes: Node[];
-    edges: Edge[];
-    selfEdges: Edge[];
-};
\ No newline at end of file
diff --git a/libs/shared/lib/model/graphology.ts b/libs/shared/lib/model/graphology.ts
deleted file mode 100644
index 48b99a327e89e2b372313abb37e8f800d8d0ea9b..0000000000000000000000000000000000000000
--- a/libs/shared/lib/model/graphology.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Attributes as GAttributes, NodeEntry, EdgeEntry } from "graphology-types";
-
-/** Attribute type, consist of a name */
-export type Attributes = GAttributes & {
-    name: string;
-    type: 'string' | 'int' | 'bool' | 'float';
-};
-
-export type Node = NodeEntry<Attributes>;
-export type Edge = EdgeEntry<Attributes, Attributes>;
\ No newline at end of file
diff --git a/libs/shared/lib/model/index.ts b/libs/shared/lib/model/index.ts
deleted file mode 100644
index 8e985605049e5c2800de806d286f3dcf76871c44..0000000000000000000000000000000000000000
--- a/libs/shared/lib/model/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './general';
-export * from './graphology';
\ No newline at end of file
diff --git a/libs/shared/lib/model/reactflow.ts b/libs/shared/lib/model/reactflow.ts
deleted file mode 100644
index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000
--- a/libs/shared/lib/model/reactflow.ts
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/libs/shared/lib/querybuilder/graph/graphology/JSONParser.tsx b/libs/shared/lib/querybuilder/graph/graphology/JSONParser.tsx
deleted file mode 100644
index 7d645f7457888aa32c25af47fd8868eaaa74dde5..0000000000000000000000000000000000000000
--- a/libs/shared/lib/querybuilder/graph/graphology/JSONParser.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * This program has been developed by students from the bachelor Computer Science at
- * Utrecht University within the Software Project course.
- * © Copyright Utrecht University (Department of Information and Computing Sciences)
- */
-
-/** JSON query format used to send a query to the backend. */
-export interface TranslatedJSONQuery {
-  return: {
-    entities: number[];
-    relations: number[];
-    groupBys: number[];
-  };
-  entities: Entity[];
-  relations: Relation[];
-  groupBys: GroupBy[];
-  machineLearning: MachineLearning[];
-  limit: number;
-}
-
-/** Interface for an entity in the JSON for the query. */
-export interface Entity {
-  name: string;
-  ID: number;
-  constraints: Constraint[];
-}
-
-/** Interface for an relation in the JSON for the query. */
-export interface Relation {
-  name: string;
-  ID: number;
-  fromType: string;
-  fromID: number;
-  toType: string;
-  toID: number;
-  depth: { min: number; max: number };
-  constraints: Constraint[];
-}
-
-/**
- * Constraint datatypes created from the attributes of a relation or entity.
- *
- * string MatchTypes: exact/contains/startswith/endswith.
- * int    MatchTypes: GT/LT/EQ.
- * bool   MatchTypes: EQ/NEQ.
- */
-export interface Constraint {
-  attribute: string;
-  dataType: string;
-
-  matchType: string;
-  value: string;
-}
-
-/** Interface for a function in the JSON for the query. */
-export interface GroupBy {
-  ID: number;
-
-  groupType: string;
-  groupID: number[];
-  groupAttribute: string;
-
-  byType: string;
-  byID: number[];
-  byAttribute: string;
-
-  appliedModifier: string;
-  relationID: number;
-  constraints: Constraint[];
-}
-
-/** Interface for Machine Learning algorithm */
-export interface MachineLearning {
-  ID?: number;
-  queuename: string;
-  parameters: string[];
-}
-/** Interface for what the JSON needs for link predicition */
-export interface LinkPrediction {
-  queuename: string;
-  parameters: {
-    key: string;
-    value: string;
-  }[];
-}
diff --git a/libs/shared/lib/querybuilder/index.ts b/libs/shared/lib/querybuilder/index.ts
index 35906bceac37fc5a9626ec94bb7e9dce8d453908..f0c630f2d0db12e8781254b62fbbd1c675771d14 100644
--- a/libs/shared/lib/querybuilder/index.ts
+++ b/libs/shared/lib/querybuilder/index.ts
@@ -1,3 +1,4 @@
 export * from './panel';
 export * from './pills';
-export * from './query-utils';
\ No newline at end of file
+export * from './query-utils';
+export * from './model';
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
similarity index 88%
rename from libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx
rename to libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
index 4866d74127fa490916db556a387888c1333fd8d0..d13b4b6f7cc485b234ea6d5740ae60ec9e84fe7b 100644
--- a/libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx
+++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx
@@ -95,6 +95,7 @@ export interface MachineLearning {
   queuename: string;
   parameters: string[];
 }
+
 /** Interface for what the JSON needs for link predicition */
 export interface LinkPrediction {
   queuename: string;
@@ -110,3 +111,18 @@ export interface ModifierStruct {
   selectedTypeID: number;
   attributeIndex: number;
 }
+
+
+/** JSON query format used to send a query to the backend. */
+export interface TranslatedJSONQuery {
+  return: {
+    entities: number[];
+    relations: number[];
+    groupBys: number[];
+  };
+  entities: Entity[];
+  relations: Relation[];
+  groupBys: GroupBy[];
+  machineLearning: MachineLearning[];
+  limit: number;
+}
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/model/graphology/index.ts b/libs/shared/lib/querybuilder/model/graphology/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..50589a9b7cce38568546263dec73d346ee49f66e
--- /dev/null
+++ b/libs/shared/lib/querybuilder/model/graphology/index.ts
@@ -0,0 +1,2 @@
+export * from './model'
+export * from './utils'
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/graph/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts
similarity index 96%
rename from libs/shared/lib/querybuilder/graph/graphology/model.ts
rename to libs/shared/lib/querybuilder/model/graphology/model.ts
index 194ed07c768ce1725b8e59171c115cd368d96567..de7c52ad5f8e25f5f0ec363991d93e3ef06f9271 100644
--- a/libs/shared/lib/querybuilder/graph/graphology/model.ts
+++ b/libs/shared/lib/querybuilder/model/graphology/model.ts
@@ -15,7 +15,7 @@ import {
   store,
 } from '@graphpolaris/shared/lib/data-access';
 import { MultiGraph } from 'graphology';
-import { calcWidthHeightOfPill } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
+import { calcWidthHeightOfPill } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 import './utils';
 
 // export interface Attributes extends EntityNode | RelationNode | AttributeNode | FunctionNode | ModifierNode  {
diff --git a/libs/shared/lib/querybuilder/graph/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts
similarity index 97%
rename from libs/shared/lib/querybuilder/graph/graphology/utils.ts
rename to libs/shared/lib/querybuilder/model/graphology/utils.ts
index 4128f61e900e39a76f203b3619199e2b8db1a915..d31faed13370f7b957747f188973d1891b8b9415 100644
--- a/libs/shared/lib/querybuilder/graph/graphology/utils.ts
+++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts
@@ -23,13 +23,13 @@ const widthPerFontsize = {
   10: 6.0167,
 };
 
-export type QueryMultiGraphExport = SerializedGraph<
+export type QueryMultiGraph = SerializedGraph<
   NodeAttributes | EntityNodeAttributes | RelationNodeAttributes,
   GAttributes,
   GAttributes
 >;
 
-export class QueryMultiGraph extends MultiGraph<
+export class QueryMultiGraphology extends MultiGraph<
   NodeAttributes | EntityNodeAttributes | RelationNodeAttributes,
   GAttributes,
   GAttributes
diff --git a/libs/shared/lib/querybuilder/model/index.ts b/libs/shared/lib/querybuilder/model/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fdf75046ac681de94eaf3c6b8e95604acff44390
--- /dev/null
+++ b/libs/shared/lib/querybuilder/model/index.ts
@@ -0,0 +1,4 @@
+export * from './BackendQueryFormat';
+export * from './graphology';
+export * from './logic';
+export * from './reactflow';
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/model/logic/index.ts b/libs/shared/lib/querybuilder/model/logic/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..465510a488946b76cf55657037a4305156ba17e2
--- /dev/null
+++ b/libs/shared/lib/querybuilder/model/logic/index.ts
@@ -0,0 +1 @@
+export * from './queryFunctions'
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/graph/logic/queryFunctions.tsx b/libs/shared/lib/querybuilder/model/logic/queryFunctions.tsx
similarity index 100%
rename from libs/shared/lib/querybuilder/graph/logic/queryFunctions.tsx
rename to libs/shared/lib/querybuilder/model/logic/queryFunctions.tsx
diff --git a/libs/shared/lib/querybuilder/graph/reactflow/handles.tsx b/libs/shared/lib/querybuilder/model/reactflow/handles.tsx
similarity index 100%
rename from libs/shared/lib/querybuilder/graph/reactflow/handles.tsx
rename to libs/shared/lib/querybuilder/model/reactflow/handles.tsx
diff --git a/libs/shared/lib/querybuilder/model/reactflow/index.ts b/libs/shared/lib/querybuilder/model/reactflow/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d808fb6e42a51a28967d0bd16062b37b5f54b17
--- /dev/null
+++ b/libs/shared/lib/querybuilder/model/reactflow/index.ts
@@ -0,0 +1,4 @@
+export * from './handles'
+export * from './model'
+export * from './pillHandles'
+export * from './utils'
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/graph/reactflow/model.tsx b/libs/shared/lib/querybuilder/model/reactflow/model.tsx
similarity index 100%
rename from libs/shared/lib/querybuilder/graph/reactflow/model.tsx
rename to libs/shared/lib/querybuilder/model/reactflow/model.tsx
diff --git a/libs/shared/lib/querybuilder/graph/reactflow/pillHandles.ts b/libs/shared/lib/querybuilder/model/reactflow/pillHandles.ts
similarity index 100%
rename from libs/shared/lib/querybuilder/graph/reactflow/pillHandles.ts
rename to libs/shared/lib/querybuilder/model/reactflow/pillHandles.ts
diff --git a/libs/shared/lib/querybuilder/graph/reactflow/utils.ts b/libs/shared/lib/querybuilder/model/reactflow/utils.ts
similarity index 100%
rename from libs/shared/lib/querybuilder/graph/reactflow/utils.ts
rename to libs/shared/lib/querybuilder/model/reactflow/utils.ts
diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.stories.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.stories.tsx
index 1bc0f5778bdbdbb158887faee1fb2728b71a34c5..0d45f3ba52a05192c81cc82e54800bce7c25db5f 100644
--- a/libs/shared/lib/querybuilder/panel/querybuilder.stories.tsx
+++ b/libs/shared/lib/querybuilder/panel/querybuilder.stories.tsx
@@ -10,8 +10,7 @@ import { configureStore } from '@reduxjs/toolkit';
 import { Meta } from '@storybook/react';
 import { Provider } from 'react-redux';
 import QueryBuilder from './querybuilder';
-import { Handles } from '../graph/reactflow/handles';
-import { QueryMultiGraph } from '../graph/graphology/utils';
+import { Handles, QueryMultiGraphology } from '../model';
 
 const Component: Meta<typeof QueryBuilder> = {
   component: QueryBuilder,
@@ -34,7 +33,7 @@ const Component: Meta<typeof QueryBuilder> = {
   ],
 };
 
-const graph = new QueryMultiGraph();
+const graph = new QueryMultiGraphology();
 graph.addPill2Graphology(
   { type: 'entity', x: 100, y: 100, name: 'Entity Pill', fadeIn: false },
   '0'
diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
index 4417154370c5074de735d09bb33af237c234fc8e..bcfcf74087fa09492d587e187dee2993ce7a6f50 100644
--- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
@@ -31,7 +31,6 @@ import {
   EntityFlowElement,
   RelationPill,
 } from '../pills';
-import { createReactFlowElements } from '../graph/reactflow/utils';
 import {
   dragPillStarted,
   dragPillStopped,
@@ -43,18 +42,13 @@ import {
   ImportExport as ExportIcon,
 } from '@mui/icons-material';
 import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
-import {
-  EntityNode,
-  QueryElementTypes,
-  RelationNode,
-} from '@graphpolaris/shared/lib/querybuilder/graph/reactflow/model';
 import {
   RelationPosToFromEntityPos,
   RelationPosToToEntityPos,
-} from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
-import { Handles } from '@graphpolaris/shared/lib/querybuilder/graph/reactflow/handles';
+} from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 import { useDispatch } from 'react-redux';
 import { Card, CardContent, Typography } from '@mui/material';
+import { Handles, QueryElementTypes, createReactFlowElements } from '../model';
 
 const nodeTypes = {
   entity: EntityFlowElement,
diff --git a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx
index 61625236bcd0383d923520946dee33e4a77bcc80..0bf09cee126a8fad32c632614015b52e6045683a 100644
--- a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx
+++ b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx
@@ -17,9 +17,9 @@ import { movieSchemaRaw } from '@graphpolaris/shared/lib/mock-data';
 import { QueryBuilder } from '@graphpolaris/shared/lib/querybuilder';
 import { configureStore } from '@reduxjs/toolkit';
 import { configSlice } from '@graphpolaris/shared/lib/data-access/store/configSlice';
-import { QueryGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/model';
+import { QueryGraph } from '@graphpolaris/shared/lib/querybuilder/model/graphology/model';
 import { MultiGraph } from 'graphology';
-import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
+import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 
 const SchemaAndQueryBuilder = () => {
   return (
@@ -69,7 +69,7 @@ export const SchemaAndQueryBuilderInteractivity = {
     const dispatch = store.dispatch;
     const schema = SchemaUtils.schemaBackend2Graphology(movieSchemaRaw);
 
-    const graph = new QueryMultiGraph();
+    const graph = new QueryMultiGraphology();
     dispatch(setQuerybuilderNodes(graph.export()));
     dispatch(setSchema(schema.export()));
   },
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill-full.stories.tsx
index 196afa73c2f61830f6c8d6a073bf4e6d713e9f83..1bfee71e602c9a5f979a4836883e30260cf9ee7f 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill-full.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill-full.stories.tsx
@@ -10,9 +10,9 @@ import { Meta } from '@storybook/react';
 import { Provider } from 'react-redux';
 import { MultiGraph } from 'graphology';
 import { QueryBuilder } from '../../../panel';
-import { QueryGraph } from '../../../graph/graphology/model';
+import { QueryGraph } from '../../../model/graphology/model';
 import { circular } from 'graphology-layout';
-import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
+import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 
 const Component: Meta<typeof QueryBuilder> = {
   component: QueryBuilder,
@@ -33,7 +33,7 @@ const mockStore = configureStore({
     querybuilder: querybuilderSlice.reducer,
   },
 });
-const graph = new QueryMultiGraph();
+const graph = new QueryMultiGraphology();
 graph.addPill2Graphology(
   {
     type: 'attribute',
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill.tsx
index 26d4bb0bc7c6fd19e1e286a8788e19cfa40d37e7..89c663492b5cc4bb1716b6cff5f4b52f574683cb 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/attributepill.tsx
@@ -4,8 +4,7 @@ import styles from './attributepill.module.scss';
 import { Handle, NodeProps, Position } from 'reactflow';
 import { AttributeOperatorSelect } from './operator';
 import Select from './select-component';
-import { AttributeNode } from '../../../graph/reactflow/model';
-import { Handles } from '../../../graph/reactflow/handles';
+import { AttributeNode, Handles } from '../../../model';
 
 /**
  * Component to render an attribute flow element
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/select-component.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/select-component.tsx
index 38a35b85e5ebfd44f7c68d9bb1b2a753ad4a2cca..3e6b6a979bf1b234affb0c3352bfbe95cea32e88 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/select-component.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/attributepill/select-component.tsx
@@ -11,7 +11,7 @@
 import { useTheme } from '@mui/material';
 import React, { useState } from 'react';
 import styles from './attributepill.module.scss';
-import { AttributeData } from '../../../graph/reactflow/model';
+import { AttributeData } from '../../../model';
 
 export default function SelectComponent({ data }: { data: AttributeData }) {
   const theme = useTheme();
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx
index 18b65e8be65a8550fd1feed2a693888d604f1a37..fe8d4464a46b28a0ad688468265409e1e783430e 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx
@@ -10,9 +10,9 @@ import { Meta } from '@storybook/react';
 import { Provider } from 'react-redux';
 import { MultiGraph } from 'graphology';
 import { QueryBuilder } from '../../../panel';
-import { QueryGraph } from '../../../graph/graphology/model';
+import { QueryGraph } from '../../../model/graphology/model';
 import { circular } from 'graphology-layout';
-import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils';
+import { QueryMultiGraphology } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils';
 
 const Component: Meta<typeof QueryBuilder> = {
   component: QueryBuilder,
@@ -33,7 +33,7 @@ const mockStore = configureStore({
     querybuilder: querybuilderSlice.reducer,
   },
 });
-const graph = new QueryMultiGraph();
+const graph = new QueryMultiGraphology();
 graph.addPill2Graphology(
   { type: 'entity', x: 100, y: 100, name: 'Entity Pill', fadeIn: false },
   '2'
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx
index 87d7a9e8ce85b6e08c8f2e90c6d723cf761405d8..b637b59389af1433c9b2acf33d2020a7a39e8ee9 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.stories.tsx
@@ -11,7 +11,7 @@ import {
   schemaSlice,
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
-import { EntityData, EntityNode } from '../../../graph-reactflow/model';
+import { EntityData } from '../../../model';
 
 const Component: Meta<typeof EntityFlowElement> = {
   /* 👇 The title prop is optional.
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx
index 51f8aa0742044a27707d2359d7bc86bdb7b10288..9a126d5d36b105cd343117c7bd102ceb175b2dae 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx
@@ -3,8 +3,7 @@ import { useTheme } from '@mui/material';
 import React, { useEffect } from 'react';
 import { ReactFlow, Handle, Position } from 'reactflow';
 import styles from './entitypill.module.scss';
-import { EntityNode } from '../../../graph/reactflow/model';
-import { Handles } from '../../../graph/reactflow/handles';
+import { EntityNode, Handles } from '../../../model';
 
 /**
  * Component to render an entity flow element
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.stories.tsx
index 2488295e09b2e2b682a60dde06359a59a526640c..559e2f1455e5a05a753398043679df86ae5bbf8d 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.stories.tsx
@@ -11,11 +11,7 @@ import {
   schemaSlice,
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
-import {
-  EntityData,
-  EntityNode,
-  FunctionData,
-} from '../../../graph-reactflow/model';
+import { FunctionData } from '../../../model';
 
 const Component: Meta<typeof FunctionFlowElement> = {
   /* 👇 The title prop is optional.
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.tsx
index c0edcaff21c16a0f102d965fa5da96759bc23495..d97f2c50b07a6d8032c6e8803f1eb3e9b9f195c3 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/functionpill/functionpill.tsx
@@ -12,8 +12,7 @@ import React, { useState } from 'react';
 import { Handle, Position } from 'reactflow';
 import styles from './functionpill.module.scss';
 import { useTheme } from '@mui/material';
-import { FunctionData, FunctionNode } from '../../../graph/reactflow/model';
-import { Handles } from '../../../graph/reactflow/handles';
+import { FunctionData, Handles } from '../../../model';
 
 const countArgs = (data: FunctionData | undefined) => {
   if (data !== undefined) {
@@ -133,9 +132,8 @@ export default function RelationFlowElement({ data }: FunctionNode) {
         <div className={styles.functionWrapper}>{rows}</div>
       </div>
       <div
-        className={`${styles.entity} entityWrapper ${
-          entity === undefined ? 'hidden' : ''
-        }`}
+        className={`${styles.entity} entityWrapper ${entity === undefined ? 'hidden' : ''
+          }`}
       >
         <Handle
           id={Handles.ToRelation}
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/modifierpill/mopdifierpill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/modifierpill/mopdifierpill.stories.tsx
index 480fe054e9465947e2e0c47d8b0a981aef0c9548..4b4c7f2633135a78a63726074eadd532e2bbbd65 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/modifierpill/mopdifierpill.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/modifierpill/mopdifierpill.stories.tsx
@@ -11,12 +11,7 @@ import {
   schemaSlice,
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
-import {
-  EntityData,
-  EntityNode,
-  FunctionData,
-  ModifierData,
-} from '../../../graph-reactflow/model';
+import { ModifierData } from '../../../model';
 
 const Component: Meta<typeof ModifierPill> = {
   /* 👇 The title prop is optional.
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx
similarity index 89%
rename from libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full.stories.tsx
rename to libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx
index b01ffd8f39dff716a8c90e804f3ef9329bd1fea6..3527cfb3331e447ddd1a2a209bc42aeb4f467392 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx
@@ -10,8 +10,9 @@ import { Meta } from '@storybook/react';
 import { Provider } from 'react-redux';
 import { MultiGraph } from 'graphology';
 import { QueryBuilder } from '../../../panel';
-import { QueryGraph } from '../../../graph/graphology/model';
+import { QueryGraph } from '../../../model/graphology/model';
 import { circular } from 'graphology-layout';
+import { QueryMultiGraphology } from '../../../model';
 
 const Component: Meta<typeof QueryBuilder> = {
   component: QueryBuilder,
@@ -32,7 +33,7 @@ const mockStore = configureStore({
     querybuilder: querybuilderSlice.reducer,
   },
 });
-const graph: QueryGraph = new MultiGraph();
+const graph = new QueryMultiGraphology();
 graph.addPill2Graphology(
   {
     type: 'relation',
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill copy.txt b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill copy.txt
deleted file mode 100644
index 805e3f3fc78bd04389de1363e61d22592aa70044..0000000000000000000000000000000000000000
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill copy.txt	
+++ /dev/null
@@ -1,242 +0,0 @@
-import React, { memo, useRef, useState } from 'react';
-
-import { handles } from '@graphpolaris/shared/lib/querybuilder/usecases';
-import { useTheme } from '@mui/material';
-import { Handle, Position } from 'reactflow';
-import cn from 'classnames';
-
-import styles from './relationpill.module.scss';
-import { Handles } from '../../../structures/Handles';
-import { RelationNode } from '../../../structures/Nodes';
-
-// export type RelationRFPillProps = {
-//   data: {
-//     name: string;
-//     suggestedForConnection: any;
-//     isFromEntityConnected?: boolean;
-//     isToEntityConnected?: boolean;
-//   };
-// };
-
-/**
- * Component to render a relation flow element
- * @param { FlowElement<RelationData>} param0 The data of a relation flow element.
- */
-export const RelationPill = memo(({ data }: RelationNode) => {
-  // export default function RelationRFPill({ data }: { data: any }) {
-  const theme = useTheme();
-  // console.log('RelationRFPill', data);
-
-  const minRef = useRef<HTMLInputElement>(null);
-  const maxRef = useRef<HTMLInputElement>(null);
-
-  const [readOnlyMin, setReadOnlyMin] = useState(true);
-  const [readOnlyMax, setReadOnlyMax] = useState(true);
-
-  const onDepthChanged = (depth: string) => {
-    // Don't allow depth above 99
-    const limit = 99;
-    if (data?.depth != undefined) {
-      data.depth.min = data.depth.min >= limit ? limit : data.depth.min;
-      data.depth.max = data.depth.max >= limit ? limit : data.depth.max;
-
-      // Check for for valid depth: min <= max
-      if (depth == 'min') {
-        if (data.depth.min > data.depth.max) data.depth.max = data.depth.min;
-        setReadOnlyMin(true);
-      } else if (depth == 'max') {
-        if (data.depth.max < data.depth.min) data.depth.min = data.depth.max;
-        setReadOnlyMax(true);
-      }
-
-      // Set to the correct width
-      if (maxRef.current)
-        maxRef.current.style.maxWidth = calcWidth(data.depth.max);
-      if (minRef.current)
-        minRef.current.style.maxWidth = calcWidth(data.depth.min);
-    }
-  };
-
-  const isNumber = (x: string) => {
-    {
-      if (typeof x != 'string') return false;
-      return !Number.isNaN(x) && !Number.isNaN(parseFloat(x));
-    }
-  };
-
-  const calcWidth = (data: number) => {
-    return data.toString().length + 0.5 + 'ch';
-  };
-
-  return (
-    <div
-      className={styles.relation}
-      style={{
-        background: theme.palette.custom.nodesBase[0],
-        borderTop: `4px solid ${theme.palette.custom.nodesBase[0]}`,
-        borderBottom: `6px solid ${theme.palette.custom.elements.relationBase[0]}`,
-      }}
-    >
-      <div className={styles.relationWrapper}>
-        <div
-          className={[
-            styles.relationNodeTriangleGeneral,
-            styles.relationNodeTriangleLeft,
-          ].join(' ')}
-          style={{ borderRightColor: theme.palette.custom.nodesBase[0] }}
-        >
-          <span className={styles.relationHandleFiller}>
-            <Handle
-              id={Handles.RelationLeft}
-              type="target"
-              position={Position.Left}
-              className={
-                styles.relationHandleLeft +
-                ' ' +
-                (false ? styles.handleConnectedBorderLeft : '')
-              }
-            />
-          </span>
-        </div>
-        <div
-          className={[
-            styles.relationNodeTriangleGeneral,
-            styles.relationNodeSmallTriangleLeft,
-          ].join(' ')}
-          style={{
-            borderRightColor: theme.palette.custom.elements.relationBase[0],
-          }}
-        ></div>
-        <div
-          className={[
-            styles.relationNodeTriangleGeneral,
-            styles.relationNodeTriangleRight,
-          ].join(' ')}
-          style={{ borderLeftColor: theme.palette.custom.nodesBase[0] }}
-        >
-          <span className={styles.relationHandleFiller}>
-            <Handle
-              id={Handles.RelationRight}
-              type="target"
-              position={Position.Right}
-              className={
-                styles.relationHandleRight +
-                ' ' +
-                (false ? styles.handleConnectedBorderRight : '')
-              }
-            />
-          </span>
-        </div>
-        <div
-          className={[
-            styles.relationNodeTriangleGeneral,
-            styles.relationNodeSmallTriangleRight,
-          ].join(' ')}
-          style={{
-            borderLeftColor: theme.palette.custom.elements.relationBase[0],
-          }}
-        ></div>
-
-        <span className={styles.relationHandleFiller}>
-          <Handle
-            id={Handles.ToAttributeHandle}
-            type="target"
-            position={Position.Bottom}
-            className={
-              styles.relationHandleBottom +
-              ' ' +
-              (false ? styles.handleConnectedFill : '')
-            }
-          />
-        </span>
-        <div className={styles.relationDataWrapper}>
-          <span className={styles.relationSpan}>{data?.name}</span>
-          <span className={styles.relationInputHolder}>
-            <span>[</span>
-            <input
-              className={
-                styles.relationInput +
-                ' ' +
-                (readOnlyMin ? styles.relationReadonly : '')
-              }
-              ref={minRef}
-              type="string"
-              min={0}
-              readOnly={readOnlyMin}
-              placeholder={'?'}
-              value={data?.depth.min}
-              onChange={(e) => {
-                if (data != undefined) {
-                  data.depth.min = isNumber(e.target.value)
-                    ? parseInt(e.target.value)
-                    : 0;
-                  e.target.style.maxWidth = calcWidth(data.depth.min);
-                }
-              }}
-              onDoubleClick={() => {
-                setReadOnlyMin(false);
-              }}
-              onBlur={(e) => {
-                onDepthChanged('min');
-              }}
-              onKeyDown={(e) => {
-                if (e.key === 'Enter') {
-                  onDepthChanged('min');
-                }
-              }}
-            ></input>
-            <span>..</span>
-            <input
-              className={
-                styles.relationInput +
-                ' ' +
-                (readOnlyMax ? styles.relationReadonly : '')
-              }
-              ref={maxRef}
-              type="string"
-              min={0}
-              readOnly={readOnlyMax}
-              placeholder={'?'}
-              value={data?.depth.max}
-              onChange={(e) => {
-                if (data != undefined) {
-                  data.depth.max = isNumber(e.target.value)
-                    ? parseInt(e.target.value)
-                    : 0;
-                  e.target.style.maxWidth = calcWidth(data.depth.max);
-                }
-              }}
-              onDoubleClick={() => {
-                setReadOnlyMax(false);
-              }}
-              onBlur={(e) => {
-                onDepthChanged('max');
-              }}
-              onKeyDown={(e) => {
-                if (e.key === 'Enter') {
-                  onDepthChanged('max');
-                }
-              }}
-            ></input>
-            <span>]</span>
-          </span>
-        </div>
-        <Handle
-          id={Handles.ReceiveFunction}
-          type="target"
-          position={Position.Bottom}
-          className={
-            styles.relationHandleFunction +
-            ' ' +
-            styles.handleFunction +
-            ' ' +
-            (false ? styles.handleConnectedFill : '')
-          }
-        />
-      </div>
-    </div>
-  );
-});
-
-RelationPill.displayName = 'RelationPill';
-export default RelationPill;
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module copy.scss b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module copy.scss
deleted file mode 100644
index e7dd844ff495b98f1ff9ceebf1e57b41dc9fede0..0000000000000000000000000000000000000000
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.module copy.scss	
+++ /dev/null
@@ -1,304 +0,0 @@
-@import '../../querypills.module.scss';
-
-.relation {
-  display: flex;
-  text-align: center;
-  font-family: monospace;
-  font-weight: bold;
-  font-size: 10px;
-  background-color: transparent;
-}
-
-.highlighted {
-  box-shadow: black 0 0 2px;
-}
-
-.contentWrapper {
-  display: flex;
-  align-items: center;
-
-  .handleLeft {
-    position: relative;
-    z-index: 3;
-
-    top: 25%;
-    border: 0px;
-    border-radius: 0px;
-
-    background: transparent;
-    transform-origin: center;
-
-    width: 0;
-    height: 0;
-    border-top: 5px solid transparent;
-    border-bottom: 5px solid transparent;
-    border-right: rgba(255, 255, 255, 0.7) 6px solid;
-
-    &::after {
-      content: '';
-      display: block;
-      position: absolute;
-      width: 0;
-      height: 0;
-      border-top: 7px solid transparent;
-      border-bottom: 7px solid transparent;
-      border-right: rgba(0, 0, 0, 0.1) 8px solid;
-      top: -7px;
-      right: -7px;
-    }
-  }
-  .highlighted {
-    z-index: -1;
-    box-shadow: 0 0 2px 1px gray;
-  }
-
-  .content {
-    margin: 0 2ch;
-    padding: 3px 0;
-    max-width: 20ch;
-    text-overflow: ellipsis;
-    overflow: hidden;
-    white-space: nowrap;
-    // pointer-events: none;
-  }
-
-  .handleRight {
-    position: relative;
-    top: 25%;
-    border: 0px;
-    border-radius: 0px;
-
-    background: transparent;
-    transform-origin: center;
-
-    width: 0;
-    height: 0;
-    border-top: 5px solid transparent;
-    border-bottom: 5px solid transparent;
-    border-left: rgba(255, 255, 255, 0.7) 6px solid;
-
-    &::after {
-      content: '';
-      display: block;
-      position: absolute;
-      width: 0;
-      height: 0;
-      border-top: 7px solid transparent;
-      border-bottom: 7px solid transparent;
-      border-left: rgba(0, 0, 0, 0.1) 8px solid;
-      top: -7px;
-      left: -7px;
-    }
-  }
-}
-
-$height: 10px;
-.arrowLeft {
-  z-index: 2;
-  width: 0;
-  height: 0;
-  border-top: $height solid transparent;
-  border-bottom: $height solid transparent;
-  transform: scale(1.028) translate(0.3px 0px);
-
-  border-right: $height solid;
-}
-
-.arrowRight {
-  width: 0;
-  height: 0;
-  border-top: $height solid transparent;
-  border-bottom: $height solid transparent;
-  transform: scale(1.02) translate(-0.3px 0px);
-
-  border-left: $height solid;
-}
-
-// Relation element
-.relation {
-  height: 36;
-  min-width: 240px;
-  display: flex;
-  text-align: center;
-  color: black;
-  line-height: 20px;
-  font-family: monospace;
-  font-weight: bold;
-  font-size: 11px;
-}
-
-.relationWrapper {
-  display: inherit;
-  align-items: center;
-  justify-content: space-between;
-}
-
-.relationNodeTriangleGeneral {
-  position: absolute;
-  width: 0;
-  height: 0;
-  margin: auto 0;
-  border-style: solid;
-  border-color: transparent;
-}
-
-.relationNodeTriangleLeft {
-  transform: translateX(-100%);
-  top: 0px;
-  border-width: 18px 24px 18px 0;
-}
-
-.relationNodeSmallTriangleLeft {
-  transform: translateX(-100%);
-  top: 30px;
-  border-width: 0 8px 6px 0;
-}
-
-.relationNodeTriangleRight {
-  right: -24px;
-  top: 0px;
-  border-width: 18px 0 18px 24px;
-}
-
-.relationNodeSmallTriangleRight {
-  right: -8px;
-  top: 30px;
-  border-width: 0 0 6px 8px;
-}
-
-.relationHandleFiller {
-  display: block;
-}
-
-.relationHandleLeft {
-  position: absolute;
-  top: 50%;
-  margin-left: 15px;
-  border-style: solid;
-  border-width: 8px 12px 8px 0;
-  border-radius: 0px;
-  left: unset;
-  border-color: transparent rgba(0, 0, 0, 0.3) transparent transparent;
-  background: transparent;
-  &::before {
-    content: '';
-    border-style: solid;
-    border-width: 6px 8px 6px 0;
-    border-color: transparent rgba(255, 255, 255, 0.6) transparent transparent;
-    background: transparent;
-    z-index: -1;
-    display: inline-block;
-    position: absolute;
-    top: -0.5em;
-    left: 0.25em;
-  }
-}
-
-.relationHandleRight {
-  position: absolute;
-  margin-right: 19px;
-  border-style: solid;
-  border-width: 8px 0 8px 12px;
-  border-radius: 0px;
-  left: unset;
-  border-color: transparent transparent transparent rgba(0, 0, 0, 0.3);
-  background: transparent;
-  &::before {
-    content: '';
-    border-style: solid;
-    border-width: 6px 0 6px 8px;
-    border-color: transparent transparent transparent rgba(255, 255, 255, 0.6);
-    background: transparent;
-    z-index: -1;
-    display: inline-block;
-    position: absolute;
-    top: -0.5em;
-    right: 0.25em;
-  }
-}
-
-.relationHandleBottom {
-  border: 0;
-  border-radius: 0;
-  width: 8;
-  height: 8;
-  left: unset;
-  margin-bottom: 18;
-  margin-left: 40;
-  background: rgba(0, 0, 0, 0.3);
-  transform: rotate(-45deg);
-  transform-origin: center;
-  margin: 5px;
-  &::before {
-    content: '';
-    width: 6;
-    height: 6;
-    left: 1;
-    bottom: 1;
-    border: 0;
-    border-radius: 0;
-    background: rgba(255, 255, 255, 0.6);
-    z-index: -1;
-    display: inline-block;
-    position: fixed;
-  }
-}
-
-.relationDataWrapper {
-  margin-left: 80;
-}
-
-.relationSpan {
-  float: left;
-  margin-left: 5;
-}
-
-.relationInputHolder {
-  display: flex;
-  float: right;
-  margin-right: 20px;
-  margin-top: 4px;
-  margin-left: 5px;
-  max-width: 80px;
-  background-color: rgba(255, 255, 255, 0.6);
-  border-radius: 2px;
-  align-items: center;
-  max-height: 12px;
-}
-
-.relationInput {
-  z-index: 1;
-  cursor: text;
-  min-width: 0px;
-  max-width: 1.5ch;
-  border: none;
-  background: transparent;
-  text-align: center;
-  font-family: monospace;
-  font-weight: bold;
-  font-size: 11px;
-  color: #181520;
-  user-select: none;
-  font-style: italic;
-  &:focus {
-    outline: none;
-    user-select: none;
-  }
-  &::placeholder {
-    outline: none;
-    user-select: none;
-    font-style: italic;
-  }
-}
-
-.relationReadonly {
-  cursor: grab !important;
-  color: #181520 !important;
-  user-select: none;
-  font-style: normal !important;
-}
-
-.relationHandleFunction {
-  margin-left: 20;
-  margin-bottom: 18px !important;
-}
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
index 35171325cb14ef9d80de42d8470862b9a5895e47..0401aba54c913537dacd9065e52e27e0cf624f8b 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.stories.tsx
@@ -11,7 +11,7 @@ import {
   schemaSlice,
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
-import { RelationData } from '../../../graph/reactflow/model';
+import { RelationData } from '../../../model';
 
 const Component: Meta<typeof RelationPill> = {
   /* 👇 The title prop is optional.
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
index b1de1902c48a34371115cf853e98de7fffaf27e2..073f594c9fc1fce02dce04fde22a09ee55b93999 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx
@@ -4,8 +4,7 @@ import { useTheme } from '@mui/material';
 import { Handle, Position } from 'reactflow';
 
 import styles from './relationpill.module.scss';
-import { RelationNode } from '../../../graph/reactflow/model';
-import { Handles } from '../../../graph/reactflow/handles';
+import { RelationNode, Handles } from '../../../model';
 
 // export type RelationRFPillProps = {
 //   data: {
@@ -90,7 +89,7 @@ export const RelationPill = memo(({ data }: RelationNode) => {
       <div className={styles.relationWrapper}>
         <span
           className={styles.relationHandleFiller}
-          // style={{ transform: 'translate(-100px,0)' }}
+        // style={{ transform: 'translate(-100px,0)' }}
         >
           <Handle
             id={Handles.RelationLeft}
diff --git a/libs/shared/lib/querybuilder/query-utils/index.ts b/libs/shared/lib/querybuilder/query-utils/index.ts
index 2a8aba6d7a2ed7ca6ec5de1991d885d9995caab8..ca1c6a26a55c79bb17402270430f7c03ce3b1adf 100644
--- a/libs/shared/lib/querybuilder/query-utils/index.ts
+++ b/libs/shared/lib/querybuilder/query-utils/index.ts
@@ -1,2 +1,2 @@
 export * from './query-utils'
-export * from './BackendQueryFormat'
\ No newline at end of file
+export * from '../model/BackendQueryFormat'
\ No newline at end of file
diff --git a/libs/shared/lib/querybuilder/query-utils/query-utils.ts b/libs/shared/lib/querybuilder/query-utils/query-utils.ts
index b029d65bfadb28406a8d1b7c4f36f465377f62f2..df25d1293680e0198f9f1420f9784385b1632ad0 100644
--- a/libs/shared/lib/querybuilder/query-utils/query-utils.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query-utils.ts
@@ -1,14 +1,13 @@
-import { EntityNodeAttributes, RelationNodeAttributes } from "../graph/graphology/model";
-import { QueryMultiGraphExport } from "../graph/graphology/utils";
-import { Handles } from "../graph/reactflow/handles";
-import { EntityNode, FunctionNode, QueryElementTypes, RelationData, RelationNode, possibleTypes } from "../graph/reactflow/model";
-import { BackendQueryFormat } from "./BackendQueryFormat";
+import { EntityNodeAttributes, RelationNodeAttributes } from "../model/graphology/model";
+import { QueryMultiGraph } from "../model/graphology/utils";
+import { BackendQueryFormat } from "../model/BackendQueryFormat";
+import { Handles, QueryElementTypes } from "../model";
 
 /**
    * Converts the ReactFlow query to a json data structure to send the query to the backend.
    * @returns {BackendQueryFormat} A JSON object in the `JSONFormat`.
    */
-export function Query2BackendQuery(databaseName: string, graph: QueryMultiGraphExport): BackendQueryFormat {
+export function Query2BackendQuery(databaseName: string, graph: QueryMultiGraph): BackendQueryFormat {
     // dict of nodes per type
     const entities = graph.nodes.filter(n => n.attributes?.type === QueryElementTypes.Entity)
         .map((n) => ({ name: n.attributes!.name, ID: Number(n.key), constraints: [] }));
diff --git a/libs/shared/lib/schema/index.ts b/libs/shared/lib/schema/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44942b3cbedffeaada7753b2bbf552b2c326905b
--- /dev/null
+++ b/libs/shared/lib/schema/index.ts
@@ -0,0 +1,2 @@
+export * from './model'
+export * from '../querybuilder/model'
\ No newline at end of file
diff --git a/libs/shared/lib/model/backend/schema.ts b/libs/shared/lib/schema/model/FromBackend.ts
similarity index 61%
rename from libs/shared/lib/model/backend/schema.ts
rename to libs/shared/lib/schema/model/FromBackend.ts
index aaebb3346d5b595d5a79238aa5c6a9de18d87876..f2c6f701b5cfa43ec6897133b13c0959c930d9ea 100644
--- a/libs/shared/lib/model/backend/schema.ts
+++ b/libs/shared/lib/schema/model/FromBackend.ts
@@ -1,27 +1,31 @@
 /*************** schema format from the backend *************** */
 /** Schema type, consist of nodes and edges */
 export type SchemaFromBackend = {
-    edges: Edge[];
-    nodes: Node[];
+    edges: SchemaEdge[];
+    nodes: SchemaNode[];
 };
 
 /** Attribute type, consist of a name */
-export type Attribute = {
+export type SchemaAttribute = {
     name: string;
     type: 'string' | 'int' | 'bool' | 'float';
+    // nodeCount: number;
+    // summedNullAmount: number;
+    // connectedRatio: number;
+    // handles: string[];
 };
 
 /** Node type, consist of a name and a list of attributes */
-export type Node = {
+export type SchemaNode = {
     name: string;
-    attributes: Attribute[];
+    attributes: SchemaAttribute[];
 };
 
 /** Edge type, consist of a name, start point, end point and a list of attributes */
-export type Edge = {
+export type SchemaEdge = {
     name: string;
     to: string;
     from: string;
     collection: string;
-    attributes: Attribute[];
+    attributes: SchemaAttribute[];
 };
\ No newline at end of file
diff --git a/libs/shared/lib/schema/model/graphology.ts b/libs/shared/lib/schema/model/graphology.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e335f845a87d3ae5f810a46b50054cbdc3bffb47
--- /dev/null
+++ b/libs/shared/lib/schema/model/graphology.ts
@@ -0,0 +1,13 @@
+import { MultiGraph } from "graphology";
+import { Attributes as GAttributes, NodeEntry, EdgeEntry, SerializedGraph } from "graphology-types";
+import { SchemaAttribute, SchemaNode } from "./FromBackend";
+
+/** Attribute type, consist of a name */
+export type SchemaGraphologyNode = GAttributes & SchemaNode;
+export type SchemaGraphologyEdge = GAttributes;
+
+export type SchemaGraphologyNodeEntry = NodeEntry<SchemaGraphologyNode>;
+export type SchemaGraphologyEdgeEntry = EdgeEntry<SchemaGraphologyNode, SchemaGraphologyNode>;
+
+export class SchemaGraphology extends MultiGraph<SchemaGraphologyNode, SchemaGraphologyEdge, GAttributes> { };
+export type SchemaGraph = SerializedGraph<SchemaGraphologyNode, SchemaGraphologyEdge, GAttributes>;
diff --git a/libs/shared/lib/schema/model/index.ts b/libs/shared/lib/schema/model/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2439a89d0d0859a188f77420ea3b863daa845012
--- /dev/null
+++ b/libs/shared/lib/schema/model/index.ts
@@ -0,0 +1,3 @@
+export * from './FromBackend'
+export * from './graphology'
+export * from './reactflow'
\ No newline at end of file
diff --git a/libs/shared/lib/schema/schema-utils/Types.tsx b/libs/shared/lib/schema/model/reactflow.tsx
similarity index 94%
rename from libs/shared/lib/schema/schema-utils/Types.tsx
rename to libs/shared/lib/schema/model/reactflow.tsx
index c3fde663afe91eca2cb15a760db8bae77768d98f..71a4399d233582b2117948e69d5bfc530fd83d88 100644
--- a/libs/shared/lib/schema/schema-utils/Types.tsx
+++ b/libs/shared/lib/schema/model/reactflow.tsx
@@ -4,9 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 import { Edge, Node } from 'reactflow';
-import { Attributes } from '@graphpolaris/shared/lib/model';
-import { MultiGraph } from 'graphology';
-import { SerializedGraph } from 'graphology-types';
+import { SchemaGraphologyNode } from './graphology';
 
 /** All possible options of node-types */
 export enum NodeType {
@@ -33,7 +31,7 @@ export enum AttributeCategory {
 
 export interface SchemaGraphData {
   name: string;
-  attributes: Attributes[];
+  attributes: SchemaGraphologyNode[];
   nodeCount: number;
   summedNullAmount: number;
 }
@@ -136,7 +134,7 @@ export interface AttributeAnalyticsPopupMenuNode extends Node {
 
 /** Typing of the attributes which are stored in the popup menu's */
 export type AttributeWithData = {
-  attribute: Attributes;
+  attribute: SchemaGraphologyNode;
   category: AttributeCategory;
   nullAmount: number;
 };
diff --git a/libs/shared/lib/schema/panel/SchemaComponent.t b/libs/shared/lib/schema/panel/SchemaComponent.t
deleted file mode 100644
index b3f2a0a482d8429cd09e58e46ff9d27142f6aa3f..0000000000000000000000000000000000000000
--- a/libs/shared/lib/schema/panel/SchemaComponent.t
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * This program has been developed by students from the bachelor Computer Science at
- * Utrecht University within the Software Project course.
- * © Copyright Utrecht University (Department of Information and Computing Sciences)
- */
-
-/* istanbul ignore file */
-/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
- * We do not test components/renderfunctions/styling files.
- * See testing plan for more details.*/
-import React from 'react';
-import BaseView from '../BaseView';
-import SchemaViewModel from '../../view-model/graph-schema/SchemaViewModel';
-import ReactFlow, {
-  ControlButton,
-  Controls,
-  FlowElement,
-  ReactFlowProvider,
-} from 'react-flow-renderer';
-import { ClassNameMap, WithStyles, withStyles } from '@material-ui/styles';
-import SettingsIcon from '@material-ui/icons/Settings';
-import exportIcon from '../icons/ExportIcon.png';
-
-import { CircularProgress, ListItemText, Menu, MenuItem } from '@material-ui/core';
-import { useStyles } from './SchemaStyleSheet';
-
-export let currentColours: any;
-
-/** SchemaComponentProps is an interface containing the SchemaViewModel. */
-export interface SchemaComponentProps extends WithStyles<typeof useStyles> {
-  schemaViewModel: SchemaViewModel;
-  currentColours: any;
-}
-
-/** The interface for the state in the schema component */
-export interface SchemaComponentState {
-  zoom: number;
-  visible: boolean;
-  elements: FlowElement[];
-  myRef: React.RefObject<HTMLDivElement>;
-  // Elements holder for the export menu
-  exportMenuAnchor?: Element;
-}
-
-/** This class attaches the observers and renders the schema to the screen */
-class SchemaComponent
-  extends React.Component<SchemaComponentProps, SchemaComponentState>
-  implements BaseView
-{
-  private schemaViewModel: SchemaViewModel;
-  private currentColours: any;
-
-  public constructor(props: SchemaComponentProps) {
-    super(props);
-
-    const { schemaViewModel } = props;
-    this.schemaViewModel = schemaViewModel;
-
-    this.state = {
-      zoom: schemaViewModel.zoom,
-      myRef: schemaViewModel.myRef,
-      visible: schemaViewModel.visible,
-      elements: [
-        ...schemaViewModel.elements.nodes,
-        ...schemaViewModel.elements.edges,
-        ...schemaViewModel.elements.selfEdges,
-        schemaViewModel.nodeQualityPopup,
-        schemaViewModel.attributeAnalyticsPopupMenu,
-      ],
-      exportMenuAnchor: undefined,
-    };
-  }
-
-  /** Attach the Viewmodel Observer and Attach the Websocket Observer. */
-  public componentDidMount(): void {
-    this.schemaViewModel.subscribeToSchemaResult();
-    this.schemaViewModel.subscribeToAnalyticsData();
-    this.schemaViewModel.attachView(this);
-  }
-
-  /** Deattach the Viewmodel Observer and Attach the Websocket Observer. */
-  public componentWillUnmount(): void {
-    this.schemaViewModel.unSubscribeFromSchemaResult();
-    this.schemaViewModel.unSubscribeFromAnalyticsData();
-    this.schemaViewModel.detachView();
-  }
-
-  /**
-   * We update the state of our statecomponent on each update of the viewmodel.
-   * Gets the parameters from schemaviewmodel.
-   */
-  public onViewModelChanged(): void {
-    this.setState({
-      myRef: this.schemaViewModel.myRef,
-      zoom: this.schemaViewModel.zoom,
-      visible: this.schemaViewModel.visible,
-      elements: [
-        ...this.schemaViewModel.elements.nodes,
-        ...this.schemaViewModel.elements.edges,
-        ...this.schemaViewModel.elements.selfEdges,
-        this.schemaViewModel.nodeQualityPopup,
-        this.schemaViewModel.attributeAnalyticsPopupMenu,
-      ],
-    });
-  }
-
-  /** The function of the component that renders the schema */
-  public render(): JSX.Element {
-    const { zoom, elements, myRef, visible } = this.state;
-    const styles = this.props.classes;
-    currentColours = this.props.currentColours;
-
-    return (
-      <div
-        style={{
-          width: '100%',
-          height: '100%',
-          backgroundColor: '#' + currentColours.visBackground,
-        }}
-        ref={myRef}
-      >
-        <ReactFlowProvider>
-          <ReactFlow
-            className={styles.schemaPanel}
-            elements={elements}
-            defaultZoom={zoom}
-            nodeTypes={this.schemaViewModel.nodeTypes}
-            edgeTypes={this.schemaViewModel.edgeTypes}
-            nodesDraggable={false}
-            onlyRenderVisibleElements={false}
-            onLoad={this.schemaViewModel.onLoad}
-          >
-            <Controls
-              showInteractive={false}
-              showZoom={false}
-              showFitView={true}
-              className={styles.controls}
-            >
-              <ControlButton
-                className={styles.exportButton}
-                title={'Export graph schema'}
-                onClick={(event) => {
-                  event.stopPropagation();
-                  this.setState({
-                    ...this.state,
-                    exportMenuAnchor: event.currentTarget,
-                  });
-                }}
-              >
-                <img src={exportIcon} width={21}></img>
-              </ControlButton>
-            </Controls>
-            <div
-              style={{
-                display: 'flex',
-                justifyContent: 'center',
-                visibility: visible ? 'visible' : 'hidden',
-              }}
-            >
-              <CircularProgress />
-            </div>
-          </ReactFlow>
-        </ReactFlowProvider>
-        <Menu
-          getContentAnchorEl={null}
-          anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
-          transformOrigin={{ vertical: 'top', horizontal: 'right' }}
-          anchorEl={this.state.exportMenuAnchor}
-          keepMounted
-          open={Boolean(this.state.exportMenuAnchor)}
-          onClose={() => this.setState({ ...this.state, exportMenuAnchor: undefined })}
-        >
-          <MenuItem className={styles.menuText} onClick={() => this.schemaViewModel.exportToPNG()}>
-            {'Export to PNG'}
-          </MenuItem>
-          <MenuItem className={styles.menuText} onClick={() => this.schemaViewModel.exportToPDF()}>
-            {'Export to PDF'}
-          </MenuItem>
-        </Menu>
-      </div>
-    );
-  }
-}
-export default withStyles(useStyles)(SchemaComponent);
diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx
index 4c54a857b261ca95077385b10dc744d4987f9c7a..5e6261cd9cf914dee6c9c189d5e90a766a12786d 100644
--- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx
+++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx
@@ -18,9 +18,8 @@ import {
 } from '@graphpolaris/shared/lib/schema/schema-utils';
 import { useTheme } from '@mui/material';
 import {
-  SchemaGraphNode,
   SchemaGraphNodeWithFunctions,
-} from '../../../schema-utils/Types';
+} from '../../../model/reactflow';
 
 /**
  * EntityNode is the node that represents the database entities.
@@ -86,9 +85,8 @@ export const EntityNode = React.memo(
            ${theme.palette.custom.nodesBase} ${calculateAttributeQuality(
               data
             )}%, 
-           ${
-             theme.palette.custom.elements.entitySecond[0]
-           } ${calculateAttributeQuality(data)}%)`,
+           ${theme.palette.custom.elements.entitySecond[0]
+              } ${calculateAttributeQuality(data)}%)`,
           }}
         >
           <span
@@ -112,9 +110,8 @@ export const EntityNode = React.memo(
             background: `linear-gradient(-90deg, 
            ${theme.palette.custom.nodesBase} 0%, 
            ${theme.palette.custom.nodesBase} ${calculateEntityQuality(data)}%, 
-           ${
-             theme.palette.custom.elements.entitySecond[0]
-           } ${calculateEntityQuality(data)}%)`,
+           ${theme.palette.custom.elements.entitySecond[0]
+              } ${calculateEntityQuality(data)}%)`,
           }}
         >
           <span
diff --git a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.stories.tsx b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.stories.tsx
index 291a55b2897cd42455214f4957c48456d2eabc4a..3e12e835b106acc72e3816fb574403329b17b8ba 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.stories.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.stories.tsx
@@ -11,7 +11,7 @@ import {
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
 import { AttributeAnalyticsPopupMenu } from './attribute-analytics-popup-menu';
-import { AttributeCategory, NodeType } from '../../../schema-utils/Types';
+import { AttributeCategory, NodeType } from '../../../model/reactflow';
 
 const Component: Meta<typeof AttributeAnalyticsPopupMenu> = {
   /* 👇 The title prop is optional.
@@ -59,16 +59,16 @@ export const Default: Story = {
         },
       ],
       isAttributeDataIn: false,
-      onClickCloseButton: () => {},
-      onClickPlaceInQueryBuilderButton: (name: string, type) => {},
-      searchForAttributes: (id: string, searchbarValue: string) => {},
-      resetAttributeFilters: (id: string) => {},
+      onClickCloseButton: () => { },
+      onClickPlaceInQueryBuilderButton: (name: string, type) => { },
+      searchForAttributes: (id: string, searchbarValue: string) => { },
+      resetAttributeFilters: (id: string) => { },
       applyAttributeFilters: (
         id: string,
         category: AttributeCategory,
         predicate: string,
         percentage: number
-      ) => {},
+      ) => { },
     },
   },
 };
diff --git a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx
index 489c172675c7310f202c03f187fb6292c1e9eea6..bc21aacc0fd2cb2a6d14916c63ff91f43a972d24 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx
@@ -23,7 +23,7 @@ import {
   AttributeAnalyticsData,
   AttributeWithData,
   NodeType,
-} from '../../../schema-utils/Types';
+} from '../../../model/reactflow';
 import { NodeProps } from 'reactflow';
 import './attribute-analytics-popup-menu.module.scss';
 
diff --git a/libs/shared/lib/schema/pills/nodes/popup/node-quality-entity-popup.tsx b/libs/shared/lib/schema/pills/nodes/popup/node-quality-entity-popup.tsx
index aaefe3ce98eaa1f18d3e21d1ae74e9bf1ad31834..609226cbc1b9532a3d96053409cd05707fb47a02 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/node-quality-entity-popup.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/node-quality-entity-popup.tsx
@@ -12,7 +12,7 @@
 import { ButtonBase, useTheme } from '@mui/material';
 import React from 'react';
 import { NodeProps } from 'reactflow';
-import { NodeQualityDataForEntities } from '../../../schema-utils/Types';
+import { NodeQualityDataForEntities } from '../../../model/reactflow';
 
 /**
  * NodeQualityEntityPopupNode is the node that represents the popup that shows the node quality for an entity
diff --git a/libs/shared/lib/schema/pills/nodes/popup/node-quality-relation-popup.tsx b/libs/shared/lib/schema/pills/nodes/popup/node-quality-relation-popup.tsx
index bd1d1a8b4a9a9602b6b182ddbd00db63ec4fb123..4a2307723af8c4906497acd7252246b58df3b9e3 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/node-quality-relation-popup.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/node-quality-relation-popup.tsx
@@ -12,7 +12,7 @@
 import { ButtonBase, useTheme } from '@mui/material';
 import React from 'react';
 import { NodeProps } from 'reactflow';
-import { NodeQualityDataForRelations } from '../../../schema-utils/Types';
+import { NodeQualityDataForRelations } from '../../../model/reactflow';
 import './node-quality-popup.module.scss';
 
 /**
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.stories.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.stories.tsx
index 9de60b14cdf8232bdb0c69c53c4b9d84ba2a66ea..e265579a36ca12d3aa828a03858629d5898679b1 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.stories.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.stories.tsx
@@ -11,7 +11,7 @@ import {
 } from '@graphpolaris/shared/lib/data-access/store';
 import { ReactFlowProvider } from 'reactflow';
 import { AttributeAnalyticsPopupMenu } from './attribute-analytics-popup-menu';
-import { AttributeCategory, NodeType } from '../../../../schema-utils/Types';
+import { AttributeCategory, NodeType } from '../../../../model/reactflow';
 
 const Component: Meta<typeof AttributeAnalyticsPopupMenu> = {
   /* 👇 The title prop is optional.
@@ -59,16 +59,16 @@ export const Default: Story = {
         },
       ],
       isAttributeDataIn: false,
-      onClickCloseButton: () => {},
-      onClickPlaceInQueryBuilderButton: (name: string, type) => {},
-      searchForAttributes: (id: string, searchbarValue: string) => {},
-      resetAttributeFilters: (id: string) => {},
+      onClickCloseButton: () => { },
+      onClickPlaceInQueryBuilderButton: (name: string, type) => { },
+      searchForAttributes: (id: string, searchbarValue: string) => { },
+      resetAttributeFilters: (id: string) => { },
       applyAttributeFilters: (
         id: string,
         category: AttributeCategory,
         predicate: string,
         percentage: number
-      ) => {},
+      ) => { },
     },
   },
 };
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx
index 81ff94097d27bd97b2de5653cb4b2b267bc6f7b5..8ba0bd7000f5aef7188999406366f02a4d9dbb92 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx
@@ -22,7 +22,7 @@ import {
   NodeType,
   AttributeAnalyticsData,
   AttributeCategory,
-} from '../../../../schema-utils/Types';
+} from '../../../../model/reactflow';
 import { Filter } from './filterbar';
 import { Search } from './searchbar';
 import { NodeProps } from 'reactflow';
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/filterbar.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/filterbar.tsx
index 868e5607edde21ecfb7d2bac0d873348d720f0c1..2c044fdc1829dc817afa8d31825a94a114a61a9a 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/filterbar.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/filterbar.tsx
@@ -22,7 +22,7 @@ import {
   AttributeAnalyticsData,
   AttributeCategory,
   NodeType,
-} from '../../../../schema-utils/Types';
+} from '../../../../model/reactflow';
 
 /** The typing for the props of the filter bar. */
 type FilterbarProps = {
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.tsx
index c609a3b8fe19a404df79706f48f0eb0eb101837c..1e9b666e6543571e668e69c929e9689143065976 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-entity-popup.tsx
@@ -12,7 +12,7 @@
 import { ButtonBase, useTheme } from '@mui/material';
 import React from 'react';
 import { NodeProps } from 'reactflow';
-import { NodeQualityDataForEntities } from '../../../../schema-utils/Types';
+import { NodeQualityDataForEntities } from '../../../../model/reactflow';
 
 /**
  * NodeQualityEntityPopupNode is the node that represents the popup that shows the node quality for an entity
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-relation-popup.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-relation-popup.tsx
index f675545e4a60085c4672141d10378ff4bd406031..7f99a7af14f980b8f9fcf1bf58707a149a1e8ff3 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-relation-popup.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/node-quality-relation-popup.tsx
@@ -15,7 +15,7 @@ import { NodeProps } from 'reactflow';
 import {
   NodeQualityDataForRelations,
   NodeType,
-} from '../../../../schema-utils/Types';
+} from '../../../../model/reactflow';
 
 /**
  * NodeQualityRelationPopupNode is the node that represents the popup that shows the node quality for a relation
diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/searchbar.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/searchbar.tsx
index ba50b8495b4030877d69dec9896652635116d0de..4b5cf9cd5957ddc6ecaf7bc6009595df072396d9 100644
--- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/searchbar.tsx
+++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/searchbar.tsx
@@ -10,7 +10,7 @@
  * See testing plan for more details.*/
 
 import React, { ReactElement, useState } from 'react';
-import { AttributeAnalyticsData } from '../../../../schema-utils/Types';
+import { AttributeAnalyticsData } from '../../../../model/reactflow';
 
 /** The typing for the props of the searchbar. */
 type SearchbarProps = {
diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx
index ca4f7f16d26d4e0f1a83ca064e425bd4eec84227..e209a5fd9f4e8b336e855fc8c5701d2aed3dc735 100644
--- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx
+++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx
@@ -22,7 +22,7 @@ import { useTheme } from '@mui/material';
 import {
   SchemaGraphRelation,
   SchemaGraphRelationWithFunctions,
-} from '../../../schema-utils/Types';
+} from '../../../model/reactflow';
 
 /**
  * Relation node component that renders a relation node for the schema.
@@ -76,7 +76,7 @@ export const RelationNode = React.memo(
       <div
         onDragStart={(event) => onDragStart(event)}
         draggable
-        // style={{ width: 100, height: 100 }}
+      // style={{ width: 100, height: 100 }}
       >
         <div
           className={styles.relationNode}
@@ -140,9 +140,8 @@ export const RelationNode = React.memo(
               ${theme.palette.custom.nodesBase} ${calculateAttributeQuality(
                 data
               )}%, 
-              ${
-                theme.palette.custom.elements.relationSecond[0]
-              } ${calculateAttributeQuality(data)}%)`,
+              ${theme.palette.custom.elements.relationSecond[0]
+                } ${calculateAttributeQuality(data)}%)`,
             }}
           >
             <span
@@ -165,9 +164,8 @@ export const RelationNode = React.memo(
               ${theme.palette.custom.nodesBase} ${calculateRelationQuality(
                 data
               )}%, 
-              ${
-                theme.palette.custom.elements.relationSecond[0]
-              } ${calculateRelationQuality(data)}%)`,
+              ${theme.palette.custom.elements.relationSecond[0]
+                } ${calculateRelationQuality(data)}%)`,
             }}
           >
             <span
diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases-edge-deleteme.ts b/libs/shared/lib/schema/schema-utils/schema-usecases-edge-deleteme.ts
deleted file mode 100644
index 08bdb1e639347f417f87dc74580c2a760792dc2e..0000000000000000000000000000000000000000
--- a/libs/shared/lib/schema/schema-utils/schema-usecases-edge-deleteme.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-import { SchemaElements } from '../../model';
-import { Attributes } from 'graphology-types';
-import { getConnectedEdges, Node, Edge } from 'reactflow';
-
-
-/** This class is responsible for creating and rendering the edges of the schema */
-export default class EdgeUseCase {
-    private counterOutgoingEdges: number;
-    private counterIncomingEdges: number;
-    private distanceCounterNormalEdges = 40;
-    private initialOffsetOfNodes = 155 - this.distanceCounterNormalEdges;
-    private distanceCounterSelfEdges = 58;
-    private setRelationNodePosition:
-        | ((
-            centerX: number,
-            centerY: number,
-            id: string,
-            from: string,
-            to: string,
-            attributes: Attributes[],
-        ) => void)
-        | undefined;
-
-    public constructor() {
-        this.counterOutgoingEdges = 1;
-        this.counterIncomingEdges = 1;
-        this.setRelationNodePosition = undefined;
-    }
-
-    /**
-     * Use the drawOrder to draw the edges of the nodes starting with the top node.
-     * We check all connected edges of a node and create lists of incoming and outgoing edges.
-     * These lists are used in renderEdges to draw the edges for the current node.
-     * @param drawOrder The order in which the nodes will be drawn.
-     * @param setRelationNodePosition The function an edge needs to create its relation node.
-     * @returns {Edge[]} The list of edges that will be rendered.
-     */
-    public positionEdges = (
-        drawOrder: Node[],
-        elements: SchemaElements,
-        setRelationNodePosition: (
-            centerX: number,
-            centerY: number,
-            id: string,
-            from: string,
-            to: string,
-            attributes: Attributes[],
-        ) => void,
-    ): Edge[] => {
-        let incomingEdges: Edge[];
-        let outgoingEdges: Edge[];
-        let edges: Edge[];
-        let used: Edge[];
-        let result: Edge[];
-        let drawOrderID: string[];
-        used = [];
-        result = [];
-
-        this.counterOutgoingEdges = 1;
-        this.counterIncomingEdges = 1;
-        this.setRelationNodePosition = setRelationNodePosition;
-        drawOrderID = this.convertDrawOrder(drawOrder);
-        for (let i = 0; i < drawOrder.length; i++) {
-            incomingEdges = [];
-            outgoingEdges = [];
-            edges = getConnectedEdges([drawOrder[i]], elements.edges);
-            edges.forEach((edge) => {
-                if (!used.includes(edge)) {
-                    if (edge.source == drawOrder[i].id) {
-                        outgoingEdges.push(edge);
-                        used.push(edge);
-                    } else {
-                        incomingEdges.push(edge);
-                        used.push(edge);
-                    }
-                }
-            });
-
-            result = [
-                ...result,
-                ...this.renderEdges(
-                    this.sortEdgeArray(incomingEdges, drawOrderID, true),
-                    this.sortEdgeArray(outgoingEdges, drawOrderID, false),
-                    drawOrderID,
-                    drawOrder,
-                    i,
-                ),
-            ];
-        }
-        if (elements.selfEdges.length > 0)
-            return [...result, ...this.renderSelfEdges(elements.selfEdges, drawOrder, drawOrderID)];
-
-        return result;
-    };
-
-    /**
-     * Goes over the list of edges to position them in the schema.
-     * First goes through all incoming edges and then through the outgoing edges
-     * @param incomingEdges The incoming edges of the node.
-     * @param outgoingEdges The outgoing edges of the node.
-     * @param drawOrderID The converted drawOrder that only contains the id of the node.
-     * @param drawOrder The order in which the nodes will be drawn.
-     * @param currentNode Index of the Node in the drawOrder array.
-     * @returns {Edge[]} Draws the edge list for the rendering of the schema.
-     */
-    public renderEdges = (
-        incomingEdges: Edge[],
-        outgoingEdges: Edge[],
-        drawOrderID: string[],
-        drawOrder: Node[],
-        currentNode: number,
-    ): Edge[] => {
-        for (let i = 0; i < incomingEdges.length; i++) {
-            incomingEdges = this.renderIncomingEdges(
-                incomingEdges,
-                drawOrderID,
-                drawOrder,
-                currentNode,
-                i,
-            );
-        }
-
-        for (let i = 0; i < outgoingEdges.length; i++) {
-            outgoingEdges = this.renderOutgoingEdges(
-                outgoingEdges,
-                drawOrderID,
-                drawOrder,
-                currentNode,
-                i,
-            );
-        }
-        return [...incomingEdges, ...outgoingEdges];
-    };
-
-    /**
-     * Draw the incoming edges between the nodes in the correct positions for the currentNode.
-     * We use the currentNode and draw all the incoming edges of this node.
-     * Nodes directly beneath are drawn with an edge from the bottom to the top handle.
-     * For edges that go to nodes further away the right handle is used for incoming edges.
-     * @param incomingEdges The incoming edges of the node.
-     * @param drawOrderID The converted drawOrder that only contains the id of the node.
-     * @param drawOrder The order in which the nodes will be drawn.
-     * @param currentNode Index of the Node in the drawOrder array.
-     * @param i Index of the edge in the edge list.
-     * @returns {Edge[]} Draws the edge list for the incoming edges.
-     */
-    public renderIncomingEdges = (
-        incomingEdges: Edge[],
-        drawOrderID: string[],
-        drawOrder: Node[],
-        currentNode: number,
-        index: number,
-    ): Edge[] => {
-        if (incomingEdges[index].source == drawOrderID[currentNode + 1] && index == 0) {
-            // Draw nodes that are directly next to eachother with an edge from the top to the bottom
-            incomingEdges[0].sourceHandle = 'entitySourceTop';
-            incomingEdges[0].targetHandle = 'entityTargetBottom';
-
-            // Add the handle id to the data of the specified node in order to display the correct handles
-            drawOrder[drawOrderID.indexOf(incomingEdges[index].source)].data.handles.push(
-                'entitySourceTop',
-            );
-            drawOrder[drawOrderID.indexOf(incomingEdges[index].target)].data.handles.push(
-                'entityTargetBottom',
-            );
-
-            incomingEdges[index].data.setRelationNodePosition = this.setRelationNodePosition;
-
-            incomingEdges[index].data.d = 0;
-        } else {
-            // Draw nodes that are not directly next to eachother with an edge from the right handle
-            incomingEdges[index].sourceHandle = 'entitySourceRight';
-            incomingEdges[index].targetHandle = 'entityTargetRight';
-
-            // Add the handle id to the data of the specified node in order to display the correct handles
-            drawOrder[drawOrderID.indexOf(incomingEdges[index].source)].data.handles.push(
-                'entitySourceRight',
-            );
-            drawOrder[drawOrderID.indexOf(incomingEdges[index].target)].data.handles.push(
-                'entityTargetRight',
-            );
-
-            // Create distance between the links with a counter
-            incomingEdges[index].data.d =
-                this.counterIncomingEdges * this.distanceCounterNormalEdges + this.initialOffsetOfNodes;
-
-            incomingEdges[index].data.setRelationNodePosition = this.setRelationNodePosition;
-
-            this.counterIncomingEdges++;
-        }
-        return incomingEdges;
-    };
-
-    /**
-     * Draw the outgoing edges between the nodes in the correct positions for the currentNode.
-     * We use the currentNode and draw all the outgoing edges of this node.
-     * Nodes directly beneath are drawn with an edge from the bottom to the top handle.
-     * For edges that go to nodes further away the right handle is used for outgoing edges.
-     * @param outgoingEdges The outgoing edges of the node.
-     * @param drawOrderID The converted drawOrder that only contains the id of the node.
-     * @param drawOrder The order in which the nodes will be drawn.
-     * @param currentNode Index of the Node in the drawOrder array.
-     * @param index Index of the edge in the edge list.
-     * @returns {Edge[]} Draws the edge list for the outgoing edges.
-     */
-    public renderOutgoingEdges = (
-        outgoingEdges: Edge[],
-        drawOrderID: string[],
-        drawOrder: Node[],
-        currentNode: number,
-        index: number,
-    ): Edge[] => {
-        if (outgoingEdges[index].target == drawOrderID[currentNode + 1] && index == 0) {
-            // Draw nodes that are directly next to eachother with a link from the top to the bottom
-            outgoingEdges[0].sourceHandle = 'entitySourceBottom';
-            outgoingEdges[0].targetHandle = 'entityTargetTop';
-
-            // Add the handle id to the data of the specified node in order to display the correct handles
-            drawOrder[drawOrderID.indexOf(outgoingEdges[index].source)].data.handles.push(
-                'entitySourceBottom',
-            );
-            drawOrder[drawOrderID.indexOf(outgoingEdges[index].target)].data.handles.push(
-                'entityTargetTop',
-            );
-
-            outgoingEdges[index].data.setRelationNodePosition = this.setRelationNodePosition;
-
-            outgoingEdges[index].data.d = 0;
-        } else {
-            outgoingEdges[index].sourceHandle = 'entitySourceLeft';
-            outgoingEdges[index].targetHandle = 'entityTargetLeft';
-
-            // Add the handle id to the data of the specified node in order to display the correct handle
-            drawOrder[drawOrderID.indexOf(outgoingEdges[index].source)].data.handles.push(
-                'entitySourceLeft',
-            );
-            drawOrder[drawOrderID.indexOf(outgoingEdges[index].target)].data.handles.push(
-                'entityTargetLeft',
-            );
-
-            // Create distance between the links with a counter
-            outgoingEdges[index].data.d =
-                -this.counterOutgoingEdges * this.distanceCounterNormalEdges - this.initialOffsetOfNodes;
-
-            outgoingEdges[index].data.setRelationNodePosition = this.setRelationNodePosition;
-
-            this.counterOutgoingEdges++;
-        }
-        return outgoingEdges;
-    };
-
-    /**
-     * Sort edges depending on if they are incoming or outgoing edges.
-     * @param input List of edges to sort.
-     * @param drawOrderID Draw order by id to sort the list with.
-     * @param source Boolean that contains if the edge is a source or target.
-     * @returns {Edge[]} List of sorted edges.
-     *
-     */
-    public sortEdgeArray(input: Edge[], drawOrderID: string[], source: boolean): Edge[] {
-        let output: Edge[];
-        output = [];
-        input.forEach((edge) => {
-            if (source) output[drawOrderID.indexOf(edge.source)] = edge;
-            else output[drawOrderID.indexOf(edge.target)] = edge;
-        });
-
-        output = output.filter(function (element) {
-            return element != null;
-        });
-        return output;
-    }
-
-    /**
-     * Edges only know the id of the nodes, so we need to convert the nodes in the drawOrder to a list of their id's.
-     * This way the edges can reference nodes from this list.
-     * @param drawOrder The order in which the nodes will be drawn.
-     * @returns {string[]} List of orderd node id's.
-     */
-    private convertDrawOrder(drawOrder: Node[]): string[] {
-        let orderedNodeIDs: string[] = [];
-        drawOrder.forEach((node) => {
-            orderedNodeIDs.push(node.id);
-        });
-        return orderedNodeIDs;
-    }
-
-    /** This creates the self-edges */
-    private renderSelfEdges(selfEdges: Edge[], drawOrder: Node[], drawOrderID: string[]): Edge[] {
-        selfEdges.forEach((edge) => {
-            edge.sourceHandle = 'entitySourceLeft';
-            edge.targetHandle = 'entityTargetRight';
-
-            // Add the handle id to the data of the specified node in order to display the correct handles
-            drawOrder[drawOrderID.indexOf(edge.source)].data.handles.push('entitySourceLeft');
-            drawOrder[drawOrderID.indexOf(edge.target)].data.handles.push('entityTargetRight');
-
-            // Create distance between the links with a counter
-            edge.data.d = this.distanceCounterSelfEdges;
-
-            edge.data.setRelationNodePosition = this.setRelationNodePosition;
-        });
-        return selfEdges;
-    }
-}
diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts b/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts
index bdc7c4122d044041469650d34ff3f65b880ec7d0..72c9a59cfa7f05e4718eb40c197604b925a24c26 100644
--- a/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts
+++ b/libs/shared/lib/schema/schema-utils/schema-usecases.spec.ts
@@ -9,6 +9,7 @@ import {
   simpleSchemaRaw,
   twitterSchemaRaw,
 } from '@graphpolaris/shared/lib/mock-data';
+import { SchemaGraphology } from "../model";
 
 describe('SchemaUsecases', () => {
   test.each([
@@ -29,9 +30,9 @@ describe('SchemaUsecases', () => {
     const parsed = SchemaUtils.schemaBackend2Graphology(
       simpleSchemaRaw as SchemaFromBackend
     );
-    const reload = MultiGraph.from(parsed.export());
+    const reload = SchemaGraphology.from(parsed.export());
 
-    expect(parsed).toStrictEqual(reload);
+    expect(parsed).toEqual(reload);
   });
 
   test.each([
diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases.ts b/libs/shared/lib/schema/schema-utils/schema-usecases.ts
index ccd647e2c6aa0c09ca1bd8588747fa3d6fd3eb7c..09b7ed3a78a8031c3753df3d8b219ab91fe74a2b 100644
--- a/libs/shared/lib/schema/schema-utils/schema-usecases.ts
+++ b/libs/shared/lib/schema/schema-utils/schema-usecases.ts
@@ -1,8 +1,8 @@
-import { SchemaGraphNodeWithFunctions, SchemaGraphRelation, SchemaGraphRelationWithFunctions } from './Types';
+import { SchemaGraphNodeWithFunctions, SchemaGraphRelation, SchemaGraphRelationWithFunctions } from '../model/reactflow';
 import Graph from 'graphology';
 import { Attributes } from 'graphology-types';
 import { MarkerType, Edge, Node } from 'reactflow';
-import { SchemaGraphNode } from '../schema-utils/Types';
+import { SchemaGraphNode } from '../model/reactflow';
 
 //TODO does not belong here; maybe should go into the GraphPolarisThemeProvider
 const ANIMATEDEDGES = false;
diff --git a/libs/shared/lib/schema/schema-utils/schema-utils.ts b/libs/shared/lib/schema/schema-utils/schema-utils.ts
index d58124b57459254a3cc21d1c7c5df49dabd3aafc..e0cd75dafaad05ad7efa4b12d8af982a75683183 100644
--- a/libs/shared/lib/schema/schema-utils/schema-utils.ts
+++ b/libs/shared/lib/schema/schema-utils/schema-utils.ts
@@ -1,42 +1,42 @@
-import { SchemaFromBackend, Node, Edge } from '@graphpolaris/shared/lib/model/backend';
-import Graph, { MultiGraph } from 'graphology';
-import { Attributes } from 'graphology-types';
+import { SchemaAttribute, SchemaFromBackend, SchemaGraphology, SchemaGraphologyNode } from '../model';
 
 export class SchemaUtils {
   public static schemaBackend2Graphology(
     schemaFromBackend: SchemaFromBackend
-  ): Graph {
+  ): SchemaGraphology {
     const { nodes, edges } = schemaFromBackend;
     // Instantiate a directed graph that allows self loops and parallel edges
-    const schemaGraph = new MultiGraph({ allowSelfLoops: true });
+    const schemaGraphology = new SchemaGraphology({ 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, {
+
+    nodes.forEach((node) => {
+      const attributes: SchemaGraphologyNode = {
         ...node,
         name: node.name,
         nodeCount: 0,
         summedNullAmount: 0,
         connectedRatio: 1,
         handles: ['entitySourceLeft', 'entityTargetLeft'],
-        attributes: {
+        attributes: [
           ...node.attributes,
-        },
+        ],
         x: 0,
         y: 0,
-      });
+      }
+      schemaGraphology.addNode(node.name, attributes);
       // console.log(node, schemaGraph.nodes());
     });
 
 
 
     // The name of the edge will be name + from + to, since edge names are not unique
-    edges.forEach((edge: Edge) => {
+    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, {
+      schemaGraphology.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, {
         nodeCount: 0,
         summedNullAmount: 0,
         fromRatio: 0,
@@ -48,7 +48,7 @@ export class SchemaUtils {
         attributes: edge.attributes,
       });
     });
-    return schemaGraph;
+    return schemaGraphology;
   }
 }
 
diff --git a/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx b/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx
index 6d5ab96ab6c2169e938ee2e45a518881b9af0fe1..8f72e455e4ce918257c2b2286552a67170cfb714 100644
--- a/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx
+++ b/libs/shared/lib/vis/nodelink/NodeLinkViewModel.tsx
@@ -10,7 +10,7 @@ import * as d3 from 'd3';
 import { jsPDF } from 'jspdf';
 import ResultNodeLinkParserUseCase, {
   isNodeLinkResult,
-} from './ResultNodeLinkParserUseCase';
+} from '../shared/ResultNodeLinkParserUseCase';
 import { AttributeData, NodeAttributeData } from '../shared/InputDataTypes';
 import { AttributeCategory } from '../shared/Types';
 import { GraphQueryResult } from '../../data-access/store'; // TODO remove
diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
index 358b43873befe1ca23101975c318e2b7901a365c..09e3295b230b3aab4a2c7c24ff0f735105b7e7bb 100644
--- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
@@ -11,7 +11,7 @@ import styles from './nodelinkvis.module.scss';
 import * as PIXI from 'pixi.js';
 import { GraphType, LinkType, NodeType } from './Types';
 import NodeLinkViewModel from './NodeLinkViewModel';
-import ResultNodeLinkParserUseCase from './ResultNodeLinkParserUseCase';
+import ResultNodeLinkParserUseCase from '../shared/ResultNodeLinkParserUseCase';
 import VisConfigPanelComponent from '../shared/VisConfigPanel/VisConfigPanel';
 import NodeLinkConfigPanelComponent from './config-panel/NodeConfigPanelComponent';
 import { use } from 'cytoscape';
diff --git a/libs/shared/lib/vis/paohvis/Types.tsx b/libs/shared/lib/vis/paohvis/Types.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2192b5612ed1b71b71451c966dd86d053ddcada2
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/Types.tsx
@@ -0,0 +1,131 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/** The data that is needed to make the complete Paohvis table. */
+export type PaohvisData = {
+  rowLabels: string[];
+  hyperEdgeRanges: HyperEdgeRange[];
+  maxRowLabelWidth: number;
+};
+
+/** The ranges of the hyperEdges consisting of the name of the range with all adequate hyperEdges. */
+export type HyperEdgeRange = {
+  rangeText: string;
+  hyperEdges: HyperEdgeI[];
+};
+
+/** The index and name of a hyperEdge. */
+export type HyperEdgeI = {
+  indices: number[];
+  frequencies: number[];
+  nameToShow: string;
+};
+
+/** All information for the axes. This is used in the parser */
+export type PaohvisAxisInfo = {
+  selectedAttribute: Attribute;
+  relation: Relation;
+  isYAxisEntityEqualToRelationFrom: boolean;
+};
+
+/** The type of an Attribute. This is used to make the HyperEdgeRanges in the parser. */
+export type Attribute = {
+  name: string;
+  type: ValueType;
+  origin: AttributeOrigin;
+};
+
+/** The type of the value of an attribute. This is primarily used to make the HyperEdgeRanges in the parser. */
+export enum ValueType {
+  text = 'text',
+  bool = 'bool',
+  number = 'number',
+  noAttribute = 'No attribute',
+}
+
+/** Attributes can come from entities or relations. This is primarily used to make the HyperEdgeRanges in the parser. */
+export enum AttributeOrigin {
+  relation = 'Relation',
+  entity = 'Entity',
+  noAttribute = 'No attribute',
+}
+
+/** The type of a relation.  This is primarily used to make the HyperEdges in the parser. */
+export type Relation = {
+  collection: string;
+  from: string;
+  to: string;
+};
+
+/** All information from the nodes. This is used in the parser. */
+export type PaohvisNodeInfo = {
+  // The rowlabels (ids from the nodes on the y-axis).
+  rowLabels: string[];
+  // Dictionary for finding the index of a row.
+  yNodesIndexDict: Record<string, number>;
+  // Dictionary for finding the attributes that belong to the nodes on the x-axis.
+  xNodesAttributesDict: Record<string, any>;
+};
+
+/** The entities with names and attribute parameters from the schema. */
+export type EntitiesFromSchema = {
+  entityNames: string[];
+  attributesPerEntity: Record<string, AttributeNames>;
+  relationsPerEntity: Record<string, string[]>;
+};
+
+/** The relations with (collection-)names and attribute parameters from the schema. */
+export type RelationsFromSchema = {
+  relationCollection: string[];
+  relationNames: Record<string, string>;
+  attributesPerRelation: Record<string, AttributeNames>;
+};
+
+/** The names of attributes per datatype. */
+export type AttributeNames = {
+  textAttributeNames: string[];
+  numberAttributeNames: string[];
+  boolAttributeNames: string[];
+};
+
+/** The options to order the nodes. */
+export enum NodeOrder {
+  alphabetical = 'Alphabetical',
+  degree = 'Degree (amount of hyperedges)',
+}
+
+/** All information on the ordering of PAOHvis. */
+export type PaohvisNodeOrder = {
+  orderBy: NodeOrder;
+  isReverseOrder: boolean;
+};
+
+/** All PAOHvis filters grouped by the filters on nodes and edges. */
+export type PaohvisFilters = {
+  nodeFilters: FilterInfo[];
+  edgeFilters: FilterInfo[];
+};
+
+/** The information of one filter on PAOHvis. */
+export type FilterInfo = {
+  targetGroup: string;
+  attributeName: string;
+  value: any;
+  predicateName: string;
+};
+
+/** The options where you can filter on. */
+export enum FilterType {
+  xaxis = 'X-axis',
+  yaxis = 'Y-axis',
+  edge = 'Edges',
+}
+
+/** Entities can come from the 'from' or the 'to' part of a relation. */
+export enum EntityOrigin {
+  from = 'From',
+  to = 'To',
+}
diff --git a/libs/shared/lib/vis/paohvis/components/HyperEdgesRange.tsx b/libs/shared/lib/vis/paohvis/components/HyperEdgesRange.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6d46ce235d1741dba73b77835f872b0f6e8ceefb
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/HyperEdgesRange.tsx
@@ -0,0 +1,159 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import { select } from 'd3';
+import React, { useEffect, useRef } from 'react';
+// import style from './PaohvisComponent.module.scss';
+import { HyperEdgeI } from '../Types';
+
+type HyperEdgeRangeProps = {
+  index: number;
+  text: string;
+  hyperEdges: HyperEdgeI[];
+  nRows: number;
+  colOffset: number;
+  xOffset: number;
+  yOffset: number;
+  rowHeight: number;
+  hyperedgeColumnWidth: number;
+  gapBetweenRanges: number;
+
+  onMouseEnter: (col: number, nameToShow: string) => void;
+  onMouseLeave: (col: number) => void;
+};
+export const HyperEdgeRange = (props: HyperEdgeRangeProps) => {
+  // calculate the width of the hyperEdgeRange.
+  const rectWidth = props.hyperEdges.length * props.hyperedgeColumnWidth + 2 * props.gapBetweenRanges;
+
+  const rows: JSX.Element[] = [];
+  for (let i = 0; i < props.nRows; i++)
+    rows.push(
+      <g key={i} className={'row-' + i}>
+        <rect
+          key={i.toString()}
+          y={i * props.rowHeight}
+          width={rectWidth}
+          height={props.rowHeight}
+          fill={i % 2 === 0 ? '#e6e6e6' : '#f5f5f5'}
+          style={{ zIndex: -1 }}
+        />
+      </g>,
+    );
+
+  let hyperedges = props.hyperEdges.map((hyperEdge, i) => (
+    <HyperEdge
+      key={i}
+      col={i + props.colOffset}
+      connectedRows={hyperEdge.indices}
+      numbersOnRows={hyperEdge.frequencies}
+      nameToShow={hyperEdge.nameToShow}
+      rowHeight={props.rowHeight}
+      xOffset={
+        props.hyperedgeColumnWidth / 2 +
+        i * props.hyperedgeColumnWidth +
+        props.gapBetweenRanges
+      }
+      radius={8}
+      onMouseEnter={props.onMouseEnter}
+      onMouseLeave={props.onMouseLeave}
+    />
+  ));
+
+  // * 3 because we have a gapBetweenRanges as padding
+  const xOffset =
+    props.xOffset +
+    (props.index * 3 + 1) * props.gapBetweenRanges +
+    props.colOffset * props.hyperedgeColumnWidth;
+
+  return (
+    <g transform={'translate(' + xOffset + ', +' + props.yOffset + ')'}>
+      <text
+        textDecoration={'underline'}
+        x="10"
+        y={props.rowHeight / 2}
+        dy=".35em"
+        style={{ fontWeight: 600, transform: 'translate3d(-10px, 15px, 0px) rotate(-30deg)' }}
+      >
+        {props.text}
+      </text>
+      <g transform={'translate(0,' + props.rowHeight + ')'}>
+        {rows}
+        {hyperedges}
+      </g>
+    </g>
+  );
+}
+
+type HyperEdgeProps = {
+  col: number;
+  connectedRows: number[];
+  numbersOnRows: number[];
+  rowHeight: number;
+  xOffset: number;
+  radius: number;
+  nameToShow: string;
+  onMouseEnter: (col: number, nameToShow: string) => void;
+  onMouseLeave: (col: number) => void;
+};
+
+const HyperEdge = (props: HyperEdgeProps) => {
+  const ref = useRef<SVGGElement>(null);
+
+  useEffect(() => {
+    if (ref.current) {
+      /** Adds mouse events when the user hovers over and out of a hyperedge. */
+      select(ref.current)
+        .on('mouseover', () => props.onMouseEnter(props.col, props.nameToShow))
+        .on('mouseout', () => props.onMouseLeave(props.col));
+    }
+  }, [ref.current])
+
+
+  // render all circles on the correct position.
+  const circles = props.connectedRows.map((row, i) => {
+    return (
+      <g key={'row' + row + ' col' + i}>
+        <circle
+          cx={0}
+          cy={row * props.rowHeight}
+          r={props.radius}
+          fill={'white'}
+          stroke={'black'}
+          strokeWidth={1}
+        />
+      </g>
+    );
+  });
+
+  // create a line between two circles.
+  const lines: JSX.Element[] = [];
+  let y1 = props.connectedRows[0] * props.rowHeight + props.radius;
+  let y2;
+  for (let i = 1; i < props.connectedRows.length; i++) {
+    y2 = props.connectedRows[i] * props.rowHeight - props.radius;
+    lines.push(
+      <line key={'line' + i} x1={0} y1={y1} x2={0} y2={y2} stroke={'black'} strokeWidth={1} />,
+    );
+    y1 = props.connectedRows[i] * props.rowHeight + props.radius;
+  }
+
+  const yOffset = props.rowHeight * 0.5;
+
+  return (
+    <g
+      ref={ref}
+      className={'col-' + props.col}
+      transform={'translate(' + props.xOffset + ',' + yOffset + ')'}
+    >
+      {lines}
+      {circles}
+    </g>
+  );
+}
diff --git a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss
new file mode 100644
index 0000000000000000000000000000000000000000..dd4f3bbc24197e5a27599dbc1b421accff43ed89
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.scss
@@ -0,0 +1,33 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+
+/* istanbul ignore file */
+
+
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+.makePaohvisMenu {
+  display: flex;
+  gap: 1em;
+  margin: 1em;
+  .textFieldMakePaohvisMenu {
+    min-width: 120px;
+  }
+}
+
+#reverseButtonLabel {
+  font-size: 17px;
+  color: inherit;
+}
+
+#makeButton {
+  height: 40px;
+  margin-right: 5em;
+  font-weight: bold;
+}
\ No newline at end of file
diff --git a/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d19e1292c7a48c9035bfc87a7616e7a2772d3d4e
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/MakePaohvisMenu.tsx
@@ -0,0 +1,600 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import { Button, MenuItem, TextField, IconButton } from '@mui/material';
+import React, { ReactElement, useEffect } from 'react';
+import {
+  Attribute,
+  AttributeNames,
+  AttributeOrigin,
+  EntitiesFromSchema,
+  EntityOrigin,
+  NodeOrder,
+  PaohvisNodeOrder,
+  RelationsFromSchema,
+  ValueType,
+} from '../Types';
+import { Sort } from '@mui/icons-material';
+import './MakePaohvisMenu.scss';
+import { useImmer } from 'use-immer';
+import { useGraphQueryResult, useSchemaGraph } from '@graphpolaris/shared/lib/data-access';
+import { calculateAttributesAndRelations, calculateAttributesFromRelation } from '../utils/utils';
+import calcEntitiesFromQueryResult from '../utils/CalcEntitiesFromQueryResult';
+import { isNodeLinkResult } from '../../shared/ResultNodeLinkParserUseCase';
+import { select } from 'd3';
+
+/** The typing for the props of the Paohvis menu */
+type MakePaohvisMenuProps = {
+  makePaohvis: (
+    entityVertical: string,
+    entityHorizontal: string,
+    relationName: string,
+    isEntityFromRelationFrom: boolean,
+    chosenAttribute: Attribute,
+    nodeOrder: PaohvisNodeOrder,
+  ) => void;
+};
+
+/** The variables in the state of the PAOHvis menu */
+type MakePaohvisMenuState = {
+  entityVertical: string;
+  entityHorizontal: string;
+  relationName: string;
+  isEntityVerticalEqualToRelationFrom: boolean;
+  attributeNameAndOrigin: string;
+  isSelectedAttributeFromEntity: boolean;
+  isButtonEnabled: boolean;
+  sortOrder: NodeOrder;
+  isReverseOrder: boolean;
+
+  entitiesFromSchema: EntitiesFromSchema;
+  relationsFromSchema: RelationsFromSchema;
+  entitiesFromQueryResult: string[];
+
+  relationNameOptions: string[];
+  attributeNameOptions: Record<string, string[]>;
+  relationValue: string,
+  attributeValue: string,
+};
+
+/** React component that renders a menu with input fields for adding a new Paohvis visualisation. */
+export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => {
+  const graphQueryResult = useGraphQueryResult();
+  const schema = useSchemaGraph();
+  const [state, setState] = useImmer<MakePaohvisMenuState>({
+    entityVertical: '',
+    entityHorizontal: '',
+    relationName: '',
+    isEntityVerticalEqualToRelationFrom: true,
+    attributeNameAndOrigin: '',
+    isSelectedAttributeFromEntity: false,
+    isButtonEnabled: false,
+    entitiesFromQueryResult: [],
+    sortOrder: NodeOrder.degree,
+    isReverseOrder: false,
+    entitiesFromSchema: { entityNames: [], attributesPerEntity: {}, relationsPerEntity: {} },
+    relationsFromSchema: {
+      relationCollection: [],
+      relationNames: {},
+      attributesPerRelation: {},
+    },
+    relationNameOptions: [],
+    attributeNameOptions: {},
+
+    relationValue: '?',
+    attributeValue: '?',
+  });
+
+  //
+  // FUNCTIONS
+  //
+
+  /**
+   * Called when the entity field is changed.
+   * Calculates the `relationNameOptions`, resets the `entityHorizontal`, `relationName` and `attributeName` and sets the new state.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   * It has the value of the entity you clicked on.
+   */
+  function onChangeEntity(event: React.ChangeEvent<HTMLInputElement>): void {
+    const newEntity = event.target.value;
+    setState((draft) => {
+      draft.relationNameOptions = [];
+      const relationNames: string[] = state.entitiesFromSchema.relationsPerEntity[newEntity];
+
+      // check if there are any relations
+      if (relationNames) {
+        // push all relation options to relationNameOptions
+        relationNames.forEach((relation) => {
+          let relationSplit = state.relationsFromSchema.relationNames[relation].split(':');
+
+          //check if relation is valid
+          if (
+            draft.entitiesFromQueryResult.includes(relationSplit[0]) &&
+            draft.entitiesFromQueryResult.includes(relationSplit[1])
+          ) {
+            // check if relation is selfedge
+            if (relationSplit[0] == relationSplit[1]) {
+              const relationFrom = `${relation}:${EntityOrigin.from}`;
+              const relationTo = `${relation}:${EntityOrigin.to}`;
+              if (
+                !draft.relationNameOptions.includes(relationFrom) &&
+                !draft.relationNameOptions.includes(relationTo)
+              ) {
+                draft.relationNameOptions.push(relationFrom);
+                draft.relationNameOptions.push(relationTo);
+              }
+            } else draft.relationNameOptions.push(relation);
+          }
+        });
+        // filter out duplicates
+        draft.relationNameOptions = draft.relationNameOptions.filter(
+          (n, i) => draft.relationNameOptions.indexOf(n) === i,
+        );
+      }
+
+      draft.relationValue = '';
+      draft.attributeValue = '';
+
+      draft.entityVertical = newEntity;
+      draft.entityHorizontal = '';
+      draft.relationName = '';
+      draft.isEntityVerticalEqualToRelationFrom = true;
+      draft.attributeNameAndOrigin = '';
+      draft.isButtonEnabled = false;
+      draft.attributeNameOptions = {};
+      return draft;
+    });
+  };
+
+
+  /**
+   * Called when the relationName field is changed.
+   * Calculates the possible attribute values, resets the `attributeValue`, sets the `entityHorizontal`.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   * It has the value of the relation you clicked on.
+   */
+  function onChangeRelationName(event: React.ChangeEvent<HTMLInputElement>): void {
+    setState((draft) => {
+      draft.relationName = event.target.value;
+      draft.attributeValue = '';
+      draft.attributeNameAndOrigin = '';
+      draft.isButtonEnabled = false;
+
+      const newRelationSplit = event.target.value.split(':');
+      const newRelation = newRelationSplit[0];
+
+      // This value should always be there
+      draft.attributeNameOptions[AttributeOrigin.noAttribute] = [ValueType.noAttribute];
+
+      const relationNames = draft.relationsFromSchema.relationNames[newRelation];
+      if (relationNames == undefined) throw new Error('This entity does not exist in the schema.');
+
+      const relationSplit: string[] = relationNames.split(':');
+      const entityVertical = draft.entityVertical;
+
+      // check if relation is self edge
+      if (relationSplit[0] == relationSplit[1]) {
+        const entityOrigin = newRelationSplit[1];
+
+        if (!isValidEntityOrigin(entityOrigin))
+          throw new Error(
+            `The entity "${entityVertical}" has an invalid entity origin: ${entityOrigin}.`,
+          );
+
+        draft.isEntityVerticalEqualToRelationFrom = entityOrigin == EntityOrigin.from;
+        draft.entityHorizontal = relationSplit[0];
+      }
+      // check if entityVertical is the entity in the 'from' of the relation
+      else if (entityVertical == relationSplit[0]) {
+        draft.isEntityVerticalEqualToRelationFrom = true;
+        draft.entityHorizontal = relationSplit[1];
+      }
+      // check if entityVertical is the entity in the 'to' of the relation
+      else if (entityVertical == relationSplit[1]) {
+        draft.isEntityVerticalEqualToRelationFrom = false;
+        draft.entityHorizontal = relationSplit[0];
+      } else
+        throw new Error(
+          `The relationNames from this.relationsFromSchema for ${newRelation} is invalid`,
+        );
+
+      draft.attributeNameOptions[AttributeOrigin.entity] = [];
+
+      if (draft.entitiesFromSchema.attributesPerEntity[draft.entityHorizontal]) {
+        let allAttributesOfEntity: AttributeNames =
+          draft.entitiesFromSchema.attributesPerEntity[draft.entityHorizontal];
+
+        let attributeNamesOfEntity: string[] = allAttributesOfEntity.textAttributeNames
+          .concat(allAttributesOfEntity.numberAttributeNames)
+          .concat(allAttributesOfEntity.boolAttributeNames);
+        draft.attributeNameOptions[AttributeOrigin.entity] = attributeNamesOfEntity;
+      }
+
+      draft.attributeNameOptions[AttributeOrigin.relation] = [];
+
+      if (draft.relationsFromSchema.attributesPerRelation[newRelation]) {
+        let allAttributesOfRelation: AttributeNames =
+          draft.relationsFromSchema.attributesPerRelation[newRelation];
+
+        let attributeNamesOfRelation: string[] = allAttributesOfRelation.textAttributeNames
+          .concat(allAttributesOfRelation.numberAttributeNames)
+          .concat(allAttributesOfRelation.boolAttributeNames);
+        draft.attributeNameOptions[AttributeOrigin.relation] = attributeNamesOfRelation;
+      }
+
+      return draft;
+    });
+  };
+
+
+  /**
+   * Called when the attributeName field is changed.
+   * Sets the chosen attribute value, enables the "Make" button and sets the state.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   * It has the value of the attributeName you clicked on.
+   */
+  function onChangeAttributeName(event: React.ChangeEvent<HTMLInputElement>): void {
+    const newAttribute = event.target.value;
+    setState((draft) => {
+      const isSelectedAttributeFromEntity = newAttribute.split(':')[1] == AttributeOrigin.entity;
+      draft.attributeValue = newAttribute;
+
+      // render changes
+      draft.attributeNameAndOrigin = newAttribute;
+      draft.isSelectedAttributeFromEntity = isSelectedAttributeFromEntity;
+      draft.isButtonEnabled = true;
+      return draft;
+    });
+  };
+
+  /**
+   * Called when the sort order field is changed.
+   * Sets the state by changing the sort order.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   */
+  function onChangeSortOrder(event: React.ChangeEvent<HTMLInputElement>): void {
+    const newSortOrder: NodeOrder = event.target.value as NodeOrder;
+    const nodeOrders: string[] = Object.values(NodeOrder);
+    if (nodeOrders.includes(newSortOrder)) {
+      unflipReverseIconButton();
+
+      setState((draft) => {
+        // render changes
+        draft.sortOrder = newSortOrder;
+        draft.isReverseOrder = false;
+        return draft;
+      });
+    } else throw new Error(newSortOrder + ' is not a sort order.');
+  };
+
+
+  /**
+   * Called when the user clicks the reverse order button.
+   * Sets the state and changes the svg of the button.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   */
+  function onClickReverseOrder(): void {
+    //change the svg of the button to be the reversed variant
+    setState((draft) => {
+      draft.isReverseOrder = !draft.isReverseOrder;
+      const isReverseOrder = draft.isReverseOrder;
+      switch (draft.sortOrder) {
+        case NodeOrder.alphabetical:
+          if (isReverseOrder) flipReverseIconButtonLabel();
+          else unflipReverseIconButtonLabel();
+          break;
+        default:
+          if (isReverseOrder) flipReverseIconButton();
+          else unflipReverseIconButton();
+          break;
+      }
+      return draft;
+    });
+  };
+
+
+  /**
+   * Called when the user clicks the "Make" button.
+   * Checks if all fields are valid before calling `makePaohvis()`.
+   */
+  function onClickMakeButton(): void {
+    const relationName = state.relationName.split(':')[0];
+
+    if (
+      state.entitiesFromSchema.entityNames.includes(state.entityVertical) &&
+      state.entitiesFromSchema.relationsPerEntity[state.entityVertical].includes(relationName)
+    ) {
+      const attributeNameAndOrigin: string[] = state.attributeNameAndOrigin.split(':');
+      const attributeName: string = attributeNameAndOrigin[0];
+      const attributeOrigin: AttributeOrigin = attributeNameAndOrigin[1] as AttributeOrigin;
+      const chosenAttribute: Attribute = {
+        name: attributeName,
+        type: getTypeOfAttribute(attributeName),
+        origin: attributeOrigin,
+      };
+
+      props.makePaohvis(
+        state.entityVertical,
+        state.entityHorizontal,
+        relationName,
+        state.isEntityVerticalEqualToRelationFrom,
+        chosenAttribute,
+        {
+          orderBy: state.sortOrder,
+          isReverseOrder: state.isReverseOrder,
+        },
+      );
+    } else {
+      setState((draft) => {
+        draft.isButtonEnabled = false;
+        return draft;
+      });
+      throw new Error('Error: chosen entity or relation is invalid.');
+    }
+  };
+
+  /**
+   * Gets the type of the value of the specified attribute by .
+   * @param attributeName The name of the specified attribute.
+   * @returns {ValueType} The type of the value of the specified attribute.
+   */
+  function getTypeOfAttribute(attributeName: string): ValueType {
+    // get the correct AttributeNames for the attribute
+    const attributeNames = state.isSelectedAttributeFromEntity
+      ? state.entitiesFromSchema.attributesPerEntity[state.entityHorizontal]
+      : state.relationsFromSchema.attributesPerRelation[state.relationName.split(':')[0]];
+
+    // look up to which ValueType the attribute belongs
+    if (attributeNames.boolAttributeNames.includes(attributeName)) return ValueType.bool;
+    if (attributeNames.numberAttributeNames.includes(attributeName)) return ValueType.number;
+    if (attributeNames.textAttributeNames.includes(attributeName)) return ValueType.text;
+    if (attributeName == ValueType.noAttribute) return ValueType.noAttribute;
+
+    throw new Error('Attribute ' + attributeName + ' does not exist');
+  }
+
+  /**
+   * Checks if the given string is a valid EntityOrigin.
+   * @param entityOrigin string that should be checked.
+   * @returns {boolean} returns true if the given string is a valid EntityOrigin.
+   */
+  function isValidEntityOrigin(entityOrigin: string): boolean {
+    return entityOrigin == EntityOrigin.from || entityOrigin == EntityOrigin.to;
+  }
+
+  /**
+   * Called when the user clicks the reverse order button when `Alphabetical` is selected.
+   * Changes the svg of the button to indicate that the order is reversed.
+   */
+  function flipReverseIconButtonLabel(): void {
+    select('.reverseIconButton').select('#reverseButtonLabel').text('Z-A');
+  }
+  /**
+   * Called when the user clicks the reverse order button when `Alphabetical` is selected.
+   * Changes the svg of the button back to normal.
+   */
+  function unflipReverseIconButtonLabel(): void {
+    select('.reverseIconButton').select('#reverseButtonLabel').text('A-Z');
+  }
+  /**
+   * Called when the user clicks the reverse order button when `Degree` is selected.
+   * Changes the svg of the button to indicate that the order is reversed.
+   */
+  function flipReverseIconButton(): void {
+    select('.reverseIconButton').transition().style('transform', 'scaleY(-1)');
+  }
+  /**
+   * Called when the user clicks the reverse order button when `Degree` is selected.
+   * Changes the svg of the button back to normal.
+   */
+  function unflipReverseIconButton(): void {
+    select('.reverseIconButton').transition().style('transform', 'scaleY(1)');
+  }
+
+  //
+  // REACTIVITY
+  //
+
+  useEffect(() => {
+    resetConfig();
+    setState((draft) => {
+      draft.entitiesFromSchema = calculateAttributesAndRelations(schema);
+      draft.relationsFromSchema = calculateAttributesFromRelation(schema);
+      return draft;
+    });
+  }, [schema]);
+
+
+  /** This method filters and makes a new Paohvis table. */
+  useEffect(() => {
+    if (isNodeLinkResult(graphQueryResult)) {
+      resetConfig();
+      setState((draft) => {
+        draft.entitiesFromQueryResult = calcEntitiesFromQueryResult(graphQueryResult);
+        return draft;
+      });
+    } else {
+      console.error('Invalid query result!')
+    }
+  }, [graphQueryResult]);
+
+
+  /** This resets the configuration. Should be called when the possible entities and relations change. */
+  function resetConfig(): void {
+    setState((draft) => {
+      draft.entityVertical = '';
+      draft.entityHorizontal = '';
+      draft.relationName = '';
+      draft.isEntityVerticalEqualToRelationFrom = true;
+      draft.attributeNameAndOrigin = '';
+      draft.isSelectedAttributeFromEntity = false;
+      draft.isButtonEnabled = false;
+      draft.entitiesFromQueryResult = [];
+
+      draft.relationNameOptions = [];
+      draft.attributeNameOptions = {};
+      return draft;
+    });
+  }
+
+  //
+  // RENDER
+  //
+
+  // Retrieve the possible entity options. If none available, set helper message.
+  let entityMenuItems: ReactElement[];
+  if (state.entitiesFromQueryResult.length > 0)
+    entityMenuItems = state.entitiesFromQueryResult.map((entity) => (
+      <MenuItem key={entity} value={entity}>
+        {entity}
+      </MenuItem>
+    ));
+  else
+    entityMenuItems = [
+      <MenuItem key="placeholder" value="" disabled>
+        No query data available
+      </MenuItem>,
+    ];
+
+  // Retrieve the possible relationName options. If none available, set helper message.
+  let relationNameMenuItems: ReactElement[];
+  if (state.relationNameOptions.length > 0)
+    relationNameMenuItems = state.relationNameOptions.map((relation) => (
+      <MenuItem key={relation} value={relation}>
+        {relation}
+      </MenuItem>
+    ));
+  else
+    relationNameMenuItems = [
+      <MenuItem key="placeholder" value="" disabled>
+        First select an entity with one or more relations
+      </MenuItem>,
+    ];
+
+  // Retrieve all the possible attributeName options. If none available, set helper message.
+  let attributeNameMenuItems: ReactElement[] = [];
+  let attributeNameMenuItemsNoAttribute: ReactElement[] = [];
+  let attributeNameMenuItemsEntity: ReactElement[] = [];
+  let attributeNameMenuItemsRelation: ReactElement[] = [];
+  if (state.attributeNameOptions['Entity'] && state.attributeNameOptions['Relation']) {
+    attributeNameMenuItemsNoAttribute = state.attributeNameOptions['No attribute'].map(
+      (attribute) => (
+        <MenuItem key={attribute} value={`${attribute}:NoAttribute`}>
+          {attribute}
+        </MenuItem>
+      ),
+    );
+    attributeNameMenuItemsEntity = state.attributeNameOptions['Entity'].map((attribute) => (
+      <MenuItem key={`${attribute}:Entity`} value={`${attribute}:Entity`}>
+        {`${attribute} : Entity`}
+      </MenuItem>
+    ));
+    attributeNameMenuItemsRelation = state.attributeNameOptions['Relation'].map(
+      (attribute) => (
+        <MenuItem key={`${attribute}:Relation`} value={`${attribute}:Relation`}>
+          {`${attribute} : Relation`}
+        </MenuItem>
+      ),
+    );
+
+    attributeNameMenuItems = attributeNameMenuItemsNoAttribute
+      .concat(attributeNameMenuItemsEntity)
+      .concat(attributeNameMenuItemsRelation);
+  } else
+    attributeNameMenuItems = [
+      <MenuItem key="placeholder" value="" disabled>
+        First select an relation with one or more attributes
+      </MenuItem>,
+    ];
+
+  // make sort order menu items
+  const sortOrderMenuItems: ReactElement[] = Object.values(NodeOrder).map((nodeOrder) => {
+    return (
+      <MenuItem key={nodeOrder} value={nodeOrder}>
+        {nodeOrder}
+      </MenuItem>
+    );
+  });
+
+  // make the reverse button
+  let reverseIcon: ReactElement;
+  switch (state.sortOrder) {
+    case NodeOrder.alphabetical:
+      reverseIcon = <span id="reverseButtonLabel">A-Z</span>;
+      break;
+    default:
+      reverseIcon = <Sort className={'reverseSortIcon'} />;
+      break;
+  }
+
+  // return the whole MakePaohvisMenu
+  return (
+    <div className="makePaohvisMenu">
+      <TextField
+        select
+        className="textFieldMakePaohvisMenu"
+        id="standard-select-entity"
+        label="Entity"
+        value={state.entityVertical}
+        onChange={onChangeEntity}
+      >
+        {entityMenuItems}
+      </TextField>
+      <TextField
+        select
+        className="textFieldMakePaohvisMenu"
+        id="standard-select-relation"
+        label="Relation"
+        value={state.relationName}
+        onChange={onChangeRelationName}
+      >
+        {relationNameMenuItems}
+      </TextField>
+      <TextField
+        select
+        className="textFieldMakePaohvisMenu"
+        id="standard-select-attribute"
+        label="Attribute"
+        value={state.attributeNameAndOrigin}
+        onChange={onChangeAttributeName}
+      >
+        {attributeNameMenuItems}
+      </TextField>
+      <TextField
+        select
+        className="textFieldMakePaohvisMenu"
+        id="standard-select-sort-order"
+        label="Sort order"
+        value={state.sortOrder}
+        onChange={onChangeSortOrder}
+      >
+        {sortOrderMenuItems}
+      </TextField>
+      <IconButton
+        className={'reverseIconButton'}
+        color="inherit"
+        onClick={onClickReverseOrder}
+      >
+        {reverseIcon}
+      </IconButton>
+
+      <Button
+        id="makeButton"
+        variant="contained"
+        disabled={!state.isButtonEnabled}
+        onClick={onClickMakeButton}
+      >
+        <span>Make</span>
+      </Button>
+    </div>
+  );
+}
+
+export default MakePaohvisMenu;
diff --git a/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d6ef0a52a6871a9352987bdc96ed2c3fa7758755
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss
@@ -0,0 +1,57 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+
+/* istanbul ignore file */
+
+
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+// Styling for the PaohvisFilterComponent
+.container {
+  font-family: 'Open Sans', sans-serif;
+  display: flex;
+  flex-direction: column;
+  p {
+    font-size: 13px;
+    font-weight: 600;
+    color: #2d2d2d;
+    display: list-item;
+  }
+  .title {
+    color: #212020;
+    font-weight: 800;
+    line-height: 1.6em;
+    font-size: 16px;
+    margin-bottom: 0.4rem;
+    margin-top: 0.1;
+    padding-left: 10px;
+  }
+  .subtitle {
+    color: #212020;
+    font-weight: 700;
+    font-size: 14px;
+    padding-left: 10px;
+    padding-bottom: 0px;
+    margin-bottom: 0px;
+  }
+}
+
+.selectContainer {
+  display: block;
+  justify-content: space-around;
+  select {
+    width: 6rem;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    option {
+      width: 35px;
+      text-overflow: ellipsis;
+    }
+  }
+}
\ No newline at end of file
diff --git a/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss.d.ts b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0139761e227918d250897db1938b8bafba18ecdf
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.module.scss.d.ts
@@ -0,0 +1,7 @@
+declare const classNames: {
+  readonly container: 'container';
+  readonly title: 'title';
+  readonly subtitle: 'subtitle';
+  readonly selectContainer: 'selectContainer';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.tsx b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6876e93637cb384fcffe381147ca5e6d82de8889
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/PaohvisFilterComponent.tsx
@@ -0,0 +1,428 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { ChangeEventHandler, ReactElement, useState, MouseEventHandler, useEffect } from 'react';
+import styles from './PaohvisFilterComponent.module.scss';
+import { AttributeNames, FilterType } from '../Types';
+import { useImmer } from 'use-immer';
+import { Button, MenuItem, TextField } from '@mui/material';
+import { useGraphQueryResult, useSchemaGraph } from '@graphpolaris/shared/lib/data-access';
+import { isNodeLinkResult } from '../../shared/ResultNodeLinkParserUseCase';
+import { isSchemaResult } from '../../shared/SchemaResultType';
+import { calculateAttributesAndRelations, calculateAttributesFromRelation } from '../utils/utils';
+import { boolPredicates, numberPredicates, textPredicates } from '../models/FilterPredicates';
+
+
+type PaohvisFilterProps = {
+  axis: FilterType;
+  entityVertical: string;
+  entityHorizontal: string;
+  relationName: string;
+
+  filterPaohvis(
+    isTargetEntity: boolean,
+    filterTarget: string,
+    attributeName: string,
+    predicate: string,
+    compareValue: string,
+  ): void;
+  resetFilter(isTargetEntity: boolean): void;
+};
+
+type PaohvisFilterState = {
+  filterTarget: string;
+  attributeNameAndType: string;
+  attributeType: string;
+  predicate: string;
+  compareValue: string;
+  isFilterButtonEnabled: boolean;
+  predicateTypeList: string[];
+  attributeNamesOptions: string[];
+
+  namesPerEntityOrRelation: string[];
+  attributesPerEntityOrRelation: Record<string, AttributeNames>;
+
+};
+
+/** Component for rendering the filters for PAOHvis */
+export const PaohvisFilterComponent = (props: PaohvisFilterProps) => {
+  const graphQueryResult = useGraphQueryResult();
+  const schema = useSchemaGraph();
+  const isTargetEntity = props.axis != FilterType.edge;
+
+  const [state, setState] = useImmer<PaohvisFilterState>({
+    filterTarget: '',
+    attributeNameAndType: '',
+    attributeType: '',
+    predicate: '',
+    compareValue: '',
+    isFilterButtonEnabled: false,
+    predicateTypeList: [],
+    attributeNamesOptions: [],
+    namesPerEntityOrRelation: [],
+    attributesPerEntityOrRelation: {},
+  });
+
+  /**
+ * Updates the list of attributes that can be chosen from for the filter component.
+ * Takes all attributes from the filter target and converts them to the format 'name:type'.
+ */
+  function updateAttributeNameOptions() {
+    const filterTarget = state.filterTarget;
+    const namesPerEntityOrRelation = state.namesPerEntityOrRelation;
+    const attributesPerEntityOrRelation = state.attributesPerEntityOrRelation;
+
+    if (!namesPerEntityOrRelation.includes(filterTarget))
+      throw new Error('The filter target does not exist in the schema');
+
+    setState(draft => {
+      // Add all possible options for attributes to attributeNamesOptions
+      // check all text attributes
+      if (attributesPerEntityOrRelation[filterTarget].textAttributeNames.length > 0)
+        attributesPerEntityOrRelation[filterTarget].textAttributeNames.map((attributeName) =>
+          draft.attributeNamesOptions.push(`${attributeName}:text`),
+        );
+
+      // check all number attributes
+      if (attributesPerEntityOrRelation[filterTarget].numberAttributeNames.length > 0)
+        attributesPerEntityOrRelation[filterTarget].numberAttributeNames.map((attributeName) =>
+          draft.attributeNamesOptions.push(`${attributeName}:number`),
+        );
+
+      // check all bool attributes
+      if (attributesPerEntityOrRelation[filterTarget].boolAttributeNames.length > 0)
+        attributesPerEntityOrRelation[filterTarget].boolAttributeNames.map((attributeName) =>
+          draft.attributeNamesOptions.push(`${attributeName}:bool`),
+        );
+      return draft;
+    });
+  };
+
+  /**
+   * This is called when the name of the attribute is changed in a filter component.
+   * Based on the type of the chosen attribute, a different list with predicates is generated.
+   * @param event that called this eventhandler.
+   */
+  function onChangeAttributeName(event: React.ChangeEvent<HTMLInputElement>): void {
+    const newAttributeNameAndType = event.target.value;
+    const newAttributeSplit = event.target.value.split(':');
+    const newAttributeType = newAttributeSplit[1];
+
+    setState(draft => {
+      if (!containsFilterTargetChosenAttribute(newAttributeNameAndType))
+        throw new Error(
+          'The chosen attribute does not exist in the entity/relation that will be filtered',
+        );
+
+      switch (newAttributeType) {
+        case 'text':
+          draft.predicateTypeList = Object.keys(textPredicates);
+          break;
+        case 'number':
+          draft.predicateTypeList = Object.keys(numberPredicates);
+          break;
+        case 'bool':
+          draft.predicateTypeList = Object.keys(boolPredicates);
+          break;
+      }
+
+      draft.attributeNameAndType = newAttributeNameAndType;
+      draft.attributeType = newAttributeType;
+      draft.predicate = '';
+      draft.compareValue = '';
+      draft.isFilterButtonEnabled = false;
+      return draft;
+    });
+  };
+
+  /**
+* This is called when the value of the predicate is changed in the filter component.
+* @param event that called this eventhandler.
+*/
+  function onChangePredicate(event: React.ChangeEvent<HTMLInputElement>): void {
+    const newpredicate = event.target.value;
+
+    if (!isPredicateValid(newpredicate)) throw new Error('The chosen predicate is invalid');
+
+    setState(draft => {
+      draft.predicate = newpredicate;
+      draft.compareValue = '';
+      draft.isFilterButtonEnabled = false;
+      return draft;
+    });
+  };
+
+  /**
+   * This is called when the value to compare the attribute with is changed in a filter component.
+   * Based on the type of the chosen attribute, the compareValue has some conditions on what type it should be.
+   * If attribute is numerical, the value has to be a number, etc.
+   * @param event that called this eventhandler.
+   */
+  function onChangeCompareValue(event: React.ChangeEvent<HTMLInputElement>): void {
+    setState(draft => {
+      draft.compareValue = event.target.value;
+      draft.isFilterButtonEnabled = isFilterConfigurationValid();
+      return draft;
+    });
+  };
+
+  /**
+* This is called when the "apply filter" button is clicked.
+* It checks if the input is correct and sends the information for the filters to the PaohvisViewModelImpl.
+*/
+  function onClickFilterPaohvisButton(): void {
+    if (!isFilterConfigurationValid())
+      throw new Error('Error: chosen attribute of predicate-value is invalid.');
+
+    props.filterPaohvis(
+      isTargetEntity,
+      state.filterTarget,
+      state.attributeNameAndType,
+      state.predicate,
+      state.compareValue,
+    );
+  };
+
+  /**
+ * This resets the filters for the chosen filter component.
+ */
+  function onClickResetFilter(): void {
+    setState(draft => {
+      draft.attributeNameAndType = '';
+      draft.attributeType = '';
+      draft.predicate = '';
+      draft.compareValue = '';
+      draft.isFilterButtonEnabled = false;
+      draft.predicateTypeList = [];
+      return draft;
+    });
+    props.resetFilter(isTargetEntity);
+  };
+
+  /**
+ * Checks if the filter configuration is valid.
+ * @returns {boolean} true if the filter configuration is valid.
+ */
+  function isFilterConfigurationValid(): boolean {
+    return (
+      containsFilterTargetChosenAttribute(state.attributeNameAndType) &&
+      isPredicateValid(state.predicate) &&
+      isCompareValueTypeValid(state.compareValue)
+    );
+  }
+  /**
+   * Checks if the given predicate is valid for the filter target.
+   * @param predicate that should be checked.
+   * @returns {boolean} true if the predicate is valid.
+   */
+  function isPredicateValid(predicate: string): boolean {
+    return state.predicateTypeList.includes(predicate);
+  }
+
+  /**
+   * This is used when the compareValue has changed.
+   * @param {string} compareValue is the value that will be used with the predicate.
+   * @returns {boolean} true if the chosen compareValue and chosen attribute are a valid combination.
+   */
+  function isCompareValueTypeValid(compareValue: string): boolean {
+    const lowerCaseCompareValue: string = compareValue.toLowerCase();
+    return (
+      (state.attributeType == 'number' && !Number.isNaN(Number(compareValue))) ||
+      (state.attributeType == 'bool' && (lowerCaseCompareValue == 'true' || lowerCaseCompareValue == 'false')) ||
+      (state.attributeType == 'text' && compareValue.length > 0)
+    );
+  }
+
+  /**
+   * This determines whether the given attribute belongs to the filter target.
+   * @param attributeNameAndType is the chosen attribute in the format of 'name:type'
+   * @returns {boolean} true when the given attribute belongs to the filter target.
+   */
+  function containsFilterTargetChosenAttribute(attributeNameAndType: string): boolean {
+    const filterTarget = state.filterTarget;
+    const split: string[] = attributeNameAndType.split(':');
+    const attributeName = split[0];
+    const attributeType = split[1];
+    const attributesPerEntityOrRelation = state.attributesPerEntityOrRelation;
+
+    switch (attributeType) {
+      case 'text':
+        return attributesPerEntityOrRelation[filterTarget].textAttributeNames.includes(attributeName,);
+      case 'number':
+        return attributesPerEntityOrRelation[filterTarget].numberAttributeNames.includes(attributeName,);
+      case 'bool':
+        return attributesPerEntityOrRelation[filterTarget].boolAttributeNames.includes(attributeName,);
+      default:
+        return false;
+    }
+  }
+
+  /**
+   * This determines on which entity/relation the filter is going to be applied to.
+   * @param entityVertical is the entity type that belongs to the Y-axis of the Paohvis table.
+   * @param entityHorizontal is the entity type that belongs to the X-axis of the Paohvis table.
+   * @param relationName is the relation type that is that is displayed in the Paohvis table
+   */
+  function determineFilterTarget(
+    entityVertical: string,
+    entityHorizontal: string,
+    relationName: string,
+  ): void {
+    setState(draft => {
+      if (props.axis === FilterType.yaxis)
+        draft.filterTarget = entityVertical;
+      else if (props.axis === FilterType.xaxis)
+        draft.filterTarget = entityHorizontal;
+      else draft.filterTarget = relationName;
+      return draft;
+    });
+  }
+
+  //
+  // REACT
+  //
+
+  useEffect(() => {
+    if (isNodeLinkResult(graphQueryResult)) {
+      setState(draft => {
+        draft.filterTarget = '';
+        draft.attributeNamesOptions = [];
+        return draft;
+      })
+    } else {
+      console.error('Invalid query result!')
+    }
+  }, [graphQueryResult]);
+
+  useEffect(() => {
+    // if (isSchemaResult(schema)) {
+    setState(draft => {
+      if (isTargetEntity) {
+        const entitiesFromSchema = calculateAttributesAndRelations(schema);
+        draft.namesPerEntityOrRelation = entitiesFromSchema.entityNames;
+        draft.attributesPerEntityOrRelation = entitiesFromSchema.attributesPerEntity;
+      } else {
+        const relationsFromSchema = calculateAttributesFromRelation(schema);
+        draft.namesPerEntityOrRelation = relationsFromSchema.relationCollection;
+        draft.attributesPerEntityOrRelation = relationsFromSchema.attributesPerRelation;
+      }
+
+      draft.filterTarget = '';
+      draft.attributeNamesOptions = [];
+      return draft;
+    });
+    // } else {
+    //   console.error('Invalid schema!')
+    // }
+  }, [schema]);
+
+
+  useEffect(() => {
+    determineFilterTarget(props.entityVertical, props.entityHorizontal, props.relationName);
+    if (state.filterTarget !== '')
+      updateAttributeNameOptions();
+  }, [props]);
+
+  //
+  // RENDER
+  //
+
+  // Check if the given entity or relation is in the queryResult
+  let attributeNameMenuItems: ReactElement[] = [];
+  if (state.attributeNamesOptions.length > 0) {
+    attributeNameMenuItems = state.attributeNamesOptions.map((attributeNameAndType) => {
+      const attributeName = attributeNameAndType.split(':')[0];
+      return (
+        <MenuItem key={attributeName} value={attributeNameAndType}>
+          {attributeNameAndType}
+        </MenuItem>
+      );
+    });
+  } else
+    attributeNameMenuItems = [
+      <MenuItem key="placeholder" value="" disabled>
+        First select an entity/relation with one or more attributes
+      </MenuItem>,
+    ];
+
+  let predicateTypeList: ReactElement[] = [];
+  if (state.predicateTypeList.length > 0) {
+    predicateTypeList = state.predicateTypeList.map((predicate) => (
+      <MenuItem key={predicate} value={predicate}>
+        {predicate}
+      </MenuItem>
+    ));
+  } else
+    predicateTypeList = [
+      <MenuItem key="placeholder" value="" disabled>
+        First select an attribute
+      </MenuItem>,
+    ];
+
+  return (
+    <div>
+      <div className={styles.container}>
+        <p className={styles.title}>PAOHVis filters{props.axis}:</p>
+        <div className={styles.selectContainer}>
+          <p className={styles.subtitle}>{state.filterTarget}</p>
+          <div style={{ padding: 10, display: 'block', width: '100%' }}>
+            <TextField
+              select
+              id="standard-select-entity"
+              style={{ minWidth: 120, marginRight: 20 }}
+              label="Attribute"
+              value={state.attributeNameAndType}
+              onChange={onChangeAttributeName}
+            >
+              {attributeNameMenuItems}
+            </TextField>
+            <TextField
+              select
+              id="standard-select-relation"
+              style={{ minWidth: 120, marginRight: 20 }}
+              label="Relation"
+              value={state.predicate}
+              onChange={onChangePredicate}
+            >
+              {predicateTypeList}
+            </TextField>
+            <TextField
+              id="standard-select-relation"
+              style={{ minWidth: 120, marginRight: 20 }}
+              label="Value"
+              value={state.compareValue}
+              onChange={onChangeCompareValue}
+            >
+              { }
+            </TextField>
+
+            <div style={{ height: 40, paddingTop: 10, marginBottom: 10 }}>
+              <Button
+                variant="contained"
+                disabled={!state.isFilterButtonEnabled}
+                onClick={onClickFilterPaohvisButton}
+              >
+                <span style={{ fontWeight: 'bold' }}>Apply filter</span>
+              </Button>
+            </div>
+            <div style={{ height: 40, marginBottom: 20 }}>
+              <Button
+                variant="contained"
+                onClick={onClickResetFilter}
+              >
+                <span style={{ fontWeight: 'bold' }}>Reset filter</span>
+              </Button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/libs/shared/lib/vis/paohvis/components/RowLabelColumn.tsx b/libs/shared/lib/vis/paohvis/components/RowLabelColumn.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..82ca7b58be3a8bd9f80f5c0b256e122d6b658f4e
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/RowLabelColumn.tsx
@@ -0,0 +1,82 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+import { select } from 'd3';
+import React, { useEffect, useRef } from 'react';
+
+type RowLabelColumnProps = {
+  onMouseEnter: (row: number) => void;
+  onMouseLeave: (row: number) => void;
+
+  titles: string[];
+  width: number;
+  rowHeight: number;
+  yOffset: number;
+};
+export const RowLabelColumn = (props: RowLabelColumnProps) => {
+  const titleRows = props.titles.map((title, i) => (
+    <RowLabel
+      key={i}
+      index={i}
+      title={title}
+      width={props.width}
+      rowHeight={props.rowHeight}
+      yOffset={props.yOffset}
+      onMouseEnter={props.onMouseEnter}
+      onMouseLeave={props.onMouseLeave}
+    />
+  ));
+
+  return <g>{titleRows}</g>;
+}
+
+type RowLabelProps = {
+  onMouseEnter: (row: number) => void;
+  onMouseLeave: (row: number) => void;
+
+  title: string;
+  width: number;
+  rowHeight: number;
+  index: number;
+  yOffset: number;
+};
+
+const RowLabel = (props: RowLabelProps) => {
+  const ref = useRef<SVGGElement>(null);
+
+  useEffect(() => {
+    if (ref.current === null) return;
+    select(ref.current)
+      .on('mouseover', () => props.onMouseEnter(props.index))
+      .on('mouseout', () => props.onMouseLeave(props.index));
+  }, [ref.current]);
+
+  return (
+    <g
+      ref={ref}
+      className={'row-' + props.index}
+      transform={
+        'translate(0,' +
+        (props.yOffset + props.rowHeight + props.index * props.rowHeight) +
+        ')'
+      }
+    >
+      <rect
+        width={props.width}
+        height={props.rowHeight}
+        fill={props.index % 2 === 0 ? '#e6e6e6' : '#f5f5f5'}
+      ></rect>
+      <text x="10" y={props.rowHeight / 2} dy=".35em" style={{ fontWeight: 600 }}>
+        {props.title}
+      </text>
+    </g>
+  );
+}
diff --git a/libs/shared/lib/vis/paohvis/components/Tooltip.scss b/libs/shared/lib/vis/paohvis/components/Tooltip.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d62ff563ba2a837caf68b4232bfa9878a2fd1513
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/Tooltip.scss
@@ -0,0 +1,20 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+
+/* istanbul ignore file */
+
+
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+.tooltip {
+  visibility: 'hidden';
+  position: 'absolute';
+  padding: 10px;
+  font-weight: 'bold';
+}
\ No newline at end of file
diff --git a/libs/shared/lib/vis/paohvis/components/Tooltip.tsx b/libs/shared/lib/vis/paohvis/components/Tooltip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..faf885025f6feedfac6ce22226a8be65cdcd9eb4
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/components/Tooltip.tsx
@@ -0,0 +1,30 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+import React from 'react';
+import './Tooltip.scss';
+import { pointer, select } from 'd3';
+
+export default class Tooltip extends React.Component {
+  onMouseOver(e: any) {
+    select('.tooltip')
+      .style('top', pointer(e)[1])
+      .style('left', pointer(e)[0] + 5);
+  }
+
+  render() {
+    return (
+      <div style={{ position: 'absolute' }} className="tooltip">
+        {' '}
+      </div>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/paohvis/models/FilterPredicates.tsx b/libs/shared/lib/vis/paohvis/models/FilterPredicates.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..71c4bf8cf22ee34666b0632539a72d1d329d8e18
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/models/FilterPredicates.tsx
@@ -0,0 +1,33 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+ type TextPredicate = (a1: string, a2: string) => boolean;
+ type NumberPredicate = (a1: number, a2: number) => boolean;
+ type BoolPredicate = (a1: boolean, a2: boolean) => boolean;
+ 
+ /** Predicates to be used on attributes that are strings. */
+ export const textPredicates: Record<string, TextPredicate> = {
+   '==': (a1, a2) => a1 == a2,
+   '!=': (a1, a2) => a1 != a2,
+   contains: (a1, a2) => a1.includes(a2),
+   excludes: (a1, a2) => !a1.includes(a2),
+ };
+ 
+ /** Predicates to be used on attributes that are numbers. */
+ export const numberPredicates: Record<string, NumberPredicate> = {
+   '==': (a1, a2) => a1 == a2,
+   '!=': (a1, a2) => a1 != a2,
+   '>=': (a1, a2) => a1 >= a2,
+   '>': (a1, a2) => a1 > a2,
+   '<=': (a1, a2) => a1 <= a2,
+   '<': (a1, a2) => a1 < a2,
+ };
+ 
+ /** Predicates to be used on attributes that are booleans. */
+ export const boolPredicates: Record<string, BoolPredicate> = {
+   '==': (a1, a2) => a1 == a2,
+   '!=': (a1, a2) => a1 != a2,
+ };
\ No newline at end of file
diff --git a/libs/shared/lib/vis/paohvis/models/PaohvisHolder.test.tsx b/libs/shared/lib/vis/paohvis/models/PaohvisHolder.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e2811c96e5afe7ab8be01604e463dc137a6ac6bc
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/models/PaohvisHolder.test.tsx
@@ -0,0 +1,32 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { describe, it, vi, expect } from 'vitest';
+import PaohvisListener from './PaohvisListener';
+import PaohvisHolder from './PaohvisHolder';
+
+describe('PaohvisHolder', () => {
+  it('should add an remove listeners correctly', () => {
+    const holder: PaohvisHolder = new PaohvisHolder();
+    const mockOnTableMade1 = vi.fn();
+    const mockOnTableMade2 = vi.fn();
+    const l1: PaohvisListener = { onTableMade: mockOnTableMade1 };
+    const l2: PaohvisListener = { onTableMade: mockOnTableMade2 };
+    holder.addListener(l1);
+    holder.addListener(l2);
+
+    expect(mockOnTableMade1).toHaveBeenCalledTimes(0);
+    expect(mockOnTableMade2).toHaveBeenCalledTimes(0);
+    holder.onTableMade('', '', '');
+    expect(mockOnTableMade1).toHaveBeenCalledTimes(1);
+    expect(mockOnTableMade2).toHaveBeenCalledTimes(1);
+
+    holder.removeListener(l2);
+    holder.onTableMade('', '', '');
+    expect(mockOnTableMade1).toHaveBeenCalledTimes(2);
+    expect(mockOnTableMade2).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/libs/shared/lib/vis/paohvis/models/PaohvisHolder.tsx b/libs/shared/lib/vis/paohvis/models/PaohvisHolder.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fe857ef4d04a172135b0fe0904f29e899977a388
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/models/PaohvisHolder.tsx
@@ -0,0 +1,59 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import PaohvisListener from './PaohvisListener';
+
+/** PaohvisHolder is an observer class for notifying listeners when a Paohvis table is made. */
+export default class PaohvisHolder {
+  private paohvisListeners: PaohvisListener[];
+  private entityVertical: string;
+  private entityHorizontal: string;
+  private relationName: string;
+
+  public constructor() {
+    this.paohvisListeners = [];
+    this.entityVertical = '';
+    this.entityHorizontal = '';
+    this.relationName = '';
+  }
+
+  /**
+   * This is called whenever a Paohvis table is made.
+   * @param entityVertical that is on the y-axis of the Paohis table.
+   * @param entityHorizontal that is on the x-axis of the Paohis table.
+   * @param relationName that is used as the hyperedge of the Paohis table.
+   */
+  public onTableMade(entityVertical: string, entityHorizontal: string, relationName: string): void {
+    this.entityVertical = entityVertical;
+    this.entityHorizontal = entityHorizontal;
+    this.relationName = relationName;
+
+    this.notifyListeners();
+  }
+
+  /**
+   * Adds a listener to the observer.
+   * @param listener The listener that we want to add.
+   */
+  public addListener(listener: PaohvisListener): void {
+    this.paohvisListeners.push(listener);
+  }
+
+  /**
+   * Removes a listener from the array of listeners.
+   * @param listener The listener that we want to remove.
+   */
+  public removeListener(listener: PaohvisListener): void {
+    this.paohvisListeners.splice(this.paohvisListeners.indexOf(listener), 1);
+  }
+
+  /** Notifies to all the listeners that a Paohvis table was made. */
+  private notifyListeners(): void {
+    this.paohvisListeners.forEach((listener) =>
+      listener.onTableMade(this.entityVertical, this.entityHorizontal, this.relationName),
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/paohvis/models/PaohvisListener.tsx b/libs/shared/lib/vis/paohvis/models/PaohvisListener.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a93293b035af6c113b2def109c2e07610fa4cee5
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/models/PaohvisListener.tsx
@@ -0,0 +1,16 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/** Interface for the listener of the PaohvisHolder observer class. */
+export default interface PaohvisListener {
+  /**
+   * This is called whenever a Paohvis table is made.
+   * @param entityVertical that is on the y-axis of the Paohis table.
+   * @param entityHorizontal that is on the x-axis of the Paohis table.
+   * @param relationName that is used as the hyperedge of the Paohis table.
+   */
+  onTableMade(entityVertical: string, entityHorizontal: string, relationName: string): void;
+}
diff --git a/libs/shared/lib/vis/paohvis/paohvis.module.scss b/libs/shared/lib/vis/paohvis/paohvis.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..009c546e97e7d866941a9674e17c1831846160d4
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/paohvis.module.scss
@@ -0,0 +1,55 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+$expandButtonSize: 13px;
+
+:export {
+  expandButtonSize: $expandButtonSize;
+}
+
+$tableFontFamily: Arial, Helvetica, sans-serif;
+$tableFontSize: 0.875rem;
+$tableFontWeight: 600;
+:export {
+  tableFontFamily: $tableFontFamily;
+  tableFontSize: $tableFontSize;
+  tableFontWeight: $tableFontWeight;
+}
+
+.container {
+  height: 100%;
+  position: relative;
+  .visContainer {
+    height: 100%;
+    overflow-y: auto;
+    transform: scale(-1, 1);
+    /** 
+    * this fixes the horizontal scrollbar to the bottom of the visContainer,
+    * the transform of the visContainer and the transform of the child div should both be removed for it to work
+    * but the position of the svgs and paohvis are not good, so don't use it yet
+    * overflow-x: auto;
+    * width: 100%;
+    * display: flex;
+    */
+  }
+  text {
+    font-family: $tableFontFamily;
+    font-size: $tableFontSize;
+    font-weight: $tableFontWeight;
+  }
+  #tableMessage {
+    margin: 1em;
+  }
+  #configPanelMessage {
+    margin: 1em;
+  }
+}
diff --git a/libs/shared/lib/vis/paohvis/paohvis.module.scss.d.ts b/libs/shared/lib/vis/paohvis/paohvis.module.scss.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cfe7ee67e5808451cbe2502e65a466f89eb30fad
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/paohvis.module.scss.d.ts
@@ -0,0 +1,9 @@
+declare const classNames: {
+  readonly expandButtonSize: 'expandButtonSize';
+  readonly tableFontFamily: 'tableFontFamily';
+  readonly tableFontSize: 'tableFontSize';
+  readonly tableFontWeight: 'tableFontWeight';
+  readonly container: 'container';
+  readonly visContainer: 'visContainer';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/paohvis/paohvis.stories.tsx b/libs/shared/lib/vis/paohvis/paohvis.stories.tsx
index 59c0ff9750d11a1386fa2a4d0b5b6176bcfb029d..8799d2ec225e0dfe053977f8b92ab5b8bc8713d3 100644
--- a/libs/shared/lib/vis/paohvis/paohvis.stories.tsx
+++ b/libs/shared/lib/vis/paohvis/paohvis.stories.tsx
@@ -1,25 +1,130 @@
-import React from "react";
-import PaohVis from "./paohvis";
-import { ComponentStory, Meta } from "@storybook/react";
+import {
+  assignNewGraphQueryResult,
+  colorPaletteConfigSlice,
+  graphQueryResultSlice,
+  schemaSlice,
+  setSchema,
+} from "../../data-access/store";
+import { GraphPolarisThemeProvider } from "../../data-access/theme";
+import { configureStore } from "@reduxjs/toolkit";
+import { Meta, ComponentStory } from "@storybook/react";
+import { Provider } from "react-redux";
 
-const Component: Meta<typeof PaohVis> = {
+import Paohvis from "./paohvis";
+import { SchemaUtils } from "../../schema/schema-utils";
+import { bigMockQueryResults, simpleSchemaRaw, smallFlightsQueryResults } from "../../mock-data";
+import { simpleSchemaAirportRaw } from "../../mock-data/schema/simpleAirportRaw";
+
+const Component: Meta<typeof Paohvis> = {
   /* 👇 The title prop is optional.
    * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
    * to learn how to generate automatic titles
    */
-  title: "Components/Visualizations/PaohVis",
-  component: PaohVis,
-  decorators: [(story) => <div style={{ padding: "3rem" }}>{story()}</div>],
+  title: "Components/Visualizations/Paohvis",
+  component: Paohvis,
+  decorators: [
+    (story) => (
+      <div
+        style={{
+          width: '80%',
+          height: '80vh',
+        }}
+      >
+        <Provider store={Mockstore}>
+          <GraphPolarisThemeProvider>
+            {story()}
+          </GraphPolarisThemeProvider>
+        </Provider>
+      </div>
+    ),
+  ],
 };
 
-const Template: ComponentStory<typeof PaohVis> = (args) => (
-  <PaohVis {...args} />
-);
+const Mockstore = configureStore({
+  reducer: {
+    schema: schemaSlice.reducer,
+    colorPaletteConfig: colorPaletteConfigSlice.reducer,
+    graphQueryResult: graphQueryResultSlice.reducer,
+  },
+});
 
-export const Primary = Template.bind({});
+export const TestWithData = {
+  args: {
+    loading: false,
+    rowHeight: 50,
+    hyperedgeColumnWidth: 50,
+    gapBetweenRanges: 10,
+  },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    const schema = SchemaUtils.schemaBackend2Graphology({
+      nodes: [
+        {
+          name: '1',
+          attributes: [{ name: 'a', type: 'string' }],
+        },
+      ],
+      edges: [
+        {
+          name: '12', from: '1', to: '1', collection: '1c', attributes: [{ name: 'a', type: 'string' }],
+        },
+      ],
+    });
 
-Primary.args = {
-  content: "Testing header #1",
-};
+    dispatch(setSchema(schema.export()));
+    dispatch(
+      assignNewGraphQueryResult({
+        nodes: [
+          { id: '1/a', attributes: { a: 's1' } },
+          { id: '1/b1', attributes: { a: 's1' } },
+          { id: '1/b2', attributes: { a: 's1' } },
+          { id: '1/b3', attributes: { a: 's1' } },
+        ],
+        edges: [
+          { id: '1c/z1', from: '1/b1', to: '1/a', attributes: { a: 's1' } },
+          // { from: 'b2', to: 'a', attributes: {} },
+          // { from: 'b3', to: 'a', attributes: {} },
+          { id: '1c/z1', from: '1/a', to: '1/b1', attributes: { a: 's1' } },
+        ],
+      })
+    )
+  }
+}
+
+export const TestWithAirportSimple = {
+  args: {
+    loading: false,
+    rowHeight: 50,
+    hyperedgeColumnWidth: 50,
+    gapBetweenRanges: 10,
+  },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaRaw);
+
+    dispatch(setSchema(schema.export()));
+    dispatch(
+      assignNewGraphQueryResult(smallFlightsQueryResults)
+    )
+  }
+}
+
+export const TestWithAirport = {
+  args: {
+    loading: false,
+    rowHeight: 50,
+    hyperedgeColumnWidth: 50,
+    gapBetweenRanges: 10,
+  },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw);
+
+    dispatch(setSchema(schema.export()));
+    dispatch(
+      assignNewGraphQueryResult(bigMockQueryResults)
+    )
+  }
+}
 
 export default Component;
diff --git a/libs/shared/lib/vis/paohvis/paohvis.tsx b/libs/shared/lib/vis/paohvis/paohvis.tsx
index d4797f6f275760911849475435e1e5444b021989..3826c893d49b5dd3946b53f4dcd6a9cac614c17b 100644
--- a/libs/shared/lib/vis/paohvis/paohvis.tsx
+++ b/libs/shared/lib/vis/paohvis/paohvis.tsx
@@ -1,19 +1,653 @@
-import styled from 'styled-components';
-interface Props {
-  content: string;
+
+import { useEffect, useRef, useState } from 'react';
+import styles from './paohvis.module.scss';
+import { Attribute, Relation, AttributeOrigin, EntitiesFromSchema, FilterInfo, FilterType, NodeOrder, PaohvisData, PaohvisNodeOrder, RelationsFromSchema, ValueType } from './Types';
+import { useImmer } from 'use-immer';
+
+import { getWidthOfText } from '../../schema/schema-utils';
+import { useGraphQueryResult, useSchemaGraph } from '../../data-access';
+import { isNodeLinkResult } from '../shared/ResultNodeLinkParserUseCase';
+import { isSchemaResult } from '../shared/SchemaResultType';
+import { calculateAttributesAndRelations, calculateAttributesFromRelation } from './utils/utils';
+import VisConfigPanelComponent from '../shared/VisConfigPanel/VisConfigPanel';
+import { PaohvisFilterComponent } from './components/PaohvisFilterComponent';
+import Tooltip from './components/Tooltip';
+import { pointer, select, selectAll } from 'd3';
+import { HyperEdgeRange } from './components/HyperEdgesRange';
+import ToPaohvisDataParserUseCase from './utils/ToPaohvisDataParserUsecase';
+import MakePaohvisMenu from './components/MakePaohvisMenu';
+import { RowLabelColumn } from './components/RowLabelColumn';
+
+type PaohvisViewModelState = {
+  rowHeight: number;
+  hyperedgeColumnWidth: number;
+  gapBetweenRanges: number;
+
+  hyperedgesOnRow: number[][];
+  allHyperEdges: number[][];
+
+  entitiesFromSchema: EntitiesFromSchema;
+  relationsFromSchema: RelationsFromSchema;
+  entityVertical: string;
+  entityHorizontal: string;
+  chosenRelation: string;
+  isEntityVerticalEqualToRelationFrom: boolean;
+  nodeOrder: { orderBy: NodeOrder, isReverseOrder: boolean };
+  paohvisFilters: { nodeFilters: FilterInfo[], edgeFilters: FilterInfo[] };
+  axisInfo: {
+    selectedAttribute: { name: string, type: ValueType, origin: AttributeOrigin },
+    relation: { collection: string, from: string, to: string },
+    isYAxisEntityEqualToRelationFrom: boolean,
+  };
+
 }
 
-const Div = styled.div`
-  background-color: red;
-  font: 'Arial';
-`;
+type Props = {
+  rowHeight: number;
+  hyperedgeColumnWidth: number;
+  gapBetweenRanges: number;
+  data?: PaohvisData;
+};
+
+export const PaohVis = (props: Props) => {
+  const svgRef = useRef<SVGSVGElement>(null);
+  const graphQueryResult = useGraphQueryResult();
+  const schema = useSchemaGraph();
+
+  const [data, setData] = useState<PaohvisData>({ rowLabels: [], hyperEdgeRanges: [], maxRowLabelWidth: 0 },);
+  const [viewModel, setViewModel] = useImmer<PaohvisViewModelState>({
+    rowHeight: 30,
+    hyperedgeColumnWidth: 20,
+    gapBetweenRanges: 5,
+
+    hyperedgesOnRow: [],
+    allHyperEdges: [],
+    entitiesFromSchema: { entityNames: [], attributesPerEntity: {}, relationsPerEntity: {} },
+    relationsFromSchema: {
+      relationCollection: [], relationNames: {}, attributesPerRelation: {},
+    },
+
+    entityVertical: '',
+    entityHorizontal: '',
+    chosenRelation: '',
+    isEntityVerticalEqualToRelationFrom: true,
+    nodeOrder: { orderBy: NodeOrder.degree, isReverseOrder: false, },
+    paohvisFilters: { nodeFilters: [], edgeFilters: [] },
+    axisInfo: {
+      selectedAttribute: {
+        name: '', type: ValueType.noAttribute, origin: AttributeOrigin.noAttribute,
+      },
+      relation: { collection: '', from: '', to: '' },
+      isYAxisEntityEqualToRelationFrom: true,
+    }
+  });
+
+  // const [state, setState] = useState({
+  //   entityVertical: '',
+  //   entityHorizontal: '',
+  //   relation: '',
+  //   isEntityFromRelationFrom: true,
+  // });
+
+
+
+  //
+  // Methods
+  //
+
+  /**
+   * Filters the current PAOHvis visualization.
+   * @param {boolean} isTargetEntity Tells if the the filter you want to remove is applied to an entity or relation.
+   * @param {string} entityOrRelationType This is the type of the target.
+   * @param {string} attribute This is the chosen attribute of the target you want to filter on.
+   * @param {string} predicate This is the chosen predicate of how you want to compare the attribute.
+   * @param {string} compareValue This is the chosen value which you want to compare the attribute values to.
+   */
+  function onClickFilterButton(
+    isTargetEntity: boolean,
+    entityOrRelationType: string,
+    attribute: string,
+    predicate: string,
+    compareValue: string,
+  ): void {
+    const attributeName: string = attribute.split(':')[0];
+    const attributeType: string = attribute.split(':')[1];
+    const lowerCaseCompareValue = compareValue.toLowerCase();
+    let compareValueTyped: any;
+    // the compareValue must be typed based on the type of the attribute.
+    switch (attributeType) {
+      case ValueType.number:
+        if (!Number.isNaN(Number(compareValue))) compareValueTyped = Number(compareValue);
+        else throw new Error('Error: This is not a correct input');
+        break;
+      case ValueType.bool:
+        if (lowerCaseCompareValue == 'true') compareValueTyped = true;
+        else if (lowerCaseCompareValue == 'false') compareValueTyped = false;
+        else throw new Error('Error: This is not a correct input');
+        break;
+      default:
+        compareValueTyped = compareValue;
+        break;
+    }
+    const newFilter: FilterInfo = {
+      targetGroup: entityOrRelationType,
+      attributeName: attributeName,
+      value: compareValueTyped,
+      predicateName: predicate,
+    };
+
+    if (isTargetEntity) {
+      // The current filter is removed and then the new one is added.
+      // TODO: to keep all filters, delete this line and improve config menu to see all filters instead of only the last one.
+      setViewModel((draft) => {
+        draft.paohvisFilters.nodeFilters.pop();
+        draft.paohvisFilters.nodeFilters.push(newFilter);
+        return draft;
+      });
+    } else {
+      // The current filter is removed and then the new one is added.
+      // TODO: to keep all filters, delete this line and improve config menu to see all filters instead of only the last one.
+      setViewModel((draft) => {
+        draft.paohvisFilters.edgeFilters.pop();
+        draft.paohvisFilters.edgeFilters.push(newFilter);
+        return draft;
+      });
+    }
+  };
+
+  /**
+   * Resets the current chosen filter.
+   * @param {boolean} isTargetEntity Tells if the the filter you want to remove is applied to an entity or relation.
+   */
+  function onClickResetButton(isTargetEntity: boolean): void {
+    setViewModel((draft) => {
+      if (isTargetEntity) draft.paohvisFilters.nodeFilters.pop();
+      else draft.paohvisFilters.edgeFilters.pop();
+      return draft;
+    });
+  };
+
+  /**
+   * SHOULD NOT BE HERE: move to tooltip
+   * This calculates a new position based on the current position of the mouse.
+   * @param {React.MouseEvent} e This is the position of the mouse.
+   */
+  function onMouseMoveToolTip(e: React.MouseEvent) {
+    select('.tooltip')
+      .style('top', pointer(e)[1] - 25 + 'px')
+      .style('left', pointer(e)[0] + 5 + 'px');
+  };
+
+  /**
+   * Handles the visual changes when you enter a certain hyperEdge on hovering.
+   * @param colIndex This is the index which states in which column you are hovering.
+   * @param nameToShow This is the name of the entity which must be shown when you are hovering on this certain hyperEdge.
+   */
+  function onMouseEnterHyperEdge(colIndex: number, nameToShow: string): void {
+    highlightAndFadeHyperEdges([colIndex]);
+    highlightAndFadeRows(new Set(viewModel.allHyperEdges[colIndex]));
+    showColName(nameToShow);
+  };
+
+  /**
+   * Handles the visual changes when you leave a certain hyperEdge on hovering.
+   * @param colIndex This is the index which states in which column you were hovering.
+   */
+  function onMouseLeaveHyperEdge(colIndex: number): void {
+    unHighlightAndUnFadeHyperEdges([colIndex]);
+    unHighlightAndUnFadeRows(new Set(viewModel.allHyperEdges[colIndex]));
+    hideColName();
+  };
+
+
+  /**
+   * This makes sure that the correct rows are highlighted and the correct rows are faded.
+   * @param {Set<number>} rows This is the set with the numbers of the rows which must be highlighted.
+   */
+  function highlightAndFadeRows(rows: Set<number>) {
+    rows.forEach((row) => highlightRow(row));
+
+    const rowsToFade = [];
+    for (let i = 0; i < data.rowLabels.length; i++) if (!rows.has(i)) rowsToFade.push(i);
+    rowsToFade.forEach((row) => fadeRow(row));
+  }
+
+  /**
+   * This makes sure that the correct rows are unhighlighted and the correct rows are unfaded.
+   * @param {Set<number>} rows This is the set with the numbers of the rows which must be unhighlighted.
+   */
+  function unHighlightAndUnFadeRows(rows: Set<number>) {
+    rows.forEach((row) => unHighlightRow(row));
+
+    const rowsToUnFade = [];
+    for (let i = 0; i < data.rowLabels.length; i++) if (!rows.has(i)) rowsToUnFade.push(i);
+    rowsToUnFade.forEach((row) => unFadeRow(row));
+  }
+
+  /**
+   * This makes sure that the correct hyperEdges are highlighted and the correct rows are faded.
+   * @param {number[]} hyperEdges This is the list with the numbers of the hyperEdges which must be highlighted.
+   */
+  function highlightAndFadeHyperEdges(hyperEdges: number[]) {
+    hyperEdges.forEach((hyperEdge) => highlightHyperedge(hyperEdge));
+
+    const colsToFade = [];
+    for (let i = 0; i < viewModel.allHyperEdges.length; i++)
+      if (!hyperEdges.includes(i)) colsToFade.push(i);
+    colsToFade.forEach((col) => fadeHyperedge(col));
+  }
+
+  /**
+   * This makes sure that the correct hyperEdges are unhighlighted and the correct rows are unfaded.
+   * @param {number[]} hyperEdges This is the list with the numbers of the hyperEdges which must be unhighlighted.
+   */
+  function unHighlightAndUnFadeHyperEdges(hyperEdges: number[]) {
+    hyperEdges.forEach((hyperEdge) => unHighlightHyperedge(hyperEdge));
+
+    const colsToUnFade = [];
+    for (let i = 0; i < viewModel.allHyperEdges.length; i++)
+      if (!hyperEdges.includes(i)) colsToUnFade.push(i);
+    colsToUnFade.forEach((col) => unFadeHyperedge(col));
+  }
+
+  /**
+   * This highlights one row given its index.
+   * @param {number} rowIndex This is the index of the row that must be highlighted.
+   */
+  function highlightRow(rowIndex: number) {
+    const row = selectAll('.row-' + rowIndex);
+    row.select('text').transition().duration(120).style('font-size', '15px');
+  }
+
+  /**
+   * This unhighlights one row given its index.
+   * @param {number} rowIndex This is the index of the row that must be unhighlighted.
+   */
+  function unHighlightRow(rowIndex: number) {
+    const row = selectAll('.row-' + rowIndex);
+    row.select('text').transition().duration(120).style('font-size', '14px');
+  }
+
+  /**
+   * This highlights one hyperEdge given its index.
+   * @param {number} columnIndex This is the index of the hyperEdge that must be highlighted.
+   */
+  function highlightHyperedge(columnIndex: number) {
+    const hyperedge = select('.col-' + columnIndex);
+    hyperedge.selectAll('circle').transition().duration(120).style('fill', 'orange');
+  }
+
+  /**
+   * This unhighlights one hyperEdge given its index.
+   * @param {number} columnIndex This is the index of the hyperEdge that must be unhighlighted.
+   */
+  function unHighlightHyperedge(columnIndex: number) {
+    const hyperedge = select('.col-' + columnIndex);
+    hyperedge.selectAll('circle').transition().duration(120).style('fill', 'white');
+  }
+
+  /**
+   * This fades one row given its index.
+   * @param {number} rowIndex This is the index of the row that must be faded.
+   */
+  function fadeRow(rowIndex: number) {
+    const row = selectAll('.row-' + rowIndex);
+    row.select('text').attr('opacity', '.3');
+  }
 
-const PoahVis = (props: Props) => {
+  /**
+   * This unfades one row given its index.
+   * @param {number} rowIndex This is the index of the row that must be unfaded.
+   */
+  function unFadeRow(rowIndex: number) {
+    const row = selectAll('.row-' + rowIndex);
+    row.select('text').attr('opacity', '1');
+  }
+
+  /**
+   * This fades one hyperEdge given its index.
+   * @param {number} columnIndex This is the index of the hyperEdge that must be faded.
+   */
+  function fadeHyperedge(columnIndex: number) {
+    const hyperedge = select('.col-' + columnIndex);
+    hyperedge.selectAll('circle').attr('opacity', '.3');
+    hyperedge.selectAll('line').attr('opacity', '.3');
+  }
+
+  /**
+   * This unfades one hyperEdge given its index.
+   * @param {number} columnIndex This is the index of the hyperEdge that must be unfaded.
+   */
+  function unFadeHyperedge(columnIndex: number) {
+    const hyperedge = select('.col-' + columnIndex);
+    hyperedge.selectAll('circle').attr('opacity', '1');
+    hyperedge.selectAll('line').attr('opacity', '1');
+  }
+
+  /**
+   * This shows the name when hovering on a hyperEdge.
+   * @param {string} nameToShow This is the name that must be shown.
+   */
+  function showColName(nameToShow: string): void {
+    select('.tooltip').text(nameToShow).style('visibility', 'visible');
+  };
+
+  /** This hides the name when leaving a hyperEdge. */
+  function hideColName(): void {
+    select('.tooltip').style('visibility', 'hidden');
+  };
+
+
+  /**
+ * Makes the new PAOHvis visualisation.
+ * @param {string} entityVertical This is the name of the vertical entity (so on the left).
+ * @param {string} entityHorizontal This is the name of the horizontal entity (so at the top).
+ * @param {string} relationName This is the (collection)-name of the relation.
+ * @param {boolean} isEntityVerticalEqualToRelationFrom Tells if the vertical entity is the from or to of the relation.
+ * @param {Attribute} chosenAttribute This is the attribute on which the PAOHvis must be grouped by.
+ * @param {PaohvisNodeOrder} nodeOrder Defines the sorting order of the PAOHvis visualisation.
+ */
+  function onClickMakeButton(
+    entityVertical: string,
+    entityHorizontal: string,
+    relationName: string,
+    isEntityVerticalEqualToRelationFrom: boolean,
+    chosenAttribute: Attribute,
+    nodeOrder: PaohvisNodeOrder,
+  ): void {
+    setViewModel(draft => {
+      draft.entityVertical = entityVertical;
+      draft.entityHorizontal = entityHorizontal;
+      draft.chosenRelation = relationName;
+      draft.isEntityVerticalEqualToRelationFrom = isEntityVerticalEqualToRelationFrom;
+      draft.nodeOrder = nodeOrder;
+
+      return draft;
+    });
+    setAxisInfo(relationName, isEntityVerticalEqualToRelationFrom, chosenAttribute);
+  };
+
+  /**
+   * This method parses data, makes a new Paohvis Table and lets the view re-render.
+   */
+  function makePaohvisTable() {
+    // set new data
+    const newData = new ToPaohvisDataParserUseCase(graphQueryResult).parseQueryResult(viewModel.axisInfo, viewModel.nodeOrder);
+    console.log(newData);
+
+    setData(newData);
+  }
+
+
+  /**
+   * This method makes and sets a new PaohvisAxisInfo.
+   * @param relationName is the relation that will be used in the Paohvis table.
+   * @param isEntityVerticalEqualToRelationFrom is true when the entity on the y-axis belongs to the 'from' part of the relation.
+   * @param chosenAttribute is the chosen attribute that will be used to divide the HyperEdges into HyperEdgeRanges.
+   */
+  function setAxisInfo(
+    relationName: string,
+    isEntityVerticalEqualToRelationFrom: boolean,
+    chosenAttribute: Attribute,
+  ): void {
+    const namesOfEntityTypes = viewModel.relationsFromSchema.relationNames[relationName].split(':');
+    const relation: Relation = {
+      collection: relationName,
+      from: namesOfEntityTypes[0],
+      to: namesOfEntityTypes[1],
+    };
+
+    // set axisInfo
+    setViewModel(draft => {
+      draft.axisInfo = {
+        selectedAttribute: chosenAttribute,
+        relation: relation,
+        isYAxisEntityEqualToRelationFrom: isEntityVerticalEqualToRelationFrom,
+      };
+      return draft;
+    });
+  }
+
+  /**
+   * Handles the visual changes when you enter a certain row on hovering.
+   * @param {number} rowIndex This is the index which states in which row you are hovering.
+   */
+  function onMouseEnterRow(rowIndex: number): void {
+    const rowsToHighlight = new Set<number>();
+    const colsToHighlight: number[] = [];
+    viewModel.hyperedgesOnRow[rowIndex].forEach((hyperedge) => {
+      colsToHighlight.push(hyperedge);
+
+      viewModel.allHyperEdges[hyperedge].forEach((row) => {
+        rowsToHighlight.add(row);
+      });
+    });
+
+    highlightAndFadeRows(rowsToHighlight);
+    highlightAndFadeHyperEdges(colsToHighlight);
+  };
+
+  /**
+   * Handles the visual changes when you leave a certain row on hovering.
+   * @param {number} rowIndex This is the index which states in which row you were hovering.
+   */
+  function onMouseLeaveRow(rowIndex: number): void {
+    const rowsToUnHighlight = new Set<number>();
+    const colsToUnHighlight: number[] = [];
+    viewModel.hyperedgesOnRow[rowIndex].forEach((hyperedge) => {
+      colsToUnHighlight.push(hyperedge);
+
+      viewModel.allHyperEdges[hyperedge].forEach((row) => {
+        rowsToUnHighlight.add(row);
+      });
+    });
+    unHighlightAndUnFadeRows(rowsToUnHighlight);
+    unHighlightAndUnFadeHyperEdges(colsToUnHighlight);
+  };
+
+  //
+  // Reactivity
+  //
+
+  useEffect(() => {
+    // if (isSchemaResult(schema)) {
+    setViewModel(draft => {
+      // When a schema is received, extract the entity names, and attributes per data type
+      draft.entitiesFromSchema = calculateAttributesAndRelations(schema);
+      draft.relationsFromSchema = calculateAttributesFromRelation(schema);
+      return draft;
+    });
+    // } else {
+    //   console.error('Invalid schema!')
+    // }
+  }, [schema]);
+
+
+  /** This method filters and makes a new Paohvis table. */
+  useEffect(() => {
+    if (isNodeLinkResult(graphQueryResult)) {
+      makePaohvisTable();
+    } else {
+      console.error('Invalid query result!')
+    }
+  }, [viewModel.paohvisFilters, graphQueryResult, viewModel.axisInfo, viewModel.nodeOrder]);
+
+
+  useEffect(() => {
+    /**
+     * Recalculates the hyperEdges and hyperEdgeRanges for the state.
+     */
+    setViewModel(draft => {
+      // Create connecteHyperedgesInColumns, to quickly lookup which hyperedges are on a row
+      draft.allHyperEdges = [];
+
+      data.hyperEdgeRanges.forEach((hyperEdgeRange) => {
+        hyperEdgeRange.hyperEdges.forEach((hyperEdge) => {
+          draft.allHyperEdges.push(hyperEdge.indices);
+        });
+      });
+
+      draft.hyperedgesOnRow = data.rowLabels.map(() => []);
+      draft.allHyperEdges.forEach((hyperedge, i) => {
+        hyperedge.forEach((row) => {
+          draft.hyperedgesOnRow[row].push(i);
+        });
+      });
+      return draft;
+    });
+  }, [data]);
+
+  //
+  // RENDER
+  //
+
+  const hyperEdgeRanges = data.hyperEdgeRanges;
+  const rowLabelColumnWidth = data.maxRowLabelWidth;
+  const hyperedgeColumnWidth = props.hyperedgeColumnWidth;
+
+  //calculate yOffset
+  let maxColWidth = 0;
+  hyperEdgeRanges.forEach((hyperEdgeRange) => {
+    const textLength = getWidthOfText(
+      hyperEdgeRange.rangeText,
+      styles.tableFontFamily,
+      styles.tableFontSize,
+      styles.tableFontWeight,
+    );
+    if (textLength > maxColWidth) maxColWidth = textLength;
+  });
+  const columnLabelAngleInRadians = Math.PI / 6;
+  const yOffset = Math.sin(columnLabelAngleInRadians) * maxColWidth;
+  const expandButtonWidth = parseFloat(styles.expandButtonSize.split('px')[0]);
+  const margin = 5;
+
+  //calc table width
+  let tableWidth = 0;
+  let tableWidthWithExtraColumnLabelWidth = 0;
+  hyperEdgeRanges.forEach((hyperEdgeRange) => {
+    const columnLabelWidth =
+      Math.cos(columnLabelAngleInRadians) *
+      getWidthOfText(
+        hyperEdgeRange.rangeText,
+        styles.tableFontFamily,
+        styles.tableFontSize,
+        styles.tableFontWeight,
+      );
+    const columnWidth =
+      hyperEdgeRange.hyperEdges.length * hyperedgeColumnWidth + props.gapBetweenRanges * 3;
+
+    tableWidth += columnWidth;
+
+    if (columnLabelWidth > columnWidth) {
+      const currentTableWidthWithLabel = tableWidth + columnLabelWidth;
+      if (currentTableWidthWithLabel > tableWidthWithExtraColumnLabelWidth)
+        tableWidthWithExtraColumnLabelWidth = currentTableWidthWithLabel;
+    }
+    if (tableWidth > tableWidthWithExtraColumnLabelWidth)
+      tableWidthWithExtraColumnLabelWidth = tableWidth;
+  });
+  tableWidthWithExtraColumnLabelWidth += rowLabelColumnWidth + expandButtonWidth + margin;
+
+  //make  all hyperEdgeRanges
+  let colOffset = 0;
+  let hyperEdgeRangeColumns: JSX.Element[] = [];
+  hyperEdgeRanges.forEach((hyperEdgeRange, i) => {
+    hyperEdgeRangeColumns.push(
+      <HyperEdgeRange
+        key={i}
+        index={i}
+        text={hyperEdgeRange.rangeText}
+        hyperEdges={hyperEdgeRange.hyperEdges}
+        nRows={data.rowLabels.length}
+        colOffset={colOffset}
+        xOffset={rowLabelColumnWidth}
+        yOffset={yOffset}
+        rowHeight={props.rowHeight}
+        hyperedgeColumnWidth={hyperedgeColumnWidth}
+        gapBetweenRanges={props.gapBetweenRanges}
+        onMouseEnter={onMouseEnterHyperEdge}
+        onMouseLeave={onMouseLeaveHyperEdge}
+      />,
+    );
+    colOffset += hyperEdgeRange.hyperEdges.length;
+  });
+
+  //display message when there is no Paohvis table to display
+  let tableMessage = <span></span>;
+  let configPanelMessage = <div></div>;
+  if (data.rowLabels.length === 0) {
+    tableMessage = (
+      <div id={styles.tableMessage}> Please choose a valid PAOHvis configuration </div>
+    );
+    configPanelMessage = (
+      <div id={styles.configPanelMessage}> Please make a PAOHvis table first </div>
+    );
+  }
+
+  // returns the whole PAOHvis visualisation panel
   return (
-    <Div>
-      <h1>{props.content}</h1>
-    </Div>
+    <div className={styles.container}>
+      <div className={styles.visContainer}>
+        <div style={{ transform: 'scale(-1,1)' }}>
+          <div
+            style={{ width: '100%', height: '100%', overflow: 'auto' }}
+            onMouseMove={onMouseMoveToolTip}
+          >
+            <MakePaohvisMenu // render the MakePAOHvisMenu
+              makePaohvis={onClickMakeButton}
+            />
+            {tableMessage}
+            <svg
+              ref={svgRef}
+              style={{
+                width: tableWidthWithExtraColumnLabelWidth,
+                height: yOffset + (data.rowLabels.length + 1) * props.rowHeight,
+              }}
+            >
+              <RowLabelColumn // render the PAOHvis itself
+                onMouseEnter={onMouseEnterRow}
+                onMouseLeave={onMouseLeaveRow}
+                titles={data.rowLabels}
+                width={rowLabelColumnWidth}
+                rowHeight={viewModel.rowHeight}
+                yOffset={yOffset}
+              />
+              {hyperEdgeRangeColumns}
+            </svg>
+            <Tooltip />
+          </div>
+        </div>
+      </div>
+      <VisConfigPanelComponent>
+        {configPanelMessage}
+        <PaohvisFilterComponent // render the PaohvisFilterComponent with all three different filter options
+          axis={FilterType.yaxis}
+          filterPaohvis={onClickFilterButton}
+          resetFilter={onClickResetButton}
+          entityVertical={viewModel.entityVertical}
+          entityHorizontal={viewModel.entityHorizontal}
+          relationName={viewModel.chosenRelation}
+        />
+        <PaohvisFilterComponent
+          axis={FilterType.xaxis}
+          filterPaohvis={onClickFilterButton}
+          resetFilter={onClickResetButton}
+          entityVertical={viewModel.entityVertical}
+          entityHorizontal={viewModel.entityHorizontal}
+          relationName={viewModel.chosenRelation}
+        />
+        <PaohvisFilterComponent
+          axis={FilterType.edge}
+          filterPaohvis={onClickFilterButton}
+          resetFilter={onClickResetButton}
+          entityVertical={viewModel.entityVertical}
+          entityHorizontal={viewModel.entityHorizontal}
+          relationName={viewModel.chosenRelation}
+        />
+      </VisConfigPanelComponent>
+    </div>
   );
 };
 
-export default PoahVis;
+
+export default PaohVis;
\ No newline at end of file
diff --git a/libs/shared/lib/vis/paohvis/paohvisViewModel.tsx b/libs/shared/lib/vis/paohvis/paohvisViewModel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/libs/shared/lib/vis/paohvis/utils/AttributesFilterUseCase.tsx b/libs/shared/lib/vis/paohvis/utils/AttributesFilterUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..725d7a88b226d1bb89d49e0382cc56c23fc793a4
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/AttributesFilterUseCase.tsx
@@ -0,0 +1,141 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { FilterInfo, PaohvisFilters } from '../Types';
+import {
+  AxisType,
+  isNotInGroup,
+} from '../../shared/ResultNodeLinkParserUseCase';
+import { boolPredicates, numberPredicates, textPredicates } from '../models/FilterPredicates';
+import { Edge, GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access';
+
+/** This is used to filter the data for Paohvis. */
+export default class AttributeFilterUsecase {
+  /** Applies all filters to the query result. */
+  public static applyFilters(
+    queryResult: GraphQueryResult,
+    paohvisFilters: PaohvisFilters,
+  ): GraphQueryResult {
+    //apply the filters to the nodes
+    let filteredNodes = queryResult.nodes;
+    paohvisFilters.nodeFilters.forEach(
+      (filter) => (filteredNodes = this.filterAxis(filteredNodes, filter)),
+    );
+
+    //filter out the unused edges
+    const nodeIds = getIds(filteredNodes);
+    let filteredEdges = filterUnusedEdges(nodeIds, queryResult.edges);
+
+    //apply the filters to the edges
+    paohvisFilters.edgeFilters.forEach(
+      (filter) => (filteredEdges = this.filterAxis(filteredEdges, filter)),
+    );
+
+    //filter out unused nodes
+    filteredNodes = filterUnusedNodes(filteredNodes, filteredEdges);
+
+    return { ...queryResult, nodes: filteredNodes, edges: filteredEdges };
+  }
+
+  /**
+   * Gets the correct predicate that belongs to the predicateName and
+   * filters the given array on the specified target group on the given attribute with the predicate and given value,
+   * @param axisNodesOrEdges is the array that should be filtered.
+   * @param filter is the filter that should be applied.
+   * @returns the array filtered with the correct predicate.
+   */
+  public static filterAxis<T extends Node | Edge>(axisNodesOrEdges: T[], filter: FilterInfo): T[] {
+    if (typeof filter.value == 'boolean') return filterBoolAttributes(axisNodesOrEdges, filter);
+
+    if (typeof filter.value == 'number') return filterNumberAttributes(axisNodesOrEdges, filter);
+
+    if (typeof filter.value == 'string') return filterTextAttributes(axisNodesOrEdges, filter);
+
+    throw new Error('Filter on this type is not supported.');
+  }
+}
+
+/** Filters the given array on the designated boolean attribute with the given predicate. */
+function filterBoolAttributes<T extends Node | Edge>(axisNodesOrEdges: T[], filter: FilterInfo): T[] {
+  const predicate = boolPredicates[filter.predicateName];
+  if (predicate == undefined) throw new Error('Predicate does not exist');
+
+  const resultNodesOrEdges = axisNodesOrEdges.filter((nodeOrEdge) => {
+    const currentAttribute = nodeOrEdge.attributes[filter.attributeName] as boolean;
+    return (
+      isNotInGroup(nodeOrEdge, filter.targetGroup) || predicate(currentAttribute, filter.value)
+    );
+  });
+
+  return resultNodesOrEdges;
+}
+
+/** Filters the given array on the designated number attribute with the given predicate. */
+function filterNumberAttributes<T extends Node | Edge>(axisNodesOrEdges: T[], filter: FilterInfo): T[] {
+  const predicate = numberPredicates[filter.predicateName];
+  if (predicate == undefined) throw new Error('Predicate does not exist');
+
+  const resultNodesOrEdges = axisNodesOrEdges.filter((nodeOrEdge) => {
+    const currentAttribute = nodeOrEdge.attributes[filter.attributeName] as number;
+    return (
+      isNotInGroup(nodeOrEdge, filter.targetGroup) || predicate(currentAttribute, filter.value)
+    );
+  });
+
+  return resultNodesOrEdges;
+}
+
+/** Filters the given array on the designated string attribute with the given predicate. */
+function filterTextAttributes<T extends Node | Edge>(axisNodesOrEdges: T[], filter: FilterInfo): T[] {
+  const predicate = textPredicates[filter.predicateName];
+  if (predicate == undefined) throw new Error('Predicate does not exist');
+
+  const resultNodesOrEdges = axisNodesOrEdges.filter((nodeOrEdge) => {
+    const currentAttribute = nodeOrEdge.attributes[filter.attributeName] as string;
+    return (
+      isNotInGroup(nodeOrEdge, filter.targetGroup) || predicate(currentAttribute, filter.value)
+    );
+  });
+
+  return resultNodesOrEdges;
+}
+
+/** Gets the ids from the given nodes or edges array. */
+export function getIds(nodesOrEdges: AxisType[]): string[] {
+  const result: string[] = [];
+  nodesOrEdges.forEach((nodeOrEdge) => result.push(nodeOrEdge.id || 'unknown id'));
+  return result;
+}
+
+/**
+ * Filters out the edges that are from or to a node that is not being used anymore.
+ * @param ids are the ids of the nodes.
+ * @param edges are the edges that should be filtered.
+ * @returns a filtered array where all edges are used.
+ */
+export function filterUnusedEdges(ids: string[], edges: Edge[]): Edge[] {
+  const filteredEdges: Edge[] = [];
+  edges.forEach(
+    (edge) => ids.includes(edge.from) && ids.includes(edge.to) && filteredEdges.push(edge),
+  );
+  return filteredEdges;
+}
+
+/** Filters out the nodes that are not used by any edge. */
+function filterUnusedNodes(nodes: Node[], edges: Edge[]): Node[] {
+  const filteredNodes: Node[] = [];
+  const frequencyDict: Record<string, number> = {};
+  nodes.forEach((node) => (frequencyDict[node.id] = 0));
+
+  edges.forEach((edge) => {
+    frequencyDict[edge.from] += 1;
+    frequencyDict[edge.to] += 1;
+  });
+
+  nodes.forEach((node) => frequencyDict[node.id] > 0 && filteredNodes.push(node));
+
+  return filteredNodes;
+}
diff --git a/libs/shared/lib/vis/paohvis/utils/CalcEntitiesFromQueryResult.tsx b/libs/shared/lib/vis/paohvis/utils/CalcEntitiesFromQueryResult.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3474abc6c55673c189b8dfb80cfcd04a88846607
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/CalcEntitiesFromQueryResult.tsx
@@ -0,0 +1,23 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { GraphQueryResult } from "@graphpolaris/shared/lib/data-access";
+import { getGroupName } from "../../shared/ResultNodeLinkParserUseCase";
+
+
+/**
+ * This calculates all entities from the query result.
+ * @param {NodeLinkResultType} message This is the message with all the information about the entities and relations.
+ * @returns {string[]} All names of the entities which are in the query result.
+ */
+export default function calcEntitiesFromQueryResult(message: GraphQueryResult): string[] {
+  const entityTypesFromQueryResult: string[] = [];
+  message.nodes.forEach((node) => {
+    const group = getGroupName(node);
+    if (!entityTypesFromQueryResult.includes(group)) entityTypesFromQueryResult.push(group);
+  });
+  return entityTypesFromQueryResult;
+}
diff --git a/libs/shared/lib/vis/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx b/libs/shared/lib/vis/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..834ec22a6c5c53dd3497506794c069f73617eca5
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx
@@ -0,0 +1,134 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import { SchemaGraph, SchemaGraphologyEdge } from '@graphpolaris/shared/lib/schema';
+import {
+  AttributeNames,
+  EntitiesFromSchema,
+  RelationsFromSchema,
+} from '../Types';
+
+/** Use case for retrieving entity names, relation names and attribute names from a schema result. */
+export default class CalcEntityAttrAndRelNamesFromSchemaUseCase {
+  /**
+   * Takes a schema result and calculates all the entity names, and relation names and attribute names per entity.
+   * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity.
+   */
+  public static calculateAttributesAndRelations(schemaResult: SchemaGraph): EntitiesFromSchema {
+    const attributesPerEntity: Record<string, AttributeNames> = this.calculateAttributes(schemaResult,);
+    const relationsPerEntity: Record<string, string[]> = this.calculateRelations(schemaResult);
+
+    return {
+      entityNames: schemaResult.nodes.filter(node => (node?.attributes?.name !== undefined)).map((node) => node.attributes!.name),
+      attributesPerEntity,
+      relationsPerEntity,
+    };
+  }
+
+  /**
+   * Takes a schema result and calculates all the attribute names per entity.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {Record<string, AttributeNames>} All attribute names per entity.
+   */
+  public static calculateAttributes(schemaResult: SchemaGraph): Record<string, AttributeNames> {
+    const attributesPerEntity: Record<string, AttributeNames> = {};
+    // Go through each entity.
+    schemaResult.nodes.forEach((node) => {
+      if (node.attributes === undefined || node.attributes.name === undefined) {
+        console.error('ERROR: Node has no attributes/name.', node);
+        return;
+      }
+
+      // Extract the attribute names per datatype for each entity.
+      const textAttributeNames: string[] = [];
+      const boolAttributeNames: string[] = [];
+      const numberAttributeNames: string[] = [];
+      node.attributes.attributes.forEach((attr) => {
+        if (attr.type == 'string') textAttributeNames.push(attr.name);
+        else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name);
+        else boolAttributeNames.push(attr.name);
+      });
+
+      // Create a new object with the arrays with attribute names per datatype.
+      attributesPerEntity[node.attributes.name] = {
+        textAttributeNames,
+        boolAttributeNames,
+        numberAttributeNames,
+      };
+    });
+    return attributesPerEntity;
+  }
+
+  /**
+   * Takes a schema result and calculates all the relation names per entity.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {Record<string, AttributeNames>} All relation (from and to) names per entity.
+   */
+  public static calculateRelations(schemaResult: SchemaGraph): Record<string, string[]> {
+    const relationsPerEntity: Record<string, string[]> = {};
+    // Go through each relation.
+    schemaResult.edges.forEach((edge) => {
+      if (edge.attributes === undefined || edge.attributes.name === undefined) {
+        console.error('ERROR: Edge has no attributes/name.', edge);
+        return;
+      }
+
+      // Extract the from-node-name (collection name) from every relation.
+      if (relationsPerEntity[edge.attributes.from]) relationsPerEntity[edge.attributes.from].push(edge.attributes.collection);
+      else relationsPerEntity[edge.attributes.from] = [edge.attributes.collection];
+      // Extract the to-node-name (collection name) from every relation.
+      if (relationsPerEntity[edge.attributes.to]) relationsPerEntity[edge.attributes.to].push(edge.attributes.collection);
+      else relationsPerEntity[edge.attributes.to] = [edge.attributes.collection];
+    });
+    return relationsPerEntity;
+  }
+
+  /**
+   * Takes a schema result and calculates all the relation collection names, and relation names and attribute names per relation.
+   * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity.
+   */
+  public static calculateAttributesFromRelation(schemaResult: SchemaGraph): RelationsFromSchema {
+    const relationCollections: string[] = [];
+    const attributesPerRelation: Record<string, AttributeNames> = {};
+    const nameOfCollectionPerRelation: Record<string, string> = {};
+    // Go through each relation.
+    schemaResult.edges.forEach((edge) => {
+      if (edge.attributes === undefined || edge.attributes.name === undefined) {
+        console.error('ERROR: Edge has no attributes/name.', edge);
+        return;
+      }
+
+      if (!nameOfCollectionPerRelation[edge.attributes.collection]) {
+        relationCollections.push(edge.attributes.collection);
+        nameOfCollectionPerRelation[edge.attributes.collection] = `${edge.attributes.name}`;
+      }
+      // Extract the attribute names per datatype for each relation.
+      const textAttributeNames: string[] = [];
+      const boolAttributeNames: string[] = [];
+      const numberAttributeNames: string[] = [];
+      edge.attributes.attributes.forEach((attr: SchemaGraphologyEdge) => {
+        if (attr.type == 'string') textAttributeNames.push(attr.name);
+        else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name);
+        else boolAttributeNames.push(attr.name);
+      });
+
+      // Create a new object with the arrays with attribute names per datatype.
+      attributesPerRelation[edge.attributes.collection] = {
+        textAttributeNames,
+        boolAttributeNames,
+        numberAttributeNames,
+      };
+    });
+    return {
+      relationCollection: relationCollections,
+      relationNames: nameOfCollectionPerRelation,
+      attributesPerRelation: attributesPerRelation,
+    };
+  }
+}
diff --git a/libs/shared/lib/vis/paohvis/utils/SortUseCase.tsx b/libs/shared/lib/vis/paohvis/utils/SortUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..818384e1323c04f4cf5f88583294abfd35a35c4d
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/SortUseCase.tsx
@@ -0,0 +1,135 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { HyperEdgeI, HyperEdgeRange, NodeOrder, PaohvisNodeOrder, ValueType } from "../Types";
+import { Node } from '@graphpolaris/shared/lib/data-access';
+
+/**
+ * This class is responsible for sorting the data for the ToPaohvisDataParser.
+ */
+export default class SortUseCase {
+  /**
+   * Sorts the given HyperEdgeRanges by their labels and sorts the indices for all HyperEdges in the HyperEdgeRanges.
+   * @param hyperEdgeRanges that should be sorted.
+   * @param type of the HyperEdgeRange labels.
+   */
+  public static sortHyperEdgeRanges(hyperEdgeRanges: HyperEdgeRange[], type: ValueType): void {
+    this.sortHyperEdgeIndices(hyperEdgeRanges);
+    this.sortHyperEdgeRangesByLabels(hyperEdgeRanges, type);
+  }
+
+  /**
+   * Sorts the given HyperEdgeRanges by their labels.
+   * They are sorted alphabetically when the labels are strings.
+   * They are sorted from lowest to highest when the labels are numbers.
+   * @param hyperEdgeRanges that should be sorted.
+   * @param type of the HyperEdgeRange labels.
+   */
+  private static sortHyperEdgeRangesByLabels(
+    hyperEdgeRanges: HyperEdgeRange[],
+    type: ValueType,
+  ): void {
+    //sort all hyperedgeranges text
+    if (type == ValueType.number) {
+      //from lowest to highest
+      hyperEdgeRanges.sort((a, b) => compareNumericalLabels(a.rangeText, b.rangeText));
+    } else {
+      //alphabetical
+      hyperEdgeRanges.sort((a, b) => a.rangeText.localeCompare(b.rangeText));
+    }
+  }
+
+  /**
+   * Sorts all indices for each HyperEdge in the HyperEdgeRanges from lowest to highest.
+   * @param hyperEdgeRanges  that should be sorted.
+   */
+  private static sortHyperEdgeIndices(hyperEdgeRanges: HyperEdgeRange[]): void {
+    hyperEdgeRanges.forEach((hyperEdgeRange) =>
+      hyperEdgeRange.hyperEdges.forEach((hyperEdge) => hyperEdge.indices.sort((n1, n2) => n1 - n2)),
+    );
+  }
+
+  /**
+   * Sort the nodes in the given order.
+   * @param nodeOrder is the order in which the nodes should be sorted.
+   * @param nodes are the nodes that will be sorted
+   * @param hyperEdgeDegree is the dictionary where you can find how many edges connected from the node.
+   */
+  public static sortNodes(
+    nodeOrder: PaohvisNodeOrder,
+    nodes: Node[],
+    hyperEdgeDegree: Record<string, number>,
+  ): void {
+    switch (nodeOrder.orderBy) {
+      //sort nodes on their degree (# number of hyperedges) (entities with most hyperedges first)
+      case NodeOrder.degree:
+        //TODO: sort when nodes have the same amount of edges
+        nodes.sort((node1, node2) => {
+          return hyperEdgeDegree[node2.id] - hyperEdgeDegree[node1.id];
+        });
+        break;
+      //sort nodes on their id alphabetically
+      case NodeOrder.alphabetical:
+        nodes.sort((node1, node2) => node1.id.localeCompare(node2.id));
+        break;
+      default:
+        throw new Error('This node order does not exist');
+    }
+    //reverse order
+    if (nodeOrder.isReverseOrder) nodes.reverse();
+  }
+
+  /**
+   * This is used to sort hyperEdges by appearance on y-axis and length of hyperEdges.
+   * @param hyperEdgeRanges that should be sorted.
+   */
+  public static sortHyperEdges(hyperEdgeRanges: HyperEdgeRange[]): void {
+    hyperEdgeRanges.forEach((hyperEdgeRange) => {
+      hyperEdgeRange.hyperEdges.sort(this.compareByLineLength);
+      hyperEdgeRange.hyperEdges.sort(this.compareByAppearanceOnYAxis);
+    });
+  }
+
+  /**
+   * This is used to compare hyperedges by their linelength in the table.
+   * Can be used to sort hyperedges from hyperedges with a shorter lineLength to longer lineLength.
+   * @param hyperEdge1 first hyperedge to compare
+   * @param hyperEdge2 second hyperedge to compare
+   * @returns {number} a negative value if the first argument is less than the second argument, zero if they're equal, and a positive value otherwise.
+   */
+  private static compareByLineLength(hyperEdge1: HyperEdgeI, hyperEdge2: HyperEdgeI): number {
+    const indices1 = hyperEdge1.indices;
+    const lineLength1 = indices1[indices1.length - 1] - indices1[0];
+
+    const indices2 = hyperEdge2.indices;
+    const lineLength2 = indices2[indices2.length - 1] - indices2[0];
+    return lineLength1 - lineLength2;
+  }
+
+  /**
+   * This is used to compare hyperedges by their appearance on the y-axis in the table.
+   * Can be used to sort hyperedges so that the hyperedges that appear higher on the y-axis should appear earlier on the x-axis.
+   * @param hyperEdge1 first hyperedge to compare
+   * @param hyperEdge2 second hyperedge to compare
+   * @returns {number} a negative value if the first argument is less than the second argument, zero if they're equal, and a positive value otherwise.
+   */
+  private static compareByAppearanceOnYAxis(hyperEdge1: HyperEdgeI, hyperEdge2: HyperEdgeI): number {
+    const start1 = hyperEdge1.indices[0];
+    const start2 = hyperEdge2.indices[0];
+    return start1 - start2;
+  }
+}
+
+/**
+ * This is used to compare numerical labels.
+ * Can be used to sort numerical labels from the label with the lowest number to the label with the highest number.
+ * @param numericalLabel1 first numerical label to compare
+ * @param numericalLabel2 second numerical label to compare
+ * @returns {number} a negative value if the first argument is less than the second argument, zero if they're equal, and a positive value otherwise.
+ */
+function compareNumericalLabels(numericalLabel1: string, numericalLabel2: string): number {
+  return parseFloat(numericalLabel1) - parseFloat(numericalLabel2);
+}
diff --git a/libs/shared/lib/vis/paohvis/utils/ToPaohvisDataParserUsecase.tsx b/libs/shared/lib/vis/paohvis/utils/ToPaohvisDataParserUsecase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..454ffe6c02e66304a2eb317c8140bda71b40ccd1
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/ToPaohvisDataParserUsecase.tsx
@@ -0,0 +1,355 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { AxisType, getGroupName } from '../../shared/ResultNodeLinkParserUseCase'
+import { Edge, GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access';
+
+import { AttributeOrigin, HyperEdgeI, HyperEdgeRange, PaohvisAxisInfo, PaohvisData, PaohvisFilters, PaohvisNodeInfo, PaohvisNodeOrder, Relation, ValueType } from '../Types';
+import AttributeFilterUsecase, { filterUnusedEdges, getIds } from './AttributesFilterUseCase';
+import SortUseCase from './SortUseCase';
+import { getWidthOfText, uniq } from './utils';
+import style from '../Paohvis.module.scss';
+
+type Index = number;
+
+/**
+ * This parser is used to parse the incoming query result to the format that's needed to make the Paohvis table.
+ */
+export default class ToPaohvisDataParserUseCase {
+  private queryResult: GraphQueryResult;
+  private xAxisNodeGroup: string;
+  private yAxisNodeGroup: string;
+  private paohvisFilters: PaohvisFilters;
+
+  public constructor(queryResult: GraphQueryResult) {
+    this.queryResult = queryResult;
+    this.xAxisNodeGroup = '';
+    this.yAxisNodeGroup = '';
+    this.paohvisFilters = { nodeFilters: [], edgeFilters: [] };
+  }
+  /**
+   * Parses query results to the format that's needed to make a Paohvis table.
+   * @param axisInfo is the information that's needed to parse everything to the correct axis.
+   * @param nodeOrder is the order in which the nodes should be parsed.
+   * @returns the information that's needed to make a Paohvis table.
+   */
+  public parseQueryResult(axisInfo: PaohvisAxisInfo, nodeOrder: PaohvisNodeOrder): PaohvisData {
+    this.setAxesNodeGroups(axisInfo);
+
+    const filteredData = AttributeFilterUsecase.applyFilters(
+      this.queryResult,
+      this.paohvisFilters,
+    );
+
+    // filter unnecessary node groups and relations
+    const nodes = filteredData.nodes.filter((node) => {
+      const nodeType = getGroupName(node);
+      return nodeType === this.xAxisNodeGroup || nodeType === this.yAxisNodeGroup;
+    });
+    const edges = filteredData.edges.filter(
+      (edge) => getGroupName(edge) === axisInfo.relation.collection,
+    );
+
+    // get hyperEdgeDegree
+    const hyperEdgeDegree: Record<string, number> =
+      ToPaohvisDataParserUseCase.GetHyperEdgeDegreeDict(
+        nodes,
+        edges,
+        axisInfo.isYAxisEntityEqualToRelationFrom,
+      );
+
+    //parse nodes
+    const rowInfo: PaohvisNodeInfo = ToPaohvisDataParserUseCase.parseNodes(
+      nodes,
+      hyperEdgeDegree,
+      nodeOrder,
+      this.yAxisNodeGroup,
+      this.xAxisNodeGroup,
+    );
+    const maxLabelWidth = calcMaxRowLabelWidth(rowInfo.rowLabels);
+
+    //parse hyperEdges
+    const filteredEdges = filterUnusedEdges(getIds(nodes), edges);
+    const resultHyperEdgeRanges = ToPaohvisDataParserUseCase.parseHyperEdgeRanges(
+      nodes,
+      filteredEdges,
+      axisInfo,
+      rowInfo,
+    );
+    SortUseCase.sortHyperEdges(resultHyperEdgeRanges);
+
+    return {
+      rowLabels: rowInfo.rowLabels,
+      hyperEdgeRanges: resultHyperEdgeRanges,
+      maxRowLabelWidth: maxLabelWidth,
+    };
+  }
+
+  /**
+   * Sets the x-axis and y-axis node groups from the given PaohvisAxisInfo in the parser.
+   * @param axisInfo is the new PaohvisAxisInfo that will be used.
+   */
+  private setAxesNodeGroups(axisInfo: PaohvisAxisInfo): void {
+    const relation: Relation = axisInfo.relation;
+    if (axisInfo.isYAxisEntityEqualToRelationFrom) {
+      this.xAxisNodeGroup = relation.to;
+      this.yAxisNodeGroup = relation.from;
+    } else {
+      this.xAxisNodeGroup = relation.from;
+      this.yAxisNodeGroup = relation.to;
+    }
+  }
+
+  /**
+   * This parses the nodes to get the information that's needed to parse the hyperedges.
+   * @param nodes are the nodes from the query result.
+   * @param hyperEdgeDegree is the dictionary where you can find how many edges connected from the node.
+   * @param nodeOrder is the order in which the nodes should be sorted.
+   * @param yAxisNodeType is the type of nodes that should be on the y-axis.
+   * @param xAxisNodeType is the type of nodes that should be on the x-axis.
+   * @returns the information that's needed to parse the hyperedges.
+   */
+  private static parseNodes(
+    nodes: Node[],
+    hyperEdgeDegree: Record<string, number>,
+    nodeOrder: PaohvisNodeOrder,
+    yAxisNodeType: string,
+    xAxisNodeType: string,
+  ): PaohvisNodeInfo {
+    const rowNodes = filterRowNodes(nodes, hyperEdgeDegree, yAxisNodeType);
+    SortUseCase.sortNodes(nodeOrder, rowNodes, hyperEdgeDegree);
+    const rowLabels = getIds(rowNodes);
+
+    //make dictionary for finding the index of a row
+    const yNodesIndexDict: Record<string, number> = {};
+    let yNodeIndexCounter = 0;
+    for (let i = 0; i < rowNodes.length; i++) {
+      yNodesIndexDict[rowNodes[i].id] = yNodeIndexCounter;
+      yNodeIndexCounter++;
+    }
+
+    const xNodesAttributesDict: Record<string, any> = getXNodesAttributesDict(
+      yAxisNodeType,
+      xAxisNodeType,
+      nodes,
+    );
+    return {
+      rowLabels: rowLabels,
+      xNodesAttributesDict: xNodesAttributesDict,
+      yNodesIndexDict: yNodesIndexDict,
+    };
+  }
+
+  /**
+   * Makes a dictionary where you can find how many edges are connected to the nodes.
+   * @param nodes are the nodes where the edges should be counted for.
+   * @param edges should be used to count how many edges are connected to the nodes.
+   * @param isEntityRelationFrom is to decide if you need to count the from's or the to's of the edge.
+   * @returns a dictionary where you can find how many edges are connected to the nodes.
+   */
+  private static GetHyperEdgeDegreeDict(
+    nodes: Node[],
+    edges: Edge[],
+    isEntityRelationFrom: boolean,
+  ): Record<string, number> {
+    const hyperEdgeDegreeDict: Record<string, number> = {};
+
+    //initialize dictionary
+    nodes.forEach((node) => (hyperEdgeDegreeDict[node.id] = 0));
+
+    //count node appearance frequencies
+    edges.forEach((edge) => {
+      if (isEntityRelationFrom) hyperEdgeDegreeDict[edge.from]++;
+      else hyperEdgeDegreeDict[edge.to]++;
+    });
+
+    return hyperEdgeDegreeDict;
+  }
+
+  /**
+   * Parses the edges to make hyperedge ranges for the Paohvis table.
+   * @param nodes the unused nodes should already be filtered out.
+   * @param edges the unused edges should already be filtered out.
+   * @param axisInfo is the information that's needed to parse the edges to their respective hyperedge range.
+   * @param rowInfo is the information about the nodes that's needed to parse the edges to their respective hyperedge range.
+   * @returns the hyperedge ranges that will be used by the Paohvis table.
+   */
+  private static parseHyperEdgeRanges(
+    nodes: Node[],
+    edges: Edge[],
+    axisInfo: PaohvisAxisInfo,
+    rowInfo: PaohvisNodeInfo,
+  ): HyperEdgeRange[] {
+    if (nodes.length == 0 || edges.length == 0) return [];
+
+    const resultHyperEdgeRanges: HyperEdgeRange[] = [];
+
+    //is used to find on which index of HyperEdgeRanges the edge should be added
+    const attributeRangesIndexDict: Record<string, Index> = {};
+    let attributeRangeCounter = 0;
+
+    //is used to find the index to which hyperedge it belongs to (in the array hyperEdgeRanges[i].hyperEdges)
+    //dict[hyperEdgeRangeLabel] gives a dict where you can find with dict[edge.to] the index where it belongs to
+    const hyperEdgesDict: Record<string, Record<string, Index>> = {};
+
+    const xAxisAttributeType: string = axisInfo.selectedAttribute.name;
+
+    //is used to find the node attributes for the x-axis
+    const xNodesAttributesDict: Record<string, any> = rowInfo.xNodesAttributesDict;
+
+    //is used to find the index of nodes in rowLabels
+    const yNodesIndexDict: Record<string, number> = rowInfo.yNodesIndexDict;
+
+    let edgeDirection: string;
+    let edgeDirectionOpposite: string;
+    const isFromOnYAxis = axisInfo.isYAxisEntityEqualToRelationFrom;
+    const collection = axisInfo.relation.collection;
+
+    for (let i = 0; i < edges.length; i++) {
+      const edge = edges[i];
+      ({ edgeDirection, edgeDirectionOpposite } = getEdgeDirections(isFromOnYAxis, edge));
+
+      let edgeIndexInResult: number = yNodesIndexDict[edgeDirection];
+
+      // check if the chosen attribute is an attribute of the edge or the node
+      let attribute: any;
+      if (axisInfo.selectedAttribute.origin == AttributeOrigin.relation)
+        attribute = edge.attributes[xAxisAttributeType];
+      else attribute = xNodesAttributesDict[edgeDirectionOpposite][xAxisAttributeType];
+
+      // if no edge attribute was selected, then all edges will be placed in one big hyperEdgeRange
+      if (attribute == undefined) attribute = ValueType.noAttribute;
+
+      if (attribute in attributeRangesIndexDict) {
+        const rangeIndex = attributeRangesIndexDict[attribute];
+        const targetHyperEdges = resultHyperEdgeRanges[rangeIndex].hyperEdges;
+        const targetHyperEdgeIndex = hyperEdgesDict[attribute][edgeDirectionOpposite];
+
+        if (targetHyperEdgeIndex != undefined) {
+          // hyperedge group already exists so add edge to group
+          targetHyperEdges[targetHyperEdgeIndex].indices.push(edgeIndexInResult);
+        } else {
+          // create new hyperedge group
+          hyperEdgesDict[attribute][edgeDirectionOpposite] = targetHyperEdges.length;
+
+          // 'add edge to new hyperedge group'
+          targetHyperEdges.push({
+            indices: [edgeIndexInResult],
+            frequencies: newFrequencies(rowInfo.rowLabels),
+            nameToShow: edgeDirectionOpposite,
+          });
+        }
+      } else {
+        // create new attribute range
+        attributeRangesIndexDict[attribute] = attributeRangeCounter;
+        attributeRangeCounter++;
+
+        hyperEdgesDict[attribute] = {};
+        hyperEdgesDict[attribute][edgeDirectionOpposite] = 0;
+
+        let label: string;
+        if (xAxisAttributeType != ValueType.noAttribute && xAxisAttributeType != '')
+          label = attribute.toString();
+        else label = 'No attribute was selected';
+        const hyperEdge: HyperEdgeI = {
+          indices: [edgeIndexInResult],
+          frequencies: newFrequencies(rowInfo.rowLabels),
+          nameToShow: edgeDirectionOpposite,
+        };
+        const hyperEdgeRange: HyperEdgeRange = {
+          rangeText: label,
+          hyperEdges: [hyperEdge],
+        };
+        resultHyperEdgeRanges.push(hyperEdgeRange);
+      }
+    }
+
+    SortUseCase.sortHyperEdgeRanges(resultHyperEdgeRanges, axisInfo.selectedAttribute.type);
+
+    //calc how many duplicate edges are in each hyperedge
+    resultHyperEdgeRanges.forEach((hyperEdgeRange) => {
+      hyperEdgeRange.hyperEdges.forEach((hyperedge) => {
+        hyperedge.indices.forEach((index) => {
+          hyperedge.frequencies[index] += 1;
+        });
+      });
+    });
+
+    //filter out duplicate indices from all hyperedges
+    resultHyperEdgeRanges.forEach((hyperEdgeRange) => {
+      hyperEdgeRange.hyperEdges.forEach((hyperedge) => {
+        hyperedge.indices = uniq(hyperedge.indices);
+      });
+    });
+
+    return resultHyperEdgeRanges;
+  }
+
+  /** Sets new PaohvisFilters. */
+  public setPaohvisFilters(filters: PaohvisFilters): void {
+    this.paohvisFilters = filters;
+  }
+}
+
+/** Calculates the width that's needed for the rowlabels. */
+function calcMaxRowLabelWidth(rowLabels: string[]) {
+  const margin = 10;
+  let maxLabelWidth = 0;
+  rowLabels.forEach((rowLabel) => {
+    const textWidth: number = getWidthOfText(
+      rowLabel + '   ',
+      style.tableFontFamily,
+      style.tableFontSize,
+      style.tableFontWeight,
+    );
+    if (textWidth > maxLabelWidth) maxLabelWidth = textWidth;
+  });
+  return maxLabelWidth + margin;
+}
+
+/** Gets a dictionary where you can find the attributes that belong to the nodes on teh x-axis.  */
+function getXNodesAttributesDict(yAxisNodeType: string, xAxisNodeType: string, nodes: Node[]) {
+  const resultXNodesAttributesDict: Record<string, any> = {};
+  if (yAxisNodeType == xAxisNodeType)
+    nodes.forEach((node) => (resultXNodesAttributesDict[node!.id] = node.attributes));
+  else
+    nodes.forEach((node) => {
+      if (getGroupName(node) == xAxisNodeType)
+        resultXNodesAttributesDict[node.id] = node.attributes;
+    });
+  return resultXNodesAttributesDict;
+}
+
+/**
+ * Gets which direction the edge will be read from. The direction can be read from:
+ * 'from => to' or 'to => from'.
+ */
+function getEdgeDirections(isFromToRelation: boolean, edge: Edge) {
+  let edgeDirection: string;
+  let edgeDirectionOpposite: string;
+  if (isFromToRelation) {
+    edgeDirection = edge.from;
+    edgeDirectionOpposite = edge.to;
+  } else {
+    edgeDirection = edge.to;
+    edgeDirectionOpposite = edge.from;
+  }
+  return { edgeDirection, edgeDirectionOpposite };
+}
+
+/** This makes an array filled with zeroes, which is used to count the frequencies of edges. */
+function newFrequencies(rowLabels: string[]): number[] {
+  return Array(rowLabels.length).fill(0);
+}
+
+/** Filters out nodes that have no edges and nodes that are not the specified type. */
+function filterRowNodes(
+  nodes: Node[],
+  hyperEdgeDegree: Record<string, number>,
+  rowNodeType: string,
+): Node[] {
+  return nodes.filter((node) => hyperEdgeDegree[node.id] > 0 && getGroupName(node) == rowNodeType);
+}
diff --git a/libs/shared/lib/vis/paohvis/utils/utils.tsx b/libs/shared/lib/vis/paohvis/utils/utils.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4476bb68fb12c1bfa5e97caa5f57cbb47f540950
--- /dev/null
+++ b/libs/shared/lib/vis/paohvis/utils/utils.tsx
@@ -0,0 +1,158 @@
+import { log } from "console";
+import { SchemaAttribute, SchemaGraph, SchemaGraphologyEdge, SchemaGraphologyNode } from "../../../schema";
+import { AttributeNames, EntitiesFromSchema, RelationsFromSchema } from "../Types";
+
+/**
+   * Takes a schema result and calculates all the entity names, and relation names and attribute names per entity.
+   * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity.
+   */
+export function calculateAttributesAndRelations(schemaResult: SchemaGraph): EntitiesFromSchema {
+    const attributesPerEntity: Record<string, AttributeNames> = calculateAttributes(
+        schemaResult,
+    );
+    const relationsPerEntity: Record<string, string[]> = calculateRelations(schemaResult);
+
+    return {
+        entityNames: schemaResult.nodes.map((node) => node?.attributes?.name || 'ERROR'),
+        attributesPerEntity,
+        relationsPerEntity,
+    };
+}
+
+
+/**
+ * Takes a schema result and calculates all the attribute names per entity.
+ * @param {SchemaResultType} schemaResult A new schema result from the backend.
+ * @returns {Record<string, AttributeNames>} All attribute names per entity.
+ */
+export function calculateAttributes(schemaResult: SchemaGraph): Record<string, AttributeNames> {
+    const attributesPerEntity: Record<string, AttributeNames> = {};
+    // Go through each entity.
+    schemaResult.nodes.forEach((node) => {
+        if (node?.attributes?.name === undefined) {
+            console.error('ERROR: Node has no name attribute or name.', node);
+            return;
+        }
+
+        // Extract the attribute names per datatype for each entity.
+        const textAttributeNames: string[] = [];
+        const boolAttributeNames: string[] = [];
+        const numberAttributeNames: string[] = [];
+
+        node.attributes.attributes.forEach((attr) => {
+            if (attr.type == 'string') textAttributeNames.push(attr.name);
+            else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name);
+            else boolAttributeNames.push(attr.name);
+        });
+
+        // Create a new object with the arrays with attribute names per datatype.
+        attributesPerEntity[node.attributes.name] = {
+            textAttributeNames,
+            boolAttributeNames,
+            numberAttributeNames,
+        };
+    });
+    return attributesPerEntity;
+}
+
+/**
+* Takes a schema result and calculates all the relation names per entity.
+* @param {SchemaResultType} schemaResult A new schema result from the backend.
+* @returns {Record<string, AttributeNames>} All relation (from and to) names per entity.
+*/
+export function calculateRelations(schemaResult: SchemaGraph): Record<string, string[]> {
+    const relationsPerEntity: Record<string, string[]> = {};
+    // Go through each relation.
+    schemaResult.edges.forEach((edge) => {
+        if (edge?.attributes === undefined) {
+            console.error('ERROR: Edge has no attribute.', edge);
+            return;
+        }
+
+        // Extract the from-node-name (collection name) from every relation.
+        if (relationsPerEntity[edge.attributes.from]) relationsPerEntity[edge.attributes.from].push(edge.attributes.collection);
+        else relationsPerEntity[edge.attributes.from] = [edge.attributes.collection];
+        // Extract the to-node-name (collection name) from every relation.
+        if (relationsPerEntity[edge.attributes.to]) relationsPerEntity[edge.attributes.to].push(edge.attributes.collection);
+        else relationsPerEntity[edge.attributes.to] = [edge.attributes.collection];
+    });
+    return relationsPerEntity;
+}
+
+/**
+ * Takes a schema result and calculates all the relation collection names, and relation names and attribute names per relation.
+ * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering.
+ * @param {SchemaResultType} schemaResult A new schema result from the backend.
+ * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity.
+ */
+export function calculateAttributesFromRelation(schemaResult: SchemaGraph): RelationsFromSchema {
+    const relationCollections: string[] = [];
+    const attributesPerRelation: Record<string, AttributeNames> = {};
+    const nameOfCollectionPerRelation: Record<string, string> = {};
+    // Go through each relation.
+    schemaResult.edges.forEach((edge) => {
+        if (edge?.attributes === undefined) {
+            console.error('ERROR: Edge has no attribute.', edge);
+            return;
+        }
+
+        if (!nameOfCollectionPerRelation[edge.attributes.collection]) {
+            relationCollections.push(edge.attributes.collection);
+            nameOfCollectionPerRelation[edge.attributes.collection] = `${edge.attributes.from}:${edge.attributes.to}:${edge.attributes.name}`;
+        }
+        // Extract the attribute names per datatype for each relation.
+        const textAttributeNames: string[] = [];
+        const boolAttributeNames: string[] = [];
+        const numberAttributeNames: string[] = [];
+        edge.attributes.attributes.forEach((attr: SchemaAttribute) => {
+            if (attr.type == 'string') textAttributeNames.push(attr.name);
+            else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name);
+            else boolAttributeNames.push(attr.name);
+        });
+
+        // Create a new object with the arrays with attribute names per datatype.
+        attributesPerRelation[edge.attributes.collection] = {
+            textAttributeNames,
+            boolAttributeNames,
+            numberAttributeNames,
+        };
+    });
+    return {
+        relationCollection: relationCollections,
+        relationNames: nameOfCollectionPerRelation,
+        attributesPerRelation: attributesPerRelation,
+    };
+}
+
+/**
+ * Filters duplicate elements from array with a hashtable.
+ * From https://stackoverflow.com/questions/9229645/remove-duplicate-values-from-js-array */
+export function uniq(element: number[]) {
+    const seen: Record<number, boolean> = {};
+    return element.filter(function (item) {
+        return seen.hasOwnProperty(item) ? false : (seen[item] = true);
+    });
+}
+
+/**
+* Calculate the width of the specified text.
+* @param txt Text input as string.
+* @param fontname Name of the font.
+* @param fontsize Size of the fond in px.
+* @param fontWeight The weight of the font.
+* @returns {number} Width of the textfield in px.
+*/
+export const getWidthOfText = (
+    txt: string,
+    fontname: string,
+    fontsize: string,
+    fontWeight = 'normal',
+): number => {
+    let c = document.createElement('canvas');
+    let ctx = c.getContext('2d') as CanvasRenderingContext2D;
+    let fontspec = fontWeight + ' ' + fontsize + ' ' + fontname;
+    if (ctx.font !== fontspec) ctx.font = fontspec;
+    return ctx.measureText(txt).width;
+};
\ No newline at end of file
diff --git a/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.t b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.t
new file mode 100644
index 0000000000000000000000000000000000000000..2b1b6b26775e238d0f5e8e1f64089e00e83f6ec4
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.t
@@ -0,0 +1,1156 @@
+import Broker from '../../../../domain/entity/broker/broker';
+import SemanticSubstratesViewModelImpl from './SemanticSubstratesViewModelImpl';
+import mockNodeLinkResult from '../../../../data/mock-data/query-result/big2ndChamberQueryResult';
+import mockSchema from '../../../../data/mock-data/schema-result/2ndChamberSchemaMock';
+import { MinMaxType } from '../../../../domain/entity/semantic-substrates/structures/Types';
+
+jest.mock('../../../view/result-visualisations/semantic-substrates/SemanticSubstratesStylesheet');
+
+describe('SemanticSubstratesViewModelImpl: Broker subscriptions', () => {
+  it('should consume schema results when subscribed', () => {
+    const viewModel = new SemanticSubstratesViewModelImpl();
+
+    const mockConsumeMessages = jest.fn();
+    viewModel.consumeMessageFromBackend = mockConsumeMessages;
+
+    viewModel.subscribeToSchemaResult();
+    Broker.instance().publish('test schema result', 'schema_result');
+    expect(mockConsumeMessages.mock.calls[0][0]).toEqual('test schema result');
+
+    viewModel.unSubscribeFromSchemaResult();
+    Broker.instance().publish('test schema result', 'schema_result');
+    expect(mockConsumeMessages).toBeCalledTimes(1);
+  });
+});
+
+describe('SemanticSubstratesViewModelImpl:OnPlotTitleChanged', () => {
+  it('should change the plot title', () => {
+    // Initialize a ViewModel with some plots.
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    const i = 1;
+    const oldPlotSpec = viewModelImpl.plotSpecifications[i];
+    viewModelImpl.onPlotTitleChanged(i, 'commissies', 'naam', 'Defensie');
+
+    expect(viewModelImpl.plotSpecifications[i]).toEqual({
+      ...oldPlotSpec,
+      entity: 'commissies',
+      labelAttributeType: 'naam',
+      labelAttributeValue: 'Defensie',
+    });
+
+    // And check if the NotifyViewAboutChanges is called.
+    expect(onViewModelChangedMock).toBeCalledTimes(1);
+
+    viewModelImpl.onPlotTitleChanged(i, 'commissies', 'id', 'Defensie');
+    expect(viewModelImpl.plotSpecifications[i]).toEqual({
+      ...oldPlotSpec,
+      entity: 'commissies',
+      labelAttributeType: 'id',
+      labelAttributeValue: 'Defensie',
+    });
+  });
+});
+
+describe('SemanticSubstratesViewModelImpl', () => {
+  it('should apply default specs on consuming a new node link result', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+
+    // Consume a nodelink query result.
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    let mockCalc = viewModelImpl.scaleCalculation;
+    let mockCalcColour = viewModelImpl.colourCalculation;
+
+    const expectedPlots = [
+      {
+        title: 'partij:VVD',
+        nodes: [
+          {
+            id: 'kamerleden/148',
+            data: { text: 'kamerleden/148' },
+            originalPosition: { x: 1, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/9ab41898-332f-4c08-a834-d58b4669c990.jpg?itok=pbUPRriP',
+              leeftijd: 43,
+              naam: 'Dilan Yeilgz-Zegerius',
+              partij: 'VVD',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 114.28571428571432, y: 100 },
+          },
+          {
+            id: 'kamerleden/132',
+            data: { text: 'kamerleden/132' },
+            originalPosition: { x: 2, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/b625d2bf-da74-40a2-b013-2069a09dcb6c.jpg?itok=zMgno8bi',
+              leeftijd: 36,
+              naam: 'Peter Valstar',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenzande",
+            },
+            scaledPosition: { x: 133.99014778325127, y: 100 },
+          },
+          {
+            id: 'kamerleden/131',
+            data: { text: 'kamerleden/131' },
+            originalPosition: { x: 3, y: 0 },
+            attributes: {
+              anc: 1304,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/97ffe642-648f-4dab-a0e8-0720e96e3e35.jpg?itok=eDllv9dS',
+              leeftijd: 49,
+              naam: 'Judith Tielen',
+              partij: 'VVD',
+              woonplaats: 'Utrecht',
+            },
+            scaledPosition: { x: 153.6945812807882, y: 100 },
+          },
+          {
+            id: 'kamerleden/128',
+            data: { text: 'kamerleden/128' },
+            originalPosition: { x: 4, y: 0 },
+            attributes: {
+              anc: 3171,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/2538ca6a-8254-4c7e-af71-a20db82afd30.jpg?itok=GvwO8NYO',
+              leeftijd: 46,
+              naam: 'Ockje Tellegen',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 173.39901477832515, y: 100 },
+          },
+          {
+            id: 'kamerleden/117',
+            data: { text: 'kamerleden/117' },
+            originalPosition: { x: 5, y: 0 },
+            attributes: {
+              anc: 2004,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/8b3664bd-77e4-468b-af96-f3f4ec27fcce.jpg?itok=ofrc9cnP',
+              leeftijd: 54,
+              naam: 'Mark Rutte',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 193.1034482758621, y: 100 },
+          },
+          {
+            id: 'kamerleden/106',
+            data: { text: 'kamerleden/106' },
+            originalPosition: { x: 6, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/0e34f732-337a-4339-961f-8c18d68e714d.jpg?itok=FTBMCT9a',
+              leeftijd: 54,
+              naam: 'Marille Paul',
+              partij: 'VVD',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 212.80788177339906, y: 100 },
+          },
+          {
+            id: 'kamerleden/100',
+            data: { text: 'kamerleden/100' },
+            originalPosition: { x: 7, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/2b223ce1-e251-462b-a5fc-971c92bbf37c.jpg?itok=MD8qI-l1',
+              leeftijd: 43,
+              naam: 'Daan de Neef',
+              partij: 'VVD',
+              woonplaats: 'Breda',
+            },
+            scaledPosition: { x: 232.512315270936, y: 100 },
+          },
+          {
+            id: 'kamerleden/97',
+            data: { text: 'kamerleden/97' },
+            originalPosition: { x: 8, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/cd02edbc-106e-46d1-8d70-6594741356ea.jpg?itok=Q9q13ntf',
+              leeftijd: 33,
+              naam: 'Fahid Minhas',
+              partij: 'VVD',
+              woonplaats: 'Schiedam',
+            },
+            scaledPosition: { x: 252.21674876847294, y: 100 },
+          },
+          {
+            id: 'kamerleden/81',
+            data: { text: 'kamerleden/81' },
+            originalPosition: { x: 9, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/8767e9de-869e-4e8a-940f-1a63d15eab3c.jpg?itok=tdoE8yQX',
+              leeftijd: 28,
+              naam: 'Daan de Kort',
+              partij: 'VVD',
+              woonplaats: 'Veldhoven',
+            },
+            scaledPosition: { x: 271.9211822660099, y: 100 },
+          },
+          {
+            id: 'kamerleden/79',
+            data: { text: 'kamerleden/79' },
+            originalPosition: { x: 10, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/88e0734b-8c03-4daa-8ec5-055e36c07030.jpg?itok=_ZteUeWk',
+              leeftijd: 39,
+              naam: 'Daniel Koerhuis',
+              partij: 'VVD',
+              woonplaats: 'Raalte',
+            },
+            scaledPosition: { x: 291.6256157635468, y: 100 },
+          },
+          {
+            id: 'kamerleden/145',
+            data: { text: 'kamerleden/145' },
+            originalPosition: { x: 11, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/8ee20f38-f3f0-4ff9-b5d9-84557b5ddcb1.jpg?itok=ExYCR1Ti',
+              leeftijd: 51,
+              naam: 'Hatte van der Woude',
+              partij: 'VVD',
+              woonplaats: 'Delft',
+            },
+            scaledPosition: { x: 311.3300492610838, y: 100 },
+          },
+          {
+            id: 'kamerleden/70',
+            data: { text: 'kamerleden/70' },
+            originalPosition: { x: 12, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/f23be4bd-7b2b-45dd-be84-4f76f11b624a.jpg?itok=DsfJ-8o0',
+              leeftijd: 43,
+              naam: 'Roelien Kamminga',
+              partij: 'VVD',
+              woonplaats: 'Groningen',
+            },
+            scaledPosition: { x: 331.03448275862075, y: 100 },
+          },
+          {
+            id: 'kamerleden/127',
+            data: { text: 'kamerleden/127' },
+            originalPosition: { x: 13, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/ce400601-651e-4ac9-a43e-ed8d4817b7fd.jpg?itok=wsISYKCx',
+              leeftijd: 44,
+              naam: 'Pim van Strien',
+              partij: 'VVD',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 350.7389162561577, y: 100 },
+          },
+          {
+            id: 'kamerleden/115',
+            data: { text: 'kamerleden/115' },
+            originalPosition: { x: 14, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/4bf69aa9-29a7-44db-b7ee-8bfa40e343dd.jpg?itok=-BKeAg8F',
+              leeftijd: 32,
+              naam: 'Queeny Rajkowski',
+              partij: 'VVD',
+              woonplaats: 'Utrecht',
+            },
+            scaledPosition: { x: 370.44334975369463, y: 100 },
+          },
+          {
+            id: 'kamerleden/64',
+            data: { text: 'kamerleden/64' },
+            originalPosition: { x: 15, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/7954eb2c-1bf5-42af-80dc-85f29b367db0.jpg?itok=9ucjAERV',
+              leeftijd: 49,
+              naam: 'Folkert Idsinga',
+              partij: 'VVD',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 390.1477832512316, y: 100 },
+          },
+          {
+            id: 'kamerleden/60',
+            data: { text: 'kamerleden/60' },
+            originalPosition: { x: 16, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/8509e44d-0f9b-413a-a2cd-f64d656c3955.jpg?itok=Iy2_G4Jx',
+              leeftijd: 53,
+              naam: 'Jacqueline van den Hil',
+              partij: 'VVD',
+              woonplaats: 'Goes',
+            },
+            scaledPosition: { x: 409.8522167487685, y: 100 },
+          },
+          {
+            id: 'kamerleden/143',
+            data: { text: 'kamerleden/143' },
+            originalPosition: { x: 17, y: 0 },
+            attributes: {
+              anc: 1823,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/7d18b6e3-de0c-4aea-bef8-242bbc38e936.jpg?itok=zebZoGjq',
+              leeftijd: 43,
+              naam: 'Jeroen van Wijngaarden',
+              partij: 'VVD',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 429.55665024630554, y: 100 },
+          },
+          {
+            id: 'kamerleden/56',
+            data: { text: 'kamerleden/56' },
+            originalPosition: { x: 18, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/a3b17348-54cf-4bca-82d4-e62bd46c2fbc.jpg?itok=MF1OOpeK',
+              leeftijd: 40,
+              naam: 'Eelco Heinen',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 449.26108374384245, y: 100 },
+          },
+          {
+            id: 'kamerleden/142',
+            data: { text: 'kamerleden/142' },
+            originalPosition: { x: 19, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/c3b1dca8-2cb4-4025-9527-36163a098c1f.jpg?itok=3BnXwQgy',
+              leeftijd: 35,
+              naam: 'Dennis Wiersma',
+              partij: 'VVD',
+              woonplaats: 'De Bilt',
+            },
+            scaledPosition: { x: 468.96551724137936, y: 100 },
+          },
+          {
+            id: 'kamerleden/46',
+            data: { text: 'kamerleden/46' },
+            originalPosition: { x: 20, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/a0b48551-42b7-4c55-9556-6b997d978ff2.jpg?itok=-A3mAzHe',
+              leeftijd: 41,
+              naam: 'Peter de Groot',
+              partij: 'VVD',
+              woonplaats: 'Harderwijk',
+            },
+            scaledPosition: { x: 488.6699507389164, y: 100 },
+          },
+          {
+            id: 'kamerleden/38',
+            data: { text: 'kamerleden/38' },
+            originalPosition: { x: 21, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/9c8bc5bc-8d13-4c11-be37-57a43a1d734b.jpg?itok=TqEaAJCz',
+              leeftijd: 30,
+              naam: 'Silvio Erkens',
+              partij: 'VVD',
+              woonplaats: 'Kerkrade',
+            },
+            scaledPosition: { x: 508.3743842364533, y: 100 },
+          },
+          {
+            id: 'kamerleden/35',
+            data: { text: 'kamerleden/35' },
+            originalPosition: { x: 22, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/ea21d788-7cfe-41ff-b22f-0664397139af.jpg?itok=4G8NOaak',
+              leeftijd: 32,
+              naam: 'Ulysse Ellian',
+              partij: 'VVD',
+              woonplaats: 'Almere',
+            },
+            scaledPosition: { x: 528.0788177339903, y: 100 },
+          },
+          {
+            id: 'kamerleden/33',
+            data: { text: 'kamerleden/33' },
+            originalPosition: { x: 23, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/c4f7c468-721e-4953-97f2-bff05fe570c4.jpg?itok=MmXfOgJx',
+              leeftijd: 41,
+              naam: 'Zohair El Yassini',
+              partij: 'VVD',
+              woonplaats: 'Utrecht',
+            },
+            scaledPosition: { x: 547.7832512315272, y: 100 },
+          },
+          {
+            id: 'kamerleden/25',
+            data: { text: 'kamerleden/25' },
+            originalPosition: { x: 24, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/bf494e35-c009-4c74-a5c6-3cb2cd26bb51.jpg?itok=2CTfrPhP',
+              leeftijd: 31,
+              naam: 'Thom van Campen',
+              partij: 'VVD',
+              woonplaats: 'Zwolle',
+            },
+            scaledPosition: { x: 567.4876847290642, y: 100 },
+          },
+          {
+            id: 'kamerleden/54',
+            data: { text: 'kamerleden/54' },
+            originalPosition: { x: 25, y: 0 },
+            attributes: {
+              anc: 2601,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/522d59a0-10ba-488f-a27a-a1dda7581900.jpg?itok=5b_3G5nE',
+              leeftijd: 43,
+              naam: 'Rudmer Heerema',
+              partij: 'VVD',
+              woonplaats: 'Alkmaar',
+            },
+            scaledPosition: { x: 587.1921182266011, y: 100 },
+          },
+          {
+            id: 'kamerleden/96',
+            data: { text: 'kamerleden/96' },
+            originalPosition: { x: 26, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/973da897-66c7-4010-a98d-505e0e97a60c.jpg?itok=HLNJtdsA',
+              leeftijd: 44,
+              naam: 'Ingrid Michon-Derkzen',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 606.896551724138, y: 100 },
+          },
+          {
+            id: 'kamerleden/9',
+            data: { text: 'kamerleden/9' },
+            originalPosition: { x: 27, y: 0 },
+            attributes: {
+              anc: 1414,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/a962ffcb-a300-45e1-8850-70c7173c233e.jpg?itok=0f-c2lGz',
+              leeftijd: 35,
+              naam: 'Bente Becker',
+              partij: 'VVD',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 626.600985221675, y: 100 },
+          },
+          {
+            id: 'kamerleden/23',
+            data: { text: 'kamerleden/23' },
+            originalPosition: { x: 28, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/e5c4753f-d2d8-4a4a-ae9d-62118892a731.jpg?itok=iGnPsRMX',
+              leeftijd: 34,
+              naam: 'Ruben Brekelmans',
+              partij: 'VVD',
+              woonplaats: 'Oisterwijk',
+            },
+            scaledPosition: { x: 646.3054187192118, y: 100 },
+          },
+          {
+            id: 'kamerleden/135',
+            data: { text: 'kamerleden/135' },
+            originalPosition: { x: 29, y: 0 },
+            attributes: {
+              anc: 3122,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/b8d002bc-ea6c-4f57-b36e-38415b205e09.jpg?itok=tHQU-Qrn',
+              leeftijd: 56,
+              naam: 'Aukje de Vries',
+              partij: 'VVD',
+              woonplaats: 'Leeuwarden',
+            },
+            scaledPosition: { x: 666.0098522167489, y: 100 },
+          },
+          {
+            id: 'kamerleden/0',
+            data: { text: 'kamerleden/0' },
+            originalPosition: { x: 30, y: 0 },
+            attributes: {
+              anc: 987,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/397c857a-fda0-414d-8fdc-8288cd3284aa.jpg?itok=55l5zRvr',
+              leeftijd: 31,
+              naam: 'Thierry Aartsen',
+              partij: 'VVD',
+              woonplaats: 'Breda',
+            },
+            scaledPosition: { x: 685.7142857142858, y: 100 },
+          },
+        ],
+        selectedAttributeCatecorigal: 'state',
+        selectedAttributeNumerical: 'long',
+        scaleCalculation: mockCalc,
+        colourCalculation: mockCalcColour,
+        minmaxXAxis: { min: -4.800000000000001, max: 35.8 },
+        minmaxYAxis: { min: -1, max: 1 },
+        width: 800,
+        height: 200,
+        yOffset: 0,
+        possibleTitleAttributeValues: [
+          'VVD',
+          'D66',
+          'GL',
+          'PVV',
+          'PvdD',
+          'PvdA',
+          'SGP',
+          'BIJ1',
+          'CU',
+          'BBB',
+          'CDA',
+          'SP',
+          'FVD',
+          'DENK',
+          'Fractie Den Haan',
+          'Volt',
+          'JA21',
+        ],
+      },
+      {
+        title: 'partij:D66',
+        nodes: [
+          {
+            id: 'kamerleden/141',
+            data: { text: 'kamerleden/141' },
+            originalPosition: { x: 1, y: 0 },
+            attributes: {
+              anc: 3171,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/d1bb566e-a111-43b7-8856-273ebac1f753.jpg?itok=-k-RCynx',
+              leeftijd: 48,
+              naam: 'Steven van Weyenberg',
+              partij: 'D66',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 114.2857142857143, y: 100 },
+          },
+          {
+            id: 'kamerleden/138',
+            data: { text: 'kamerleden/138' },
+            originalPosition: { x: 2, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/6a54c095-9872-4322-81be-114b74233d40.jpg?itok=uf9jc1Gk',
+              leeftijd: 36,
+              naam: 'Hanneke van der Werf',
+              partij: 'D66',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 140.25974025974028, y: 100 },
+          },
+          {
+            id: 'kamerleden/123',
+            data: { text: 'kamerleden/123' },
+            originalPosition: { x: 3, y: 0 },
+            attributes: {
+              anc: 1304,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/da9f33b0-80ca-49eb-a1ec-def3b46538d9.jpg?itok=nL6HIBWH',
+              leeftijd: 38,
+              naam: 'Joost Sneller',
+              partij: 'D66',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 166.23376623376626, y: 100 },
+          },
+          {
+            id: 'kamerleden/114',
+            data: { text: 'kamerleden/114' },
+            originalPosition: { x: 4, y: 0 },
+            attributes: {
+              anc: 1414,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/6fc5878e-d76a-4e9d-a4e3-e5ef581bb2ff.jpg?itok=qf_oudwu',
+              leeftijd: 30,
+              naam: 'Rens Raemakers',
+              partij: 'D66',
+              woonplaats: 'Neer',
+            },
+            scaledPosition: { x: 192.20779220779224, y: 100 },
+          },
+          {
+            id: 'kamerleden/147',
+            data: { text: 'kamerleden/147' },
+            originalPosition: { x: 5, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/45ef5aae-a6d0-4bad-861e-dd5b883a9665.jpg?itok=h5aEZJVM',
+              leeftijd: 56,
+              naam: 'Jorien Wuite',
+              partij: 'D66',
+              woonplaats: 'Voorburg',
+            },
+            scaledPosition: { x: 218.18181818181822, y: 100 },
+          },
+          {
+            id: 'kamerleden/107',
+            data: { text: 'kamerleden/107' },
+            originalPosition: { x: 6, y: 0 },
+            attributes: {
+              anc: 42,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/d89044ef-b7df-4a15-be10-d342fe381664.jpg?itok=gmiV1adi',
+              leeftijd: 42,
+              naam: 'Wieke Paulusma',
+              partij: 'D66',
+              woonplaats: 'Groningen',
+            },
+            scaledPosition: { x: 244.1558441558442, y: 100 },
+          },
+          {
+            id: 'kamerleden/105',
+            data: { text: 'kamerleden/105' },
+            originalPosition: { x: 7, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/2690bccc-5463-459a-b1df-cb93e222b348.jpg?itok=UZOQRvuv',
+              leeftijd: 37,
+              naam: 'Jan Paternotte',
+              partij: 'D66',
+              woonplaats: 'Leiden',
+            },
+            scaledPosition: { x: 270.1298701298702, y: 100 },
+          },
+          {
+            id: 'kamerleden/94',
+            data: { text: 'kamerleden/94' },
+            originalPosition: { x: 8, y: 0 },
+            attributes: {
+              anc: 3171,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/17f26a48-9f54-42a1-9890-c6da761a9a2f.jpg?itok=BA8JoO5G',
+              leeftijd: 65,
+              naam: 'Paul van Meenen',
+              partij: 'D66',
+              woonplaats: 'Leiden',
+            },
+            scaledPosition: { x: 296.10389610389615, y: 100 },
+          },
+          {
+            id: 'kamerleden/86',
+            data: { text: 'kamerleden/86' },
+            originalPosition: { x: 9, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/fb291eb2-962a-482f-971a-0b62673f9a86.jpg?itok=2J7OszgV',
+              leeftijd: 41,
+              naam: 'Jeanet van der Laan',
+              partij: 'D66',
+              woonplaats: 'Lisse',
+            },
+            scaledPosition: { x: 322.0779220779221, y: 100 },
+          },
+          {
+            id: 'kamerleden/71',
+            data: { text: 'kamerleden/71' },
+            originalPosition: { x: 10, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/8349fff0-99b3-449a-bd4d-932ae95b29da.jpg?itok=xYICmuaX',
+              leeftijd: 37,
+              naam: 'Hlya Kat',
+              partij: 'D66',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 348.0519480519481, y: 100 },
+          },
+          {
+            id: 'kamerleden/69',
+            data: { text: 'kamerleden/69' },
+            originalPosition: { x: 11, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/64457f28-37b6-4948-860a-f28bc62b99ae.jpg?itok=Q2hiR0vj',
+              leeftijd: 59,
+              naam: 'Sigrid Kaag',
+              partij: 'D66',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 374.02597402597405, y: 100 },
+          },
+          {
+            id: 'kamerleden/68',
+            data: { text: 'kamerleden/68' },
+            originalPosition: { x: 12, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/666f31ca-041f-466d-9e78-804317921d69.jpg?itok=G3ozh6C3',
+              leeftijd: 37,
+              naam: 'Romke de Jong',
+              partij: 'D66',
+              woonplaats: 'Gorredijk',
+            },
+            scaledPosition: { x: 400.0000000000001, y: 100 },
+          },
+          {
+            id: 'kamerleden/66',
+            data: { text: 'kamerleden/66' },
+            originalPosition: { x: 13, y: 0 },
+            attributes: {
+              anc: 1527,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/49be3576-cea3-46c0-87eb-89beb108248d.jpg?itok=VfqikY8P',
+              leeftijd: 34,
+              naam: 'Rob Jetten',
+              partij: 'D66',
+              woonplaats: 'Ubbergen',
+            },
+            scaledPosition: { x: 425.97402597402595, y: 100 },
+          },
+          {
+            id: 'kamerleden/134',
+            data: { text: 'kamerleden/134' },
+            originalPosition: { x: 14, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/19c8d0fb-66ca-4b45-be82-057cb36e1e61.jpg?itok=Bkq8gaTU',
+              leeftijd: 57,
+              naam: 'Hans Vijlbrief',
+              partij: 'D66',
+              woonplaats: 'Woubrugge',
+            },
+            scaledPosition: { x: 451.94805194805195, y: 100 },
+          },
+          {
+            id: 'kamerleden/52',
+            data: { text: 'kamerleden/52' },
+            originalPosition: { x: 15, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/03d66e4a-8698-44fb-9c29-625630da268c.jpg?itok=hZ9q6Llc',
+              leeftijd: 39,
+              naam: 'Alexander Hammelburg',
+              partij: 'D66',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 477.9220779220779, y: 100 },
+          },
+          {
+            id: 'kamerleden/51',
+            data: { text: 'kamerleden/51' },
+            originalPosition: { x: 16, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/425ddb12-0b71-44b6-9875-70b075dc99f7.jpg?itok=txVxBI0l',
+              leeftijd: 34,
+              naam: 'Kiki Hagen',
+              partij: 'D66',
+              woonplaats: 'Mijdrecht',
+            },
+            scaledPosition: { x: 503.8961038961039, y: 100 },
+          },
+          {
+            id: 'kamerleden/47',
+            data: { text: 'kamerleden/47' },
+            originalPosition: { x: 17, y: 0 },
+            attributes: {
+              anc: 1525,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/762e0f40-f100-4896-85a2-b1d8818fae41.jpg?itok=YM5w7UBV',
+              leeftijd: 53,
+              naam: 'Tjeerd de Groot',
+              partij: 'D66',
+              woonplaats: 'Haarlem',
+            },
+            scaledPosition: { x: 529.8701298701299, y: 100 },
+          },
+          {
+            id: 'kamerleden/121',
+            data: { text: 'kamerleden/121' },
+            originalPosition: { x: 18, y: 0 },
+            attributes: {
+              anc: 3171,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/bf030048-0434-42f1-bbed-8b0c04014db4.jpg?itok=G35Rmw4M',
+              leeftijd: 39,
+              naam: 'Sjoerd Sjoerdsma',
+              partij: 'D66',
+              woonplaats: "'s-Gravenhage",
+            },
+            scaledPosition: { x: 555.8441558441559, y: 100 },
+          },
+          {
+            id: 'kamerleden/42',
+            data: { text: 'kamerleden/42' },
+            originalPosition: { x: 19, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/49e7fc50-90f5-4476-9275-c31f88864082.jpg?itok=n7ZLWx8e',
+              leeftijd: 48,
+              naam: 'Lisa van Ginneken',
+              partij: 'D66',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 581.8181818181819, y: 100 },
+          },
+          {
+            id: 'kamerleden/22',
+            data: { text: 'kamerleden/22' },
+            originalPosition: { x: 20, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/e85125f3-b7cf-426c-9355-412838893f55.jpg?itok=Qs1_f_yV',
+              leeftijd: 42,
+              naam: 'Faissal Boulakjar',
+              partij: 'D66',
+              woonplaats: 'Teteringen',
+            },
+            scaledPosition: { x: 607.7922077922078, y: 100 },
+          },
+          {
+            id: 'kamerleden/21',
+            data: { text: 'kamerleden/21' },
+            originalPosition: { x: 21, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/13e26587-88d8-440b-bb93-527a8e845342.jpg?itok=18OPNvjY',
+              leeftijd: 45,
+              naam: 'Raoul Boucke',
+              partij: 'D66',
+              woonplaats: 'Rotterdam',
+            },
+            scaledPosition: { x: 633.7662337662338, y: 100 },
+          },
+          {
+            id: 'kamerleden/14',
+            data: { text: 'kamerleden/14' },
+            originalPosition: { x: 22, y: 0 },
+            attributes: {
+              anc: 3171,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/aa0272f6-6e3c-41f7-b040-b774b30d2196.jpg?itok=IQu5H4Bx',
+              leeftijd: 49,
+              naam: 'Vera Bergkamp',
+              partij: 'D66',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 659.7402597402597, y: 100 },
+          },
+          {
+            id: 'kamerleden/12',
+            data: { text: 'kamerleden/12' },
+            originalPosition: { x: 23, y: 0 },
+            attributes: {
+              anc: 1948,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/29124ef6-e1d5-4138-bf01-8433afde7269.jpg?itok=qw0TE4jl',
+              leeftijd: 42,
+              naam: 'Salima Belhaj',
+              partij: 'D66',
+              woonplaats: 'Rotterdam',
+            },
+            scaledPosition: { x: 685.7142857142858, y: 100 },
+          },
+        ],
+        selectedAttributeCatecorigal: 'state',
+        selectedAttributeNumerical: 'long',
+        scaleCalculation: mockCalc,
+        colourCalculation: mockCalcColour,
+        minmaxXAxis: { min: -3.4000000000000004, max: 27.4 },
+        minmaxYAxis: { min: -1, max: 1 },
+        width: 800,
+        height: 200,
+        yOffset: 250,
+        possibleTitleAttributeValues: [
+          'VVD',
+          'D66',
+          'GL',
+          'PVV',
+          'PvdD',
+          'PvdA',
+          'SGP',
+          'BIJ1',
+          'CU',
+          'BBB',
+          'CDA',
+          'SP',
+          'FVD',
+          'DENK',
+          'Fractie Den Haan',
+          'Volt',
+          'JA21',
+        ],
+      },
+      {
+        title: 'partij:GL',
+        nodes: [
+          {
+            id: 'kamerleden/140',
+            data: { text: 'kamerleden/140' },
+            originalPosition: { x: 1, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/c7822b58-103f-4612-87ef-648be97192c6.jpg?itok=jDwKCC26',
+              leeftijd: 39,
+              naam: 'Lisa Westerveld',
+              partij: 'GL',
+              woonplaats: 'Nijmegen',
+            },
+            scaledPosition: { x: 114.28571428571432, y: 100 },
+          },
+          {
+            id: 'kamerleden/89',
+            data: { text: 'kamerleden/89' },
+            originalPosition: { x: 2, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/66b2be3d-10f8-46a4-90c6-b86005df0a50.jpg?itok=obcIgEe-',
+              leeftijd: 31,
+              naam: 'Senna Maatoug',
+              partij: 'GL',
+              woonplaats: 'Leiden',
+            },
+            scaledPosition: { x: 209.52380952380958, y: 100 },
+          },
+          {
+            id: 'kamerleden/124',
+            data: { text: 'kamerleden/124' },
+            originalPosition: { x: 3, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/b50b3f0b-afc8-46e5-bfda-cb60e2935a5a.jpg?itok=7R0cTE10',
+              leeftijd: 55,
+              naam: 'Bart Snels',
+              partij: 'GL',
+              woonplaats: 'Utrecht',
+            },
+            scaledPosition: { x: 304.7619047619048, y: 100 },
+          },
+          {
+            id: 'kamerleden/87',
+            data: { text: 'kamerleden/87' },
+            originalPosition: { x: 4, y: 0 },
+            attributes: {
+              anc: 1526,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/75a44c3a-5914-4c8e-9ca4-d987853f5844.jpg?itok=kA8ZaoNf',
+              leeftijd: 56,
+              naam: 'Tom van der Lee',
+              partij: 'GL',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 400.0000000000001, y: 100 },
+          },
+          {
+            id: 'kamerleden/24',
+            data: { text: 'kamerleden/24' },
+            originalPosition: { x: 5, y: 0 },
+            attributes: {
+              anc: 1085,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/ccea26dd-dcae-4174-8945-ec5cb568e951.jpg?itok=z7gsBQYd',
+              leeftijd: 51,
+              naam: 'Laura Bromet',
+              partij: 'GL',
+              woonplaats: 'Monnickendam',
+            },
+            scaledPosition: { x: 495.23809523809535, y: 100 },
+          },
+          {
+            id: 'kamerleden/20',
+            data: { text: 'kamerleden/20' },
+            originalPosition: { x: 6, y: 0 },
+            attributes: {
+              anc: 57,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/dab4cf29-4843-4261-b09b-973fe98dbf65.jpg?itok=szVeNy9I',
+              leeftijd: 27,
+              naam: 'Kauthar Bouchallikh',
+              partij: 'GL',
+              woonplaats: 'Amsterdam',
+            },
+            scaledPosition: { x: 590.4761904761906, y: 100 },
+          },
+          {
+            id: 'kamerleden/34',
+            data: { text: 'kamerleden/34' },
+            originalPosition: { x: 7, y: 0 },
+            attributes: {
+              anc: 1637,
+              img: 'https://www.tweedekamer.nl/sites/default/files/styles/member_parlement_profile_square/public/551f9a19-738c-4298-afe1-5ca7959ab74b.jpg?itok=24RXpYrd',
+              leeftijd: 45,
+              naam: 'Corinne Ellemeet',
+              partij: 'GL',
+              woonplaats: 'Abcoude',
+            },
+            scaledPosition: { x: 685.7142857142859, y: 100 },
+          },
+        ],
+        selectedAttributeCatecorigal: 'state',
+        selectedAttributeNumerical: 'long',
+        scaleCalculation: mockCalc,
+        colourCalculation: mockCalcColour,
+        minmaxXAxis: { min: -0.20000000000000018, max: 8.2 },
+        minmaxYAxis: { min: -1, max: 1 },
+        width: 800,
+        height: 200,
+        yOffset: 500,
+        possibleTitleAttributeValues: [
+          'VVD',
+          'D66',
+          'GL',
+          'PVV',
+          'PvdD',
+          'PvdA',
+          'SGP',
+          'BIJ1',
+          'CU',
+          'BBB',
+          'CDA',
+          'SP',
+          'FVD',
+          'DENK',
+          'Fractie Den Haan',
+          'Volt',
+          'JA21',
+        ],
+      },
+    ];
+
+    // Check the plots and the relations.
+    expect(viewModelImpl.plots).toEqual(expectedPlots);
+    expect(viewModelImpl.allRelations).toEqual(expectedRelations);
+
+    // Also check if notifyViewAboutChanges is called.
+    expect(onViewModelChangedMock).toHaveBeenCalledTimes(2);
+  });
+
+  it('add a plot and delete itonDelete', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    viewModelImpl.onDelete(1);
+  });
+
+  it('onBrush', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    let expectedFilters: { x: MinMaxType; y: MinMaxType } = {
+      x: { min: 30, max: 40 },
+      y: { min: 30, max: 40 },
+    };
+
+    viewModelImpl.onBrush(1, { min: 30, max: 40 }, { min: 30, max: 40 });
+
+    expect(viewModelImpl.filtersPerPlot[1]).toEqual(expectedFilters);
+  });
+
+  it('onAxisChanges', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    const expectedX = 'nieuweXaxis';
+    const expectedY = 'nieuweYaxis';
+    const expectedX2 = 'evenly spaced';
+    const expectedY2 = '# outbound connections';
+
+    // change labels
+    viewModelImpl.onAxisLabelChanged(1, 'x', 'nieuweXaxis');
+    viewModelImpl.onAxisLabelChanged(1, 'y', 'nieuweYaxis');
+
+    expect(viewModelImpl.plotSpecifications[1].xAxisAttributeType).toEqual(expectedX);
+    expect(viewModelImpl.plotSpecifications[1].yAxisAttributeType).toEqual(expectedY);
+
+    // change labels again
+    viewModelImpl.onAxisLabelChanged(1, 'x', 'evenly spaced');
+    viewModelImpl.onAxisLabelChanged(1, 'y', '# outbound connections');
+
+    expect(viewModelImpl.plotSpecifications[1].xAxis).toEqual(expectedX2);
+    expect(viewModelImpl.plotSpecifications[1].yAxis).toEqual(expectedY2);
+  });
+
+  it('onCheckboxChanges', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    viewModelImpl.onCheckboxChanged(1, 2, true);
+
+    expect(viewModelImpl.visibleRelations[1][2]).toBeTruthy();
+  });
+
+  it('addPlot', () => {
+    const viewModelImpl = new SemanticSubstratesViewModelImpl();
+    viewModelImpl.consumeMessageFromBackend(mockSchema);
+    viewModelImpl.consumeMessageFromBackend(mockNodeLinkResult);
+
+    const onViewModelChangedMock = jest.fn();
+    const baseViewMock = {
+      onViewModelChanged: onViewModelChangedMock,
+    };
+    viewModelImpl.attachView(baseViewMock);
+
+    expect(viewModelImpl.plots).toHaveLength(3);
+
+    viewModelImpl.addPlot('partij', 'naam', 'VVD');
+
+    expect(viewModelImpl.plots).toHaveLength(4);
+  });
+});
+
+const deletePlot = [
+  {
+    title: 'partij:VVD',
+    nodes: [
+      {
+        id: 'kamerleden/148',
+        data: {
+          text: 'kamerleden/148',
+        },
+        originalPosition: {
+          x: 1,
+          y: 0,
+        },
+        scaledPosition: {
+          x: 114.28571428571432,
+          y: 100,
+        },
+      },
+    ],
+  },
+];
+
+const expectedRelations = [
+  [[], [], []],
+  [[], [], []],
+  [[], [], []],
+];
diff --git a/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..04ea7bf0c8887aeb015a14a35123cc2a9ba57fac
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx
@@ -0,0 +1,68 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import {
+  MinMaxType,
+  PlotType,
+  RelationType,
+  AxisLabel,
+  PlotSpecifications,
+  EntitiesFromSchema,
+} from './Types';
+import CalcScaledPosUseCase from './utils/CalcScaledPositionsUseCase';
+import FilterUseCase from './utils/FilterUseCase';
+
+import CalcDefaultPlotSpecsUseCase from './utils/CalcDefaultPlotSpecsUseCase';
+import ToPlotDataParserUseCase from './utils/ToPlotDataParserUseCase';
+import CalcXYMinMaxUseCase from './utils/CalcXYMinMaxUseCase';
+
+import { ClassNameMap } from '@mui/material';
+import { NodeLinkResultType, isNodeLinkResult } from '../shared/ResultNodeLinkParserUseCase';
+import CalcEntityAttrNamesFromSchemaUseCase from './utils/CalcEntityAttrNamesFromSchemaUseCase';
+import CalcEntityAttrNamesFromResultUseCase from './utils/CalcEntityAttrNamesFromResultUseCase';
+import { isSchemaResult } from '../shared/SchemaResultType';
+
+/** An implementation of the `SemanticSubstratesViewModel` to be used by the semantic-substrates view. */
+export default class SemanticSubstratesViewModel {
+  /**
+   * These functions are mock function for now, but can be properly implemented later down the line.
+   */
+  public scaleCalculation: (x: number) => number = (x: number) => {
+    return 3;
+  };
+  public colourCalculation: (x: string) => string = (x: string) => {
+    return '#d56a50';
+  };
+  public relationColourCalculation: (x: string) => string = (x: string) => {
+    return '#d49350';
+  };
+  private relationScaleCalculation: (x: number) => number = (x: number) => {
+    return 1;
+  };
+
+  /** Initializes the plots and relations arrays as empty arrays. */
+  public constructor(state: SemanticSubstratesState, setState) {
+    this.state = state;
+    this.setState = setState;
+  }
+
+
+
+
+
+  // /**
+  //  * Subscribe to the schema result routing key so that this view model is receiving results that are to be visualised.
+  //  */
+  // public subscribeToSchemaResult(): void {
+  //   Broker.instance().subscribe(this, 'schema_result');
+  // }
+
+  // /**
+  //  * Unsubscribe from the schema result routing key so that this view model no longer handles query results from the backend.
+  //  */
+  // public unSubscribeFromSchemaResult(): void {
+  //   Broker.instance().unSubscribe(this, 'schema_result');
+  // }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/Types.tsx b/libs/shared/lib/vis/semanticsubstrates/Types.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..28691f3ceffa2d19ea8b4217d5b353b0700170ce
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/Types.tsx
@@ -0,0 +1,104 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { XYPosition } from "reactflow";
+
+/** Stores the minimum and maximum of the data. */
+export type MinMaxType = {
+  min: number;
+  max: number;
+};
+
+/** The data of a node. */
+export type NodeData = {
+  text: string;
+};
+
+/** The from and to index of a relation. */
+export type RelationType = {
+  fromIndex: number;
+  toIndex: number;
+  value?: number;
+  colour?: string;
+};
+
+/** Used as input for calculating the min max values and scale the position. */
+export type PlotInputData = {
+  title: string;
+  nodes: InputNodeType[];
+  width: number;
+  height: number;
+};
+
+/** The nodes for inputdata for plot generation. */
+export interface InputNodeType {
+  id: string;
+  data: NodeData;
+  originalPosition: XYPosition;
+  attributes: Record<string, any>;
+}
+
+/** The node type for nodes in plots. */
+export interface NodeType extends InputNodeType {
+  scaledPosition: XYPosition;
+}
+
+/** Used for semantic substrate components as input. */
+export type PlotType = {
+  title: string;
+  nodes: NodeType[];
+  minmaxXAxis: { min: number; max: number };
+  minmaxYAxis: { min: number; max: number };
+  selectedAttributeNumerical: string;
+  selectedAttributeCatecorigal: string;
+  scaleCalculation: (x: number) => number;
+  colourCalculation: (x: string) => string;
+  width: number;
+  height: number;
+  yOffset: number;
+
+  //Used for autocompleting the attribute value in a plot title/filter
+  possibleTitleAttributeValues: string[];
+};
+
+/** The entities with names and attribute parameters from the schema. */
+export type EntitiesFromSchema = {
+  entityNames: string[];
+  attributesPerEntity: Record<string, AttributeNames>;
+};
+
+/** The names of attributes per datatype. */
+export type AttributeNames = {
+  textAttributeNames: string[];
+  numberAttributeNames: string[];
+  boolAttributeNames: string[];
+};
+
+/** The specifications for filtering nodes and scaling a plot. */
+export type PlotSpecifications = {
+  entity: string;
+
+  labelAttributeType: string;
+  labelAttributeValue: string;
+
+  xAxis: AxisLabel;
+  yAxis: AxisLabel;
+
+  // If the axisPositioning is attributeType, this will be used to index the attributes of a node
+  xAxisAttributeType: string;
+  yAxisAttributeType: string;
+
+  width: number;
+  height: number;
+};
+
+/** The default possible options for an axis label. */
+export enum AxisLabel {
+  evenlySpaced = 'evenly spaced',
+  outboundConnections = '# outbound connections',
+  inboundConnections = '# inbound connections',
+  byAttribute = 'byAttribute',
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstrateConfigPanel.tsx b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstrateConfigPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8270164c6fae79528e5dfc8965089f4548f6d4ef
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstrateConfigPanel.tsx
@@ -0,0 +1,80 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { useRef, useState } from 'react';
+import styles from '../SemanticSubstratesComponent.module.scss';
+import {
+  EntityWithAttributes,
+  FSSConfigPanelProps,
+} from './Types';
+import SemanticSubstratesConfigPanelViewModelImpl from './SemanticSubstratesConfigPanelViewModel';
+import SemanticSubstratesConfigPanelViewModel from './SemanticSubstratesConfigPanelViewModel';
+
+/** Component for rendering config input fields for Faceted Semantic Subrate attributes. */
+export default function FSSConfigPanel(props: FSSConfigPanelProps) {
+  let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModel =
+    new SemanticSubstratesConfigPanelViewModelImpl(props);
+
+  FSSConfigPanelViewModel.makeNodeTypes();
+  FSSConfigPanelViewModel.makeRelationTypes();
+
+  return (
+    <div className={styles.container}>
+      <p className={styles.title}>Semantic Substrates Settings:</p>
+
+      {/* Experimental options implementation */}
+      <p className={styles.subtitle}>- Visual Assignment</p>
+      <div className={styles.subContainer}>
+        <p>Node: </p>
+        <select onChange={(e) => FSSConfigPanelViewModel.onNodeChange(e.target.value)}>
+          {FSSConfigPanelViewModel.nodes.map((n) => (
+            <option key={n.name} value={n.name}>
+              {n.name}
+            </option>
+          ))}
+        </select>
+      </div>
+
+      {/* NODE ATTRIBUTES */}
+      <div className={styles.subContainer}>
+        <p>Attribute: </p>
+        <select
+          id="attribute-select"
+          onChange={(e) => {
+            FSSConfigPanelViewModel.onAttributeChange(e.target.value);
+          }}
+        >
+          {FSSConfigPanelViewModel.getUniqueNodeAttributes().map((name) => (
+            <option key={name} value={name}>
+              {name}
+            </option>
+          ))}
+        </select>
+      </div>
+
+      {/* RELATION ATTRIBUTES */}
+      <div className={styles.subContainer}>
+        <p>Relation Attribute: </p>
+        <select
+          id="relation-attribute-select"
+          onChange={(e) => {
+            FSSConfigPanelViewModel.onRelationAttributeChange(e.target.value);
+          }}
+        >
+          {FSSConfigPanelViewModel.getUniqueRelationAttributes().map((name) => (
+            <option key={name} value={name}>
+              {name}
+            </option>
+          ))}
+        </select>
+      </div>
+    </div>
+  );
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.t b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.t
new file mode 100644
index 0000000000000000000000000000000000000000..3075b2e5bbc8162bd3845b495ec2f8058c5847c8
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.t
@@ -0,0 +1,280 @@
+import SemanticSubstratesConfigPanelViewModelImpl from './SemanticSubstratesConfigPanelViewModel';
+import SemanticSubstratesViewModelImpl from '../SemanticSubstratesViewModel';
+import big2ndChamberQueryResult from '../../../../data/mock-data/query-result/big2ndChamberQueryResult';
+import smallFlightsQueryResults from '../../../../data/mock-data/query-result/smallFlightsQueryResults';
+
+jest.mock('../../../view/result-visualisations/semantic-substrates/SemanticSubstratesStylesheet');
+
+let parties = ['BIJ1', 'VVD', 'PvdA', 'PVV', 'BBB', 'D66', 'GL'];
+
+describe('SemanticSubstratesConfigPanelViewModelImpl: unique attribute creation', () => {
+  it('Should consume result corectly to generate the correct unique node attributes.', () => {
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: big2ndChamberQueryResult,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange('kamerleden');
+    expect(FSSConfigPanelViewModel.getUniqueNodeAttributes()).toEqual([
+      'anc',
+      'img',
+      'leeftijd',
+      'naam',
+      'partij',
+      'woonplaats',
+    ]);
+  });
+
+  it('Should consume result corectly to generate the correct unique relation attributes.', () => {
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: big2ndChamberQueryResult,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    expect(FSSConfigPanelViewModel.getUniqueRelationAttributes()).toEqual([]);
+  });
+});
+
+describe('SemanticSubstratesConfigPanelViewModelImpl: unique attribute creation', () => {
+  it('Should consume result corectly to generate the correct unique node attributes.', () => {
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange('airports');
+    expect(FSSConfigPanelViewModel.getUniqueNodeAttributes()).toEqual([
+      'city',
+      'country',
+      'lat',
+      'long',
+      'name',
+      'state',
+      'vip',
+    ]);
+  });
+
+  it('Should consume result corectly to generate the correct unique relation attributes.', () => {
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    expect(FSSConfigPanelViewModel.getUniqueRelationAttributes()).toEqual([
+      'Year',
+      'Month',
+      'Day',
+      'DayOfWeek',
+      'DepTime',
+      'ArrTime',
+      'DepTimeUTC',
+      'ArrTimeUTC',
+      'UniqueCarrier',
+      'FlightNum',
+      'TailNum',
+      'Distance',
+    ]);
+  });
+});
+
+describe('SemanticSubstratesConfigPanelViewModelImpl: Correct values should be set', () => {
+  it("Should consume result corectly and get the calculations for the numerical attribute 'leeftijd' ", () => {
+    let attributeToSelect = 'leeftijd';
+    let nodeToSelect = 'kamerleden';
+    let maxAgeInParlement = 69;
+    let minAgeInParlement = 23;
+    let minSize = 1;
+    let maxSize = 10;
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: big2ndChamberQueryResult,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange(nodeToSelect);
+
+    expect(FSSConfigPanelViewModel.currentNode).toEqual(nodeToSelect);
+    expect(FSSConfigPanelViewModel.isNodeAttributeNumber(attributeToSelect)).toEqual(true);
+
+    let calculationF = FSSConfigPanelViewModel.getTheScaleCalculationForNodes(
+      attributeToSelect,
+      minSize,
+      maxSize,
+    );
+    expect(calculationF(maxAgeInParlement)).toEqual(maxSize);
+    expect(calculationF((maxAgeInParlement + minAgeInParlement) / 2)).toEqual(
+      (maxSize + minSize) / 2,
+    );
+    expect(calculationF(minAgeInParlement)).toEqual(minSize);
+  });
+
+  it("Should consume result corectly and get the calculations for the catecorigal attribute 'partij' ", () => {
+    let attributeToSelect = 'partij';
+    let nodeToSelect = 'kamerleden';
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: big2ndChamberQueryResult,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange(nodeToSelect);
+
+    expect(FSSConfigPanelViewModel.currentNode).toEqual(nodeToSelect);
+    expect(FSSConfigPanelViewModel.isNodeAttributeNumber(attributeToSelect)).toEqual(false);
+
+    let calculationF = FSSConfigPanelViewModel.getTheColourCalculationForNodes(attributeToSelect);
+    parties.forEach((party) => expect(calculationF(party)).toBeDefined());
+    expect(calculationF('Not a party')).toBeUndefined();
+  });
+});
+
+describe('SemanticSubstratesConfigPanelViewModelImpl: Correct values should be set for smallFlightsQueryResults', () => {
+  it("Should consume result corectly and get the calculations for the numerical attribute 'distance' ", () => {
+    let attributeToSelect = 'Distance';
+    let maxDistance = 487;
+    let minDistance = 200;
+    let minSize = 1;
+    let maxSize = 10;
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+
+    expect(FSSConfigPanelViewModel.isRelationAttributeNumber(attributeToSelect)).toEqual(true);
+
+    let calculationF = FSSConfigPanelViewModel.getTheScaleCalculationForRelations(
+      attributeToSelect,
+      minSize,
+      maxSize,
+    );
+    expect(calculationF(maxDistance)).toEqual(maxSize);
+    expect(calculationF((maxDistance + minDistance) / 2)).toEqual((maxSize + minSize) / 2);
+    expect(calculationF(minDistance)).toEqual(minSize);
+  });
+
+  it("Should consume result corectly and get the calculations for the catecorigal attribute 'UniqueCarrier' ", () => {
+    let attributeToSelect = 'UniqueCarrier';
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+
+    expect(FSSConfigPanelViewModel.isRelationAttributeNumber(attributeToSelect)).toEqual(false);
+
+    let calculationF =
+      FSSConfigPanelViewModel.getTheColourCalculationForRelations(attributeToSelect);
+    expect(calculationF('NW')).toBeDefined();
+    expect(calculationF('WN')).toBeDefined();
+    expect(calculationF('Not a UniqueCarrier')).toBeUndefined();
+  });
+
+  it("Should consume result corectly and get the calculations for the catecorigal attribute 'state' ", () => {
+    let attributeToSelect = 'state';
+    let nodeToSelect = 'airports';
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange(nodeToSelect);
+
+    expect(FSSConfigPanelViewModel.currentNode).toEqual(nodeToSelect);
+    expect(FSSConfigPanelViewModel.isNodeAttributeNumber(attributeToSelect)).toEqual(false);
+
+    let calculationF = FSSConfigPanelViewModel.getTheColourCalculationForNodes(attributeToSelect);
+    expect(calculationF('CA')).toBeDefined();
+    expect(calculationF('NY')).toBeDefined();
+    expect(calculationF('Not a state')).toBeUndefined();
+  });
+
+  it('Should update the visualisation when the attribute changes', () => {
+    let attributeToSelect = 'state';
+    let attributeToSelect2 = 'lat';
+    let nodeToSelect = 'airports';
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange(nodeToSelect);
+
+    // change the attribute for the first time
+    FSSConfigPanelViewModel.onAttributeChange(attributeToSelect);
+    expect(FSSConfigPanelViewModel.currentAttribute).toEqual(attributeToSelect);
+
+    // change the attribute for the second time, this time with an attribute that has numbers as values
+    FSSConfigPanelViewModel.onAttributeChange(attributeToSelect2);
+    expect(FSSConfigPanelViewModel.currentAttribute).toEqual(attributeToSelect2);
+  });
+
+  it('Should update the visualisation when the relation attribute changes', () => {
+    let attributeToSelect = 'TailNum';
+    let attributeToSelect2 = 'Distance';
+    let nodeToSelect = 'airports';
+    let mockProperties = {
+      fssViewModel: new SemanticSubstratesViewModelImpl(),
+      graph: smallFlightsQueryResults,
+    };
+
+    let FSSConfigPanelViewModel: SemanticSubstratesConfigPanelViewModelImpl =
+      new SemanticSubstratesConfigPanelViewModelImpl(mockProperties);
+
+    FSSConfigPanelViewModel.makeRelationTypes();
+    FSSConfigPanelViewModel.makeNodeTypes();
+    FSSConfigPanelViewModel.onNodeChange(nodeToSelect);
+
+    // change the attribute for the first time
+    FSSConfigPanelViewModel.onRelationAttributeChange(attributeToSelect);
+    expect(FSSConfigPanelViewModel.currentRelationAttribute).toEqual(attributeToSelect);
+
+    // change the attribute for the second time, this time with an attribute that has numbers as values
+    FSSConfigPanelViewModel.onRelationAttributeChange(attributeToSelect2);
+    expect(FSSConfigPanelViewModel.currentRelationAttribute).toEqual(attributeToSelect2);
+  });
+});
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4f257a6af7a3753be55996c9f374bc2d77226e4a
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx
@@ -0,0 +1,355 @@
+import { range } from 'd3';
+
+import {
+  EntityWithAttributes,
+  FSSConfigPanelProps,
+} from './Types';
+import { Link, Node } from '../../shared/ResultNodeLinkParserUseCase';
+import SemanticSubstratesViewModel from '../SemanticSubstratesViewModel';
+
+/** Viewmodel for rendering config input fields for Faceted Semantic Substrate attributes. */
+export default class SemanticSubstratesConfigPanelViewModel {
+  public nodes: EntityWithAttributes[];
+  public relations: EntityWithAttributes[];
+
+  minimalNodeSize = 2;
+  maximalNodeSize = 10;
+
+  minimalRelationWidth = 0.05;
+  maximalRelationWidth = 4;
+
+  nodesloaded: Node[];
+  relationsloaded: Link[];
+  public currentNode = '';
+  currentRelation = '';
+  currentAttribute = '';
+  currentRelationAttribute = '';
+  // Faceted Semantic Substrates View Model.
+  fssViewModel: SemanticSubstratesViewModel;
+  /**
+   * The constructor for the FSSConfigPanelViewModelImpl (FacetedSemanticSubstratesConfigPanelViewModelImpl).
+   * This handles the view for changing how to display the attributes of the nodes and relations
+   * in the FSS view.
+   * @param props The properties for the FacetedSemanticSubstratesConfigPanel. These define how the
+   * fss ViewModel is generated.
+   */
+  public constructor(props: FSSConfigPanelProps) {
+    let graph = props.graph;
+    let fssViewModel = props.fssViewModel;
+
+    this.nodes = [];
+    this.relations = [];
+
+    this.nodesloaded = graph.nodes;
+    this.relationsloaded = graph.edges;
+    this.fssViewModel = fssViewModel;
+  }
+
+  /**
+   * Creates a list of unique node types based on the current list of nodes.
+   */
+  public makeNodeTypes() {
+    this.nodes = MakeTypesFromGraphUseCase.makeNodeTypes(this.nodesloaded);
+    if (!this.isNodeSet()) {
+      if (this.nodes[0]) {
+        this.currentNode = this.nodes[0].name;
+      }
+    }
+  }
+
+  /**
+   * Creates a list of unique relation types based on the current list of relations.
+   */
+  public makeRelationTypes() {
+    this.relations = MakeTypesFromGraphUseCase.makeRelationTypes(this.relationsloaded);
+    if (!this.isRelationSet()) {
+      if (this.relations[0]) {
+        this.currentRelation = this.relations[0].name;
+      }
+    }
+  }
+
+  /**
+   * Checks if the current node type exists in the types list.
+   * @returns True if the node has been set.
+   */
+  private isNodeSet() {
+    for (let i in range(this.nodes.length)) {
+      let node = this.nodes[i];
+      if (this.currentNode == node.name) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Checks if the current relation type exists in the types list.
+   * @returns True if the function has been set.
+   */
+  private isRelationSet() {
+    for (let i in range(this.relations.length)) {
+      let relation = this.relations[i];
+      if (this.currentRelation == relation.name) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Updates the state variables and dropdowns. Should be triggered on switching nodes in the dropdown.
+   * @param newCurrentNode The new node type that has been selected.
+   */
+  public onNodeChange(newCurrentNode: string) {
+    this.currentNode = newCurrentNode;
+    this.makeNodeTypes();
+  }
+
+  /**
+   * Tells the FSSViewModel what node attributes have been changed, when something has changed.
+   */
+  public onAttributeChange(attributeSelected: string) {
+    //Retrieve the current visualisation and set the vis dropdown to this.
+    this.currentAttribute = attributeSelected;
+    if (this.isNodeAttributeNumber(attributeSelected)) {
+      this.fssViewModel.selectedAttributeNumerical = attributeSelected;
+      this.fssViewModel.changeSelectedAttributeNumerical(
+        this.getTheScaleCalculationForNodes(
+          attributeSelected,
+          this.minimalNodeSize,
+          this.maximalNodeSize,
+        ),
+      );
+    } else {
+      this.fssViewModel.selectedAttributeCatecorigal = attributeSelected;
+      this.fssViewModel.changeSelectedAttributeCatecorigal(
+        this.getTheColourCalculationForNodes(attributeSelected),
+      );
+    }
+  }
+
+  /**
+   * Tells the FSSViewModel what relation attributes have been changed, when something has changed.
+   */
+  public onRelationAttributeChange(attributeSelected: string) {
+    //Retrieve the current visualisation and set the vis dropdown to this.
+    this.currentRelationAttribute = attributeSelected;
+    if (this.isRelationAttributeNumber(attributeSelected)) {
+      this.fssViewModel.relationSelectedAttributeNumerical = attributeSelected;
+      this.fssViewModel.changeRelationSelectedAttributeNumerical(
+        this.getTheScaleCalculationForRelations(
+          this.fssViewModel.relationSelectedAttributeNumerical,
+          this.minimalRelationWidth,
+          this.maximalRelationWidth,
+        ),
+      );
+    } else {
+      this.fssViewModel.relationSelectedAttributeCatecorigal = attributeSelected;
+      this.fssViewModel.changeRelationSelectedAttributeCatecorigal(
+        this.getTheColourCalculationForRelations(
+          this.fssViewModel.relationSelectedAttributeCatecorigal,
+        ),
+      );
+    }
+  }
+
+  /**
+   * Gets a list of all different attributes that are present in the loaded nodes list.
+   * @returns List of strings that each represent an attribute.
+   */
+  public getUniqueNodeAttributes() {
+    let uniqueValues: string[] = [];
+    this.nodesloaded.forEach((node) => {
+      if (node.attributes) {
+        for (const attribute in node.attributes) {
+          if (uniqueValues.find((x) => x == attribute) == undefined) {
+            uniqueValues.push(attribute);
+          }
+        }
+      }
+    });
+    return uniqueValues;
+  }
+
+  /**
+   * Gets a list with unique relation attributes that are presented in the loaded relations list.
+   * @returns List of string that each represent an attribute.
+   */
+  public getUniqueRelationAttributes() {
+    let uniqueValues: string[] = [];
+    this.relationsloaded.forEach((relation) => {
+      if (relation.attributes) {
+        for (const attribute in relation.attributes) {
+          if (uniqueValues.find((x) => x == attribute) == undefined) {
+            uniqueValues.push(attribute);
+          }
+        }
+      }
+    });
+    return uniqueValues;
+  }
+
+  /**
+   * Checks if the attribute is a numerical value.
+   * @param attribute The attribute of the nodes to check for.
+   * @returns True if all values of that attribute are numbers. Returns false
+   * if one or more values for this attribute are not a number
+   */
+  public isNodeAttributeNumber(attribute: string): boolean {
+    let values: number[] = [];
+    for (const node of this.nodesloaded) {
+      if (node.attributes) {
+        if (node.attributes[attribute] != undefined) {
+          if (isNaN(node.attributes[attribute])) return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks if all values of an attribute of the list of relations are a number.
+   * @param attribute The attribute of the relations to check for
+   * @returns True if all values of that attribute are numbers. Returns false
+   * if one or more values for this attribute are not a number
+   */
+  public isRelationAttributeNumber(attribute: string): boolean {
+    for (const relation of this.relationsloaded) {
+      if (relation.attributes) {
+        if (relation.attributes[attribute] != undefined) {
+          if (isNaN(relation.attributes[attribute])) return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Gets the scaling value for pixel sizes.
+   * @param attribute The selected attribute.
+   * @param minP Minimum node size.
+   * @param maxP Maximum node size.
+   * @returns Scaling value as a number.
+   */
+  public getTheScaleCalculationForNodes(
+    attribute: string,
+    minP: number,
+    maxP: number,
+  ): (x: number) => number {
+    let values: number[] = [];
+    this.nodesloaded.forEach((node) => {
+      if (node.attributes) {
+        if (node.attributes[attribute] != undefined) {
+          values.push(node.attributes[attribute]);
+        }
+      }
+    });
+
+    //value min/max
+    let minX = Math.min(...values);
+    let maxX = Math.max(...values);
+
+    let a = (maxP - minP) / (maxX - minX);
+    let b = maxP - a * maxX;
+
+    //linear scaling between minP and maxP
+    return (x: number) => a * x + b; // - minX;
+  }
+
+  /**
+   * Creates a function to get the colours for each value of a given attribute of a relation.
+   * @param attribute The attribute to generate a colour calculation for.
+   * @returns Returns colourisation fucntion for each different attribute value
+   */
+  public getTheColourCalculationForNodes(attribute: string): (x: string) => string {
+    let uniqueValues: string[] = [];
+    this.nodesloaded.forEach((node) => {
+      if (node.attributes) {
+        if (node.attributes[attribute] != undefined) {
+          uniqueValues.push(node.attributes[attribute]);
+        }
+      }
+    });
+
+    let colours = ColourPalettes['default'].nodes;
+
+    // Create the key value pairs.
+    let valueToColour: Record<string, string> = {};
+    let i = 0;
+    uniqueValues.forEach((uniqueValue) => {
+      valueToColour[uniqueValue] = '#' + colours[i];
+      i++;
+      if (i > colours.length) {
+        i = 0;
+      }
+    });
+
+    //Get a colour for each attribute
+    return (x: string) => valueToColour[x];
+  }
+
+  /**
+   * returns the scaling function for relations. (Can be used in pixel size.)
+   * @param attribute Attribute to generate a size calculation for.
+   * @param minP Minimal width
+   * @param maxP Maximum width
+   * @returns
+   */
+  public getTheScaleCalculationForRelations(
+    attribute: string,
+    minP: number,
+    maxP: number,
+  ): (x: number) => number {
+    let values: number[] = [];
+    this.relationsloaded.forEach((relation) => {
+      if (relation.attributes) {
+        if (relation.attributes[attribute] != undefined) {
+          values.push(relation.attributes[attribute]);
+        }
+      }
+    });
+
+    //value min/max
+    let minX = Math.min(...values);
+    let maxX = Math.max(...values);
+
+    let a = (maxP - minP) / (maxX - minX);
+    let b = maxP - a * maxX;
+
+    //linear scaling between minP and maxP
+    return (x: number) => a * x + b; // - minX;
+  }
+
+  /**
+   * Generates a function that returns a value for each given possible value of an attribute.
+   * @param attribute The attribute to generate the function for.
+   * @returns A function to get the correct colour for a relation attribute.
+   */
+  public getTheColourCalculationForRelations(attribute: string): (x: string) => string {
+    let uniqueValues: string[] = [];
+    this.relationsloaded.forEach((relation) => {
+      if (relation.attributes) {
+        if (relation.attributes[attribute] != undefined) {
+          uniqueValues.push(relation.attributes[attribute]);
+        }
+      }
+    });
+
+    let colours = ColourPalettes['default'].elements.relation;
+
+    // Create the key value pairs.
+    let valueToColour: Record<string, string> = {};
+    let i = 0;
+    uniqueValues.forEach((uniqueValue) => {
+      valueToColour[uniqueValue] = '#' + colours[i];
+      i++;
+      if (i > colours.length) {
+        i = 0;
+      }
+    });
+
+    //Get a colour for each attribute
+    return (x: string) => valueToColour[x];
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx b/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3a7462d70c02e0b0505ecfe11b0962d9fa7c4a3e
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx
@@ -0,0 +1,23 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { NodeLinkResultType } from "../../shared/ResultNodeLinkParserUseCase";
+import SemanticSubstratesViewModel from "../SemanticSubstratesViewModel";
+
+/* An entity that has an attribute (Either a node with attributes or an edges with attributes)
+  For the config-panel of semantic-substrates.*/
+export type EntityWithAttributes = {
+  name: string;
+  attributes: string[];
+  group: number;
+};
+
+/** Props for this component */
+export type FSSConfigPanelProps = {
+  graph: NodeLinkResultType;
+  // currentColours: any;
+  fssViewModel: SemanticSubstratesViewModel;
+};
diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3784b6597965313ec3b952d0945f1fccf5d820c9
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss
@@ -0,0 +1,106 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+.container {
+  font-family: 'Open Sans', sans-serif;
+  display: flex;
+  flex-direction: column;
+  p {
+    margin: 0.5rem 0;
+    font-size: 13px;
+    font-weight: 600;
+    color: #2d2d2d;
+  }
+  .title {
+    color: #212020;
+    font-weight: 800;
+    line-height: 1.6em;
+    font-size: 16px;
+    text-align: center;
+    margin-bottom: 1rem;
+    margin-top: 0;
+  }
+  .subtitle {
+    color: #212020;
+    font-weight: 700;
+    font-size: 14px;
+    margin-top: 1.5rem;
+  }
+  .subsubtitle {
+    font-weight: 700;
+    margin-top: 0.9rem;
+  }
+  .subContainer {
+    .rulesContainer {
+      margin-top: 0.5rem;
+      .subsubtitle {
+        text-align: center;
+      }
+    }
+  }
+}
+
+.selectContainer {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  select {
+    width: 6rem;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    option {
+      width: 35px;
+      text-overflow: ellipsis;
+    }
+  }
+}
+
+.container {
+  // @global TODO FIX
+  .selection {
+    fill-opacity: 0.1;
+    stroke: lightgrey;
+  }
+
+  .delete-plot-icon {
+    & path {
+      opacity: 0.4;
+      transition: 0.1s;
+
+      transform-box: fill-box;
+    }
+    &:hover {
+      & path {
+        opacity: 0.8;
+        fill: #d00;
+        transform: scale(1.1);
+      }
+      & .lid {
+        transform: rotate(-15deg) scale(1.1);
+      }
+    }
+  }
+
+  display: flex;
+  flex-direction: row;
+
+  width: 100%;
+
+  margin-top: 10px;
+  margin-left: 30px;
+  overflow-y: auto;
+}
+
+.checkboxGroup {
+  display: flex;
+  flex-direction: column;
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss.d.ts b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e99e35702b7241ffb7e3cf1132a12d9367a0fdb
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.module.scss.d.ts
@@ -0,0 +1,14 @@
+declare const classNames: {
+  readonly container: 'container';
+  readonly title: 'title';
+  readonly subtitle: 'subtitle';
+  readonly subsubtitle: 'subsubtitle';
+  readonly subContainer: 'subContainer';
+  readonly rulesContainer: 'rulesContainer';
+  readonly selectContainer: 'selectContainer';
+  readonly selection: 'selection';
+  readonly 'delete-plot-icon': 'delete-plot-icon';
+  readonly lid: 'lid';
+  readonly checkboxGroup: 'checkboxGroup';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx
index bf93c79f47039ef123bfed274b23fb5d6bbedc78..49af3a0c219b02ac5e41e4d102bc21f401789677 100644
--- a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.stories.tsx
@@ -1,6 +1,9 @@
 import {
+  assignNewGraphQueryResult,
   colorPaletteConfigSlice,
   graphQueryResultSlice,
+  schemaSlice,
+  setSchema,
 } from "../../data-access/store";
 import { GraphPolarisThemeProvider } from "../../data-access/theme";
 import { configureStore } from "@reduxjs/toolkit";
@@ -8,6 +11,7 @@ import { Meta, ComponentStory } from "@storybook/react";
 import { Provider } from "react-redux";
 
 import SemanticSubstrates from "./semanticsubstrates";
+import { SchemaUtils } from "../../schema/schema-utils";
 
 const Component: Meta<typeof SemanticSubstrates> = {
   /* 👇 The title prop is optional.
@@ -27,24 +31,50 @@ const Component: Meta<typeof SemanticSubstrates> = {
 
 const Mockstore = configureStore({
   reducer: {
+    schema: schemaSlice.reducer,
     colorPaletteConfig: colorPaletteConfigSlice.reducer,
     graphQueryResult: graphQueryResultSlice.reducer,
   },
 });
-const Template: ComponentStory<typeof SemanticSubstrates> = (args) => (
-  <SemanticSubstrates />
-);
 
-export const Primary = Template.bind({});
-
-Primary.args = {
-  content: "Testing header #1",
-};
-
-export const Secondary = Template.bind({});
+export const TestWithData = {
+  args: {
+    loading: false,
+  },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    const schema = SchemaUtils.schemaBackend2Graphology({
+      nodes: [
+        {
+          name: '1',
+          attributes: [{ name: 'a', type: 'string' }],
+        },
+      ],
+      edges: [
+        {
+          name: '12', from: '1', to: '1', collection: '1c', attributes: [{ name: 'a', type: 'string' }],
+        },
+      ],
+    });
 
-Secondary.args = {
-  content: "Testing header number twoooo",
-};
+    dispatch(setSchema(schema.export()));
+    dispatch(
+      assignNewGraphQueryResult({
+        nodes: [
+          { id: '1/a', attributes: { a: 's1' } },
+          { id: '1/b1', attributes: { a: 's1' } },
+          { id: '1/b2', attributes: { a: 's1' } },
+          { id: '1/b3', attributes: { a: 's1' } },
+        ],
+        edges: [
+          { id: '12/z1', from: '1/b1', to: '1/a', attributes: { a: 's1' } },
+          // { from: 'b2', to: 'a', attributes: {} },
+          // { from: 'b3', to: 'a', attributes: {} },
+          { id: '12/z1', from: '1/a', to: '1/b1', attributes: { a: 's1' } },
+        ],
+      })
+    )
+  }
+}
 
 export default Component;
diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
index 7465247d4e3ec354314acdb4f2a38e2740d79fcf..f5149f537fa7466c34a26b14beb913e7e7a59211 100644
--- a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
@@ -1,85 +1,529 @@
-import { Theme, useTheme } from '@mui/material/styles';
-import Box from '@mui/system/Box';
+import { useTheme } from '@mui/material';
 import {
-  changeDataPointColors,
-  changePrimary,
-  toggleDarkMode,
-  useAppDispatch,
+  useAppDispatch, useGraphQueryResult, useSchemaGraph,
 } from '@graphpolaris/shared/lib/data-access/store';
-import styled from 'styled-components';
-import { Button } from '@mui/material';
+import { useEffect, useRef, useState } from 'react';
+import { AxisLabel, EntitiesFromSchema, MinMaxType, PlotSpecifications, PlotType, RelationType } from './Types';
+import { NodeLinkResultType, isNodeLinkResult } from '../shared/ResultNodeLinkParserUseCase';
+import styles from './semanticsubstrates.module.scss';
+import AddPlotButtonComponent from './subcomponents/AddPlotButtonComponent';
+import SVGCheckboxesWithSemanticSubstrLabel from './subcomponents/SVGCheckBoxComponent';
+import AddPlotPopup from './subcomponents/AddPlotPopup';
+import VisConfigPanelComponent from '../shared/VisConfigPanel/VisConfigPanel';
+import FSSConfigPanel from './configpanel/SemanticSubstrateConfigPanel';
+import Plot from './subcomponents/PlotComponent';
+import LinesBetweenPlots from './subcomponents/LinesBetweenPlotsComponent';
+import Color from 'color';
+import CalcEntityAttrNamesFromResultUseCase from './utils/CalcEntityAttrNamesFromResultUseCase';
+import CalcEntityAttrNamesFromSchemaUseCase from './utils/CalcEntityAttrNamesFromSchemaUseCase';
+import CalcDefaultPlotSpecsUseCase from './utils/CalcDefaultPlotSpecsUseCase';
+import ToPlotDataParserUseCase from './utils/ToPlotDataParserUseCase';
+import CalcXYMinMaxUseCase from './utils/CalcXYMinMaxUseCase';
+import CalcScaledPosUseCase from './utils/CalcScaledPositionsUseCase';
+import { useImmer } from "use-immer";
+import FilterUseCase from './utils/FilterUseCase';
 
-const Div = styled.div`
-  // add :{ theme: Theme } if you want to have typing (autocomplete)
-  background-color: ${({ theme }: { theme: Theme }) =>
-    theme.palette.primary.main};
-`;
+export type SemanticSubstrateState = {
+  plotSpecifications: PlotSpecifications[];
+
+  nodeRadius: number;
+  nodeColors: string[];
+
+  gapBetweenPlots: number;
+
+  entitiesFromSchemaPruned: EntitiesFromSchema;
+
+  selectedAttributeNumerical: string;
+  selectedAttributeCatecorigal: string;
+  relationSelectedAttributeNumerical: string;
+  relationSelectedAttributeCatecorigal: string;
+  scaleCalculation: (x: number) => number,
+  colourCalculation: (x: string) => string,
+
+  relationScaleCalculation: (x: number) => number,
+  relationColourCalculation: (x: string) => string,
+
+  addPlotButtonAnchor: (EventTarget & SVGRectElement) | null;
+}
+
+export type SemanticSubstratePlotState = {
+  plots: PlotType[];
+
+  // Relations is a 3d array, with the first array denoting the outbound plot, the second array denotes the inbound plot
+  // For example, [plot1][plot2] will give all edges which are outbound from plot1 and ingoing to plot2
+  allRelations: RelationType[][][];
+  filteredRelations: RelationType[][][];
+
+  // Determines if connections [fromPlotIndex][toPlotIndex] will be shown
+  visibleRelations: boolean[][];
+
+  // Used for filtering the relations when brushing (brush is the square selection tool)
+  filtersPerPlot: { x: MinMaxType; y: MinMaxType }[];
+}
+
+/**
+ * These functions are mock function for now, but can be properly implemented later down the line.
+ */
+const scaleCalculation: (x: number) => number = (x: number) => {
+  return 3;
+};
+const colourCalculation: (x: string) => string = (x: string) => {
+  return '#d56a50';
+};
+const relationColourCalculation: (x: string) => string = (x: string) => {
+  return '#d49350';
+};
+const relationScaleCalculation: (x: number) => number = (x: number) => {
+  return 1;
+};
 
 const SemanticSubstrates = () => {
   const dispatch = useAppDispatch();
   const theme = useTheme();
+  const graphQueryResult = useGraphQueryResult();
+  const schema = useSchemaGraph();
+  const [entitiesFromSchema, setEntitiesFromSchema] = useState<EntitiesFromSchema>({ entityNames: [], attributesPerEntity: {} });
 
-  return (
-    <>
-      <Div>
-        <h1>semantic substrates, primary.main</h1>
-      </Div>
-      <div
-        style={{
-          backgroundColor: theme.palette.primary.dark,
-        }}
-      >
-        <h1>semantic substrates, primary.dark</h1>
-      </div>
-      <input
-        onChange={(v) =>
-          dispatch(
-            changePrimary({
-              main: v.currentTarget.value,
-              darkMode: theme.palette.mode,
-            })
-          )
+  const [state, setState] = useImmer<SemanticSubstrateState>({
+    plotSpecifications: [],
+    entitiesFromSchemaPruned: { entityNames: [], attributesPerEntity: {} },
+    selectedAttributeNumerical: 'long', selectedAttributeCatecorigal: 'state',
+    relationSelectedAttributeNumerical: 'Distance', relationSelectedAttributeCatecorigal: 'Day',
+    nodeRadius: 3,
+    nodeColors: ['#D56A50', '#1E9797', '#d49350', '#1e974a', '#D49350'],
+    gapBetweenPlots: 50,
+    scaleCalculation: scaleCalculation,
+    colourCalculation: colourCalculation,
+    relationScaleCalculation: relationScaleCalculation,
+    relationColourCalculation: relationColourCalculation,
+
+    addPlotButtonAnchor: null,
+  });
+
+  const [plotState, setPlotState] = useImmer<SemanticSubstratePlotState>({
+    allRelations: [], filteredRelations: [], plots: [], filtersPerPlot: [], visibleRelations: [],
+  });
+
+  useEffect(() => {
+    const ret = CalcEntityAttrNamesFromSchemaUseCase.calculate(schema);
+    setEntitiesFromSchema(ret);
+  }, [schema]);
+
+  useEffect(() => {
+    if (isNodeLinkResult(graphQueryResult)) {
+      // Only apply new result if we have a valid schema
+      if (entitiesFromSchema.entityNames.length === 0) {
+        console.log('Semantic substrates: No valid schema available.');
+        return;
+      }
+
+      setState((draft) => {
+        draft.entitiesFromSchemaPruned = CalcEntityAttrNamesFromResultUseCase.CalcEntityAttrNamesFromResult(
+          graphQueryResult,
+          entitiesFromSchema,
+        );
+
+        // Generate default plots, if there aren't any currently
+        if (state.plotSpecifications.length == 0) {
+          draft.plotSpecifications = CalcDefaultPlotSpecsUseCase.calculate(graphQueryResult);
         }
-        type="color"
-        name="head"
-        value={theme.palette.primary.main}
-      />
-      <p>Change Primary Color</p>
-      <input
-        onChange={(v) =>
-          dispatch(
-            changeDataPointColors([
-              v.currentTarget.value,
-              ...theme.palette.custom.dataPointColors.slice(1),
-            ])
-          )
+
+        return draft;
+      })
+
+    } else {
+      console.error('Invalid query result!')
+    }
+
+    applyNewPlotSpecifications();
+  }, [graphQueryResult, entitiesFromSchema]);
+
+  useEffect(() => { applyNewPlotSpecifications(); }, [state]);
+
+
+
+  /**
+   * Update the scaling of the nodes after a new attribute has been selected. This function is called when the new attribute has numbers as values.
+   * @param scale The scaling function provided by the config panel ViewModel.
+   */
+  function changeSelectedAttributeNumerical(scale: (x: number) => number): void {
+    setState({ ...state, scaleCalculation: scale });
+  }
+
+  /**
+   * Update the colours of the nodes after a new attribute has been selected. This function is called when the new attribute has NaNs as values.
+   * @param getColour The colouring function provided by the config panel ViewModel.
+   */
+  function changeSelectedAttributeCatecorigal(getColour: (x: string) => string): void {
+    setState({ ...state, colourCalculation: getColour });
+  }
+
+  /**
+   * Update the scaling of the nodes after a new relation attribute has been selected. This function is called when the new relation attribute has numbers as values.
+   * @param scale The scaling function provided by the config panel ViewModel.
+   */
+  function changeRelationSelectedAttributeNumerical(scale: (x: number) => number): void {
+    setState({ ...state, relationScaleCalculation: scale });
+  }
+
+  /**
+   * Update the colours of the nodes after a new relation attribute has been selected. This function is called when the new relation attribute has NaNs as values.
+   * @param getColour The colouring function provided by the config panel ViewModel.
+   */
+  function changeRelationSelectedAttributeCatecorigal(getColour: (x: string) => string): void {
+    setState({ ...state, relationColourCalculation: getColour });
+  }
+
+  /**
+   * Apply plot specifications to a node link query result. Calculates the plots and relations using the provided plot specs.
+   * @param {PlotSpecifications[]} plotSpecs The plotspecs to filter the new plots with.
+   * @param {NodeLinkResultType} queryResult The query result to apply the plot specs to.
+   */
+  function applyNewPlotSpecifications(): void {
+
+    // Parse the incoming data to plotdata with the auto generated plot specifications
+    const { plots, relations } = ToPlotDataParserUseCase.parseQueryResult(
+      graphQueryResult,
+      state.plotSpecifications,
+      state.relationSelectedAttributeNumerical,
+      state.relationSelectedAttributeCatecorigal,
+      state.relationScaleCalculation,
+      state.relationColourCalculation,
+    );
+
+    setPlotState((draft) => {
+      draft.allRelations = relations;
+      // Clone relations to filteredRelations, because the filters are initialized with no constraints
+      draft.filteredRelations = relations.map((i) => i.map((j) => j.map((v) => v)));
+
+      // Calculate the scaled positions to the width and height of a plot
+      let yOffset = 0;
+      draft.plots = plots.map((plot, i) => {
+        const minmaxAxis = CalcXYMinMaxUseCase.calculate(plot);
+        const scaledPositions = CalcScaledPosUseCase.calculate(plot, minmaxAxis.x, minmaxAxis.y);
+
+        // Collect all possible values for a certain attribute, used for the autocomplete title field
+        const possibleTitleAttributeValues = graphQueryResult.nodes.reduce(
+          (values: Set<string>, node) => {
+            // Filter on nodes which are of the entity type in the plotspec
+            // Use a Set so we only collect unique values
+            if (
+              node.id.split('/')[0] == state.plotSpecifications[i].entity &&
+              state.plotSpecifications[i].labelAttributeType in node.attributes
+            )
+              values.add(node.attributes[state.plotSpecifications[i].labelAttributeType] as string);
+            return values;
+          },
+          new Set<string>(),
+        );
+
+        // Accumulate the yOffset for each plot, with the width and gapbetweenplots
+        const thisYOffset = yOffset;
+        yOffset += plot.height + state.gapBetweenPlots;
+        return {
+          title: plot.title,
+          nodes: plot.nodes.map((node, i) => {
+            return { ...node, scaledPosition: scaledPositions[i] };
+          }),
+          selectedAttributeNumerical: state.selectedAttributeNumerical,
+          scaleCalculation: state.scaleCalculation,
+          selectedAttributeCatecorigal: state.selectedAttributeCatecorigal,
+          colourCalculation: state.colourCalculation,
+          minmaxXAxis: minmaxAxis.x,
+          minmaxYAxis: minmaxAxis.y,
+          width: plot.width,
+          height: plot.height,
+          yOffset: thisYOffset,
+          possibleTitleAttributeValues: Array.from(possibleTitleAttributeValues),
+        };
+      });
+
+      // Initialize filters with no constraints
+      draft.filtersPerPlot = plotState.plots.map((plot) => {
+        return {
+          x: { min: plot.minmaxXAxis.min, max: plot.minmaxXAxis.max },
+          y: { min: plot.minmaxYAxis.min, max: plot.minmaxYAxis.max },
+        };
+      });
+
+      // Initialize the visible relations with all false values
+      if (draft.visibleRelations.length !== draft.plots.length) {
+        draft.visibleRelations = new Array(draft.plots.length)
+          .fill([])
+          .map(() => new Array(draft.plots.length).fill(false))
+      }
+
+      return draft;
+    });
+  }
+
+  /**
+   * Updates the visualisation when a checkbox is pressed.
+   * @param fromPlotIndex Which plot the connections should come from.
+   * @param toPlotIndex Which plot the connections should go to.
+   * @param value Whether the box has been checked or not.
+   */
+  function onCheckboxChanged(fromPlotIndex: number, toPlotIndex: number, value: boolean): void {
+    setPlotState((draft) => {
+      draft.visibleRelations[fromPlotIndex][toPlotIndex] = value;
+      return draft;
+    });
+  }
+
+  /** Callback for the square selection tool on the plots.
+   * Changes the filter of the plot index and applies this new filter.
+   * @param {number} plotIndex The index of the plot where there is brushed.
+   * @param {MinMaxType} xRange The min and max x values of the selection.
+   * @param {MinMaxType} yRange The min and max y values of the selection.
+   */
+  function onBrush(plotIndex: number, xRange: MinMaxType, yRange: MinMaxType) {
+    setPlotState((draft) => {
+      // Change the filter of the corresponding plot
+      draft.filtersPerPlot[plotIndex] = {
+        x: xRange,
+        y: yRange,
+      };
+
+      // Apply the new filter to the relations
+      FilterUseCase.filterRelations(
+        draft.allRelations,
+        draft.filteredRelations,
+        draft.plots,
+        draft.filtersPerPlot,
+        plotIndex,
+      );
+      return draft;
+    });
+  }
+
+  /**
+   * Prepends a new plot specifications and re-applies this to the current nodelink query result.
+   * @param {string} entity The entity filter for the new plot.
+   * @param {string} attributeName The attribute to filter on for the new plot.
+   * @param {string} attributeValue The value for the attributeto filter on for the new plot.
+   */
+  function addPlot(entity: string, attributeName: string, attributeValue: string): void {
+    setState((draft) => {
+      draft.plotSpecifications = [
+        {
+          entity: entity,
+          labelAttributeType: attributeName,
+          labelAttributeValue: attributeValue,
+          xAxis: AxisLabel.evenlySpaced, // Use default evenly spaced and # outbound connections on the x and y axis
+          yAxis: AxisLabel.outboundConnections,
+          xAxisAttributeType: '',
+          yAxisAttributeType: '',
+          width: 800, // The default width and height are 800x200
+          height: 200,
+        },
+        ...state.plotSpecifications,
+      ];
+
+      return draft;
+    });
+  }
+
+  /**
+   * Callback when the delete button of a plot is pressed.
+   * Will remove values at plotIndex from the `plotSpecifications`, `plots` , `filtersPerPlot` , `relations` , `filteredRelations` and `visibleRelations`.
+   * @param {number} plotIndex The index for the plot that needs to be deleted.
+   */
+  function onDelete(plotIndex: number): void {
+    setState((draft) => {
+      draft.plotSpecifications.splice(plotIndex, 1);
+      return draft;
+    });
+
+    setPlotState((draft) => {
+      // Recalculate the plot y offsets with this plot removed.
+      for (let i = plotIndex + 1; i < draft.plots.length; i++)
+        draft.plots[i].yOffset -= draft.plots[plotIndex].height + state.gapBetweenPlots;
+
+      draft.plots.splice(plotIndex, 1);
+      draft.filtersPerPlot.splice(plotIndex, 1);
+
+      draft.allRelations.splice(plotIndex, 1);
+      draft.allRelations = draft.allRelations.map((r) => r.filter((_, i) => i != plotIndex));
+      draft.filteredRelations.splice(plotIndex, 1);
+      draft.filteredRelations = draft.filteredRelations.map((r) => r.filter((_, i) => i != plotIndex));
+      draft.visibleRelations.splice(plotIndex, 1);
+      draft.visibleRelations = draft.visibleRelations.map((r) => r.filter((_, i) => i != plotIndex));
+
+      return draft
+    });
+  }
+
+  /**
+   * Changes the axislabel of a plot.
+   * @param {number} plotIndex The plot number for which to change an axis.
+   * @param {'x' | 'y'} axis The axis to change. Can only be 'x' or 'y'.
+   * @param {string} newLabel The axis label.
+   */
+  function onAxisLabelChanged(plotIndex: number, axis: 'x' | 'y', newLabel: string): void {
+    setState((draft) => {
+      const axisLabels: string[] = Object.values(AxisLabel);
+      if (axisLabels.includes(newLabel) && newLabel != AxisLabel.byAttribute) {
+        // If the new axis label is one of "# outbound conn", "# inbound conn" or "evenly spaced"
+        if (axis === 'x') draft.plotSpecifications[plotIndex].xAxis = newLabel as AxisLabel;
+        else if (axis === 'y') draft.plotSpecifications[plotIndex].yAxis = newLabel as AxisLabel;
+      } else {
+        // Else it is an attribute of the entity
+        if (axis === 'x') {
+          draft.plotSpecifications[plotIndex].xAxis = AxisLabel.byAttribute;
+          draft.plotSpecifications[plotIndex].xAxisAttributeType = newLabel;
+        } else if (axis === 'y') {
+          draft.plotSpecifications[plotIndex].yAxis = AxisLabel.byAttribute;
+          draft.plotSpecifications[plotIndex].yAxisAttributeType = newLabel;
         }
-        type="color"
-        id="head"
-        name="head"
-        value={theme.palette.custom.dataPointColors[0]}
-      />
-      <p>Change dataPointColor 0</p>
-      <input
-        onChange={(v) =>
-          dispatch(
-            changeDataPointColors([
-              theme.palette.custom.dataPointColors[0],
-              v.currentTarget.value,
-              ...theme.palette.custom.dataPointColors.slice(2),
-            ])
-          )
+      }
+
+      return draft;
+
+    });
+  }
+
+  /**
+   * Applies the new plot filter. Called when the user changes the plot title/filter.
+   * @param {number} plotIndex The plotindex of which the title is changed.
+   * @param {string} entity The new entity value for the plot filter, might be unchanged.
+   * @param {string} attrName The new attribute name for the plot filter, might be unchanged.
+   * @param {string} attrValue The new attribute value for the plot filter.
+   */
+  function onPlotTitleChanged(
+    plotIndex: number,
+    entity: string,
+    attrName: string,
+    attrValue: string,
+  ): void {
+    // If the entity or the attrName changed, auto select a default attrValue
+    if (
+      (entity != state.plotSpecifications[plotIndex].entity ||
+        attrName != state.plotSpecifications[plotIndex].labelAttributeType) &&
+      attrValue == state.plotSpecifications[plotIndex].labelAttributeValue
+    ) {
+      const firstValidNode = graphQueryResult.nodes.find(
+        (node) => node.id.split('/')[0] == entity && attrName in node.attributes,
+      );
+      if (firstValidNode != undefined) attrValue = firstValidNode.attributes[attrName] as string;
+    }
+
+    setState((draft) => {
+
+      draft.plotSpecifications[plotIndex] = {
+        ...draft.plotSpecifications[plotIndex],
+        entity,
+        labelAttributeType: attrName,
+        labelAttributeValue: attrValue,
+      };
+
+      return draft;
+    });
+  };
+
+
+  const plotElements: JSX.Element[] = [];
+  for (let i = 0; i < plotState.plots.length; i++) {
+    if (!state.plotSpecifications?.[i]) continue;
+
+    plotElements.push(
+      <Plot
+        key={plotState.plots[i].title + i}
+        plotData={plotState.plots[i]}
+        plotSpecification={state.plotSpecifications[i]}
+        entitiesFromSchema={entitiesFromSchema}
+        nodeColor={state.nodeColors[i]}
+        nodeRadius={state.nodeRadius}
+        width={plotState.plots[i].width}
+        height={plotState.plots[i].height}
+        selectedAttributeNumerical={plotState.plots[i].selectedAttributeNumerical}
+        selectedAttributeCatecorigal={plotState.plots[i].selectedAttributeCatecorigal}
+        scaleCalculation={plotState.plots[i].scaleCalculation}
+        colourCalculation={plotState.plots[i].colourCalculation}
+        onBrush={(xRange: MinMaxType, yRange: MinMaxType) =>
+          // Call the onBrush method with the index of this plot
+          onBrush(i, xRange, yRange)
+        }
+        onDelete={() => onDelete(i)}
+        onAxisLabelChanged={(axis: 'x' | 'y', value: string) =>
+          onAxisLabelChanged(i, axis, value)
+        }
+        onTitleChanged={(entity: string, attrName: string, attrValue: string) =>
+          onPlotTitleChanged(i, entity, attrName, attrValue)
+        }
+      />,
+    );
+  }
+
+  // Create the arrow-lines between the plots.
+  const linesBetween: JSX.Element[] = [];
+  for (let i = 0; i < plotState.filteredRelations.length; i++) {
+    for (let j = 0; j < plotState.filteredRelations[i].length; j++) {
+      let color = Color(state.nodeColors[i]);
+      color = i == j ? color.lighten(0.3) : color.darken(0.3);
+      if (plotState.visibleRelations?.[i]?.[j]) {
+        // Only create the lines if the checkbox is ticked
+        linesBetween.push(
+          <LinesBetweenPlots
+            key={plotState.plots[i].title + i + '-' + plotState.plots[j].title + j}
+            fromPlot={plotState.plots[i]}
+            toPlot={plotState.plots[j]}
+            relations={plotState.filteredRelations[i][j]}
+            color={color.hex()}
+            nodeRadius={state.nodeRadius}
+          />,
+        );
+      }
+    }
+  }
+
+  useEffect(() => { }, [state]);
+
+  const heightOfAllPlots = plotState.plots.length > 0
+    ? plotState.plots[plotState.plots.length - 1].yOffset + plotState.plots[plotState.plots.length - 1].height + 50
+    : 0;
+  return (
+    <div className={styles.container}>
+      <div style={{ width: '100%', height: '100%' }}>
+        <svg style={{ width: '100%', height: heightOfAllPlots + 60 }}>
+          <AddPlotButtonComponent
+            x={750}
+            onClick={(event: React.MouseEvent<SVGRectElement>) =>
+              setState({ ...state, addPlotButtonAnchor: event.currentTarget })
+            }
+          />
+          <g transform={'translate(60,60)'}>
+            <SVGCheckboxesWithSemanticSubstrLabel
+              plots={plotState.plots}
+              relations={plotState.allRelations}
+              visibleRelations={plotState.visibleRelations}
+              nodeColors={state.nodeColors}
+              onCheckboxChanged={(fromPlot: number, toPlot: number, value: boolean) =>
+                onCheckboxChanged(fromPlot, toPlot, value)
+              }
+            />
+            {plotElements}
+            {linesBetween},
+          </g>
+        </svg>
+      </div>
+      <AddPlotPopup
+        open={Boolean(state.addPlotButtonAnchor)}
+        anchorEl={state.addPlotButtonAnchor}
+        entitiesFromSchema={entitiesFromSchema}
+        nodeLinkResultNodes={graphQueryResult.nodes}
+        handleClose={() => setState({ ...state, addPlotButtonAnchor: null })}
+        addPlot={(entity: string, attributeName: string, attributeValue: string) =>
+          addPlot(entity, attributeName, attributeValue)
         }
-        type="color"
-        id="head"
-        name="head"
-        value={theme.palette.custom.dataPointColors[1]}
       />
-      <p>Change dataPointColor 1</p>
-      <Button variant="contained" onClick={() => dispatch(toggleDarkMode())}>
-        toggle dark mode
-      </Button>
-    </>
+      <VisConfigPanelComponent>
+        {/* <FSSConfigPanel
+          fssViewModel={semanticSubstratesViewModel.current}
+          graph={state.nodeLinkQueryResult}
+        // currentColours={currentColours}
+        /> */}
+      </VisConfigPanelComponent>
+    </div>
   );
 };
 
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..72574e2237948f516527644cf84493f134fdd873
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss
@@ -0,0 +1,63 @@
+root {
+  &:hover {
+    & .display {
+      & .background {
+        fill: '#009100';
+        transition-delay: 0s;
+      }
+      & .plus {
+        transform: translate(-4px, 0);
+        transition-delay: 0s;
+
+        & line {
+          transition: 0.2s;
+          transition-delay: 0s;
+          stroke-width: 3;
+        }
+        & .xAxis {
+          transform: translate(0, 0) scale(1, 1);
+        }
+        & .yAxis {
+          transform: translate(0, 0);
+        }
+        & .nodes {
+          opacity: 0;
+          transition-delay: 0s;
+        }
+      }
+    }
+  }
+}
+
+.background {
+  transition: 0.1s;
+  transition-delay: 0.1s;
+}
+
+.plus {
+  transform-box: fill-box;
+  transform-origin: center center;
+  transition: 0.2s;
+  transition-delay: 0.1s;
+  transform: translate(0, 0);
+
+  & line {
+    transition: 0.2s;
+    transition-delay: 0.1s;
+    stroke-width: 1;
+  }
+
+  & .nodes {
+    transition-delay: 0.1s;
+  }
+}
+
+.xAxis {
+  transform-box: fill-box;
+  transform-origin: center right;
+  transform: translate(0, 5px) scale(1.4, 1);
+}
+
+.yAxis {
+  transform: translate(-11px, 0);
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss.d.ts b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0d249f1229f4c5f24f4487d0c6c29d9e31d1b45d
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.module.scss.d.ts
@@ -0,0 +1,9 @@
+declare const classNames: {
+  readonly display: 'display';
+  readonly background: 'background';
+  readonly plus: 'plus';
+  readonly xAxis: 'xAxis';
+  readonly yAxis: 'yAxis';
+  readonly nodes: 'nodes';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d26288f8b3c6ba26972fd033f38838c32304e858
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotButtonComponent.tsx
@@ -0,0 +1,80 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React from 'react';
+import { createStyles, Theme } from '@mui/material';
+import styles from './AddPlotButtonComponent.module.scss';
+
+/**
+ * Contains a component that renders the "add plot" button with SVG elements for semantic substrates.
+ * @param props The x and y, and an on click event function.
+ */
+export default function AddPlotButtonComponent(props: {
+  x?: number;
+  y?: number;
+  onClick(event: React.MouseEvent<SVGRectElement>): void;
+}) {
+
+  return (
+    <g transform={`translate(${props.x || 0},${props.y || 0})`} className={styles.root}>
+      <g className={'display'}>
+        <rect
+          className={styles.background}
+          x="5"
+          y="1"
+          width="108"
+          height="29"
+          rx="3"
+          fill="green"
+        />
+        <path
+          d="M20.986 18.264H17.318L16.73 20H14.224L17.78 10.172H20.552L24.108 20H21.574L20.986 18.264ZM20.37 16.416L19.152 12.818L17.948 16.416H20.37ZM24.7143 16.08C24.7143 15.2773 24.8636 14.5727 25.1623 13.966C25.4703 13.3593 25.8856 12.8927 26.4083 12.566C26.9309 12.2393 27.5143 12.076 28.1583 12.076C28.6716 12.076 29.1383 12.1833 29.5583 12.398C29.9876 12.6127 30.3236 12.902 30.5663 13.266V9.64H32.9603V20H30.5663V18.88C30.3423 19.2533 30.0203 19.552 29.6003 19.776C29.1896 20 28.7089 20.112 28.1583 20.112C27.5143 20.112 26.9309 19.9487 26.4083 19.622C25.8856 19.286 25.4703 18.8147 25.1623 18.208C24.8636 17.592 24.7143 16.8827 24.7143 16.08ZM30.5663 16.094C30.5663 15.4967 30.3983 15.0253 30.0623 14.68C29.7356 14.3347 29.3343 14.162 28.8583 14.162C28.3823 14.162 27.9763 14.3347 27.6403 14.68C27.3136 15.016 27.1503 15.4827 27.1503 16.08C27.1503 16.6773 27.3136 17.1533 27.6403 17.508C27.9763 17.8533 28.3823 18.026 28.8583 18.026C29.3343 18.026 29.7356 17.8533 30.0623 17.508C30.3983 17.1627 30.5663 16.6913 30.5663 16.094ZM34.2162 16.08C34.2162 15.2773 34.3656 14.5727 34.6642 13.966C34.9722 13.3593 35.3876 12.8927 35.9102 12.566C36.4329 12.2393 37.0162 12.076 37.6602 12.076C38.1736 12.076 38.6402 12.1833 39.0602 12.398C39.4896 12.6127 39.8256 12.902 40.0682 13.266V9.64H42.4622V20H40.0682V18.88C39.8442 19.2533 39.5222 19.552 39.1022 19.776C38.6916 20 38.2109 20.112 37.6602 20.112C37.0162 20.112 36.4329 19.9487 35.9102 19.622C35.3876 19.286 34.9722 18.8147 34.6642 18.208C34.3656 17.592 34.2162 16.8827 34.2162 16.08ZM40.0682 16.094C40.0682 15.4967 39.9002 15.0253 39.5642 14.68C39.2376 14.3347 38.8362 14.162 38.3602 14.162C37.8842 14.162 37.4782 14.3347 37.1422 14.68C36.8156 15.016 36.6522 15.4827 36.6522 16.08C36.6522 16.6773 36.8156 17.1533 37.1422 17.508C37.4782 17.8533 37.8842 18.026 38.3602 18.026C38.8362 18.026 39.2376 17.8533 39.5642 17.508C39.9002 17.1627 40.0682 16.6913 40.0682 16.094ZM49.555 13.294C49.7883 12.93 50.1103 12.636 50.521 12.412C50.9316 12.188 51.4123 12.076 51.963 12.076C52.607 12.076 53.1903 12.2393 53.713 12.566C54.2356 12.8927 54.6463 13.3593 54.945 13.966C55.253 14.5727 55.407 15.2773 55.407 16.08C55.407 16.8827 55.253 17.592 54.945 18.208C54.6463 18.8147 54.2356 19.286 53.713 19.622C53.1903 19.9487 52.607 20.112 51.963 20.112C51.4216 20.112 50.941 20 50.521 19.776C50.1103 19.552 49.7883 19.2627 49.555 18.908V23.724H47.161V12.188H49.555V13.294ZM52.971 16.08C52.971 15.4827 52.803 15.016 52.467 14.68C52.1403 14.3347 51.7343 14.162 51.249 14.162C50.773 14.162 50.367 14.3347 50.031 14.68C49.7043 15.0253 49.541 15.4967 49.541 16.094C49.541 16.6913 49.7043 17.1627 50.031 17.508C50.367 17.8533 50.773 18.026 51.249 18.026C51.725 18.026 52.131 17.8533 52.467 17.508C52.803 17.1533 52.971 16.6773 52.971 16.08ZM59.0569 9.64V20H56.6629V9.64H59.0569ZM64.3478 20.112C63.5825 20.112 62.8918 19.9487 62.2758 19.622C61.6692 19.2953 61.1885 18.8287 60.8338 18.222C60.4885 17.6153 60.3158 16.906 60.3158 16.094C60.3158 15.2913 60.4932 14.5867 60.8478 13.98C61.2025 13.364 61.6878 12.8927 62.3038 12.566C62.9198 12.2393 63.6105 12.076 64.3758 12.076C65.1412 12.076 65.8318 12.2393 66.4478 12.566C67.0638 12.8927 67.5492 13.364 67.9038 13.98C68.2585 14.5867 68.4358 15.2913 68.4358 16.094C68.4358 16.8967 68.2538 17.606 67.8898 18.222C67.5352 18.8287 67.0452 19.2953 66.4198 19.622C65.8038 19.9487 65.1132 20.112 64.3478 20.112ZM64.3478 18.04C64.8052 18.04 65.1925 17.872 65.5098 17.536C65.8365 17.2 65.9998 16.7193 65.9998 16.094C65.9998 15.4687 65.8412 14.988 65.5238 14.652C65.2158 14.316 64.8332 14.148 64.3758 14.148C63.9092 14.148 63.5218 14.316 63.2138 14.652C62.9058 14.9787 62.7518 15.4593 62.7518 16.094C62.7518 16.7193 62.9012 17.2 63.1998 17.536C63.5078 17.872 63.8905 18.04 64.3478 18.04ZM74.0599 17.97V20H72.8419C71.9739 20 71.2972 19.79 70.8119 19.37C70.3266 18.9407 70.0839 18.2453 70.0839 17.284V14.176H69.1319V12.188H70.0839V10.284H72.4779V12.188H74.0459V14.176H72.4779V17.312C72.4779 17.5453 72.5339 17.7133 72.6459 17.816C72.7579 17.9187 72.9446 17.97 73.2059 17.97H74.0599Z"
+          fill="white"
+        />
+        <g className={styles.plus}>
+          <line
+            className={styles.xAxis}
+            x1="103.987"
+            y1="15.6278"
+            x2="88.9872"
+            y2="15.5"
+            stroke="white"
+            strokeWidth="3"
+          />
+          <line
+            className={styles.yAxis}
+            x1="96.5"
+            y1="8"
+            x2="96.5"
+            y2="23"
+            stroke="white"
+            strokeWidth="3"
+          />
+          <g className={'nodes'}>
+            <circle cx="89" cy="16" r="1" fill="white" />
+            <circle cx="94" cy="14" r="1" fill="white" />
+            <circle cx="99" cy="14" r="1" fill="white" />
+          </g>
+        </g>
+      </g>
+      <g>
+        <rect
+          x="5"
+          y="1"
+          width="108"
+          height="29"
+          rx="3"
+          fill="transparent"
+          onClick={(e) => props.onClick(e)}
+        />
+      </g>
+    </g>
+  );
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f53468bc4fe77f355f9d5cbb3dcee2ebf4cdff93
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx
@@ -0,0 +1,216 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import { Link, Node } from '../../shared/ResultNodeLinkParserUseCase';
+import { Button, MenuItem, Popover, TextField } from '@mui/material';
+import React, { ReactElement } from 'react';
+import { EntitiesFromSchema } from '../Types';
+import OptimizedAutocomplete from './OptimizedAutocomplete';
+
+/** The typing for the props of the plot popups */
+type AddPlotPopupProps = {
+  anchorEl: Element | null;
+  open: boolean;
+  entitiesFromSchema: EntitiesFromSchema;
+  nodeLinkResultNodes: Node[];
+
+  // Called when the the popup should close.
+  handleClose(): void;
+  // Called when the has filled in the required field and pressed the "add" button.
+  addPlot(entity: string, attributeName: string, attributeValue: string): void;
+};
+
+/** The variables in the state of the plot popups */
+type AddPlotPopupState = {
+  entity: string;
+  attributeName: string;
+  isButtonEnabled: boolean;
+};
+
+/** React component that renders a popup with input fields for adding a plot. */
+export default class AddPlotPopup extends React.Component<AddPlotPopupProps, AddPlotPopupState> {
+  classes: any;
+  possibleAttrValues: string[] = [];
+  attributeNameOptions: string[] = [];
+  attributeValue = '?';
+
+  constructor(props: AddPlotPopupProps) {
+    super(props);
+
+    this.state = {
+      entity: '',
+      attributeName: '',
+      isButtonEnabled: false,
+    };
+  }
+
+  /**
+   * Called when the entity field is changed.
+   * Sets the `attributeNameOptions`, resets the `attributeValue` and sets the state.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   */
+  private entityChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const newEntity = event.target.value;
+
+    this.attributeNameOptions = [];
+    if (this.props.entitiesFromSchema.attributesPerEntity[newEntity])
+      this.attributeNameOptions =
+        this.props.entitiesFromSchema.attributesPerEntity[newEntity].textAttributeNames;
+
+    this.attributeValue = '';
+    this.setState({ ...this.state, entity: newEntity, attributeName: '', isButtonEnabled: false });
+  };
+
+  /**
+   * Called when the attribute name field is changed.
+   * Sets the possible attribute values, resets the `attributeValue`, enables the "Add" button if all fields valid and sets the state.
+   * @param {React.ChangeEvent<HTMLInputElement>} event The event that is given by the input field when a change event is fired.
+   */
+  private attrNameChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const newAttrName = event.target.value;
+
+    this.possibleAttrValues = Array.from(
+      this.props.nodeLinkResultNodes.reduce((values: Set<string>, node) => {
+        if (this.state.entity == node.id.split('/')[0] && newAttrName in node.attributes)
+          values.add(node.attributes[newAttrName]);
+        return values;
+      }, new Set<string>()),
+    );
+
+    // Reset attribute value.
+    this.attributeValue = '';
+    // Filter the possible attribute values from the entity attributes from the schema.
+    const isButtonDisabled =
+      this.props.entitiesFromSchema.entityNames.includes(this.state.entity) &&
+      this.props.entitiesFromSchema.attributesPerEntity[
+        this.state.entity
+      ].textAttributeNames.includes(newAttrName);
+    this.setState({
+      ...this.state,
+      attributeName: newAttrName,
+      isButtonEnabled: isButtonDisabled,
+    });
+  };
+
+  /**
+   * Called when the user clicks the "Add" button.
+   * Checks if all fields are valid before calling `handleClose()` and `addPlot()`.
+   */
+  private addPlotButtonClicked = () => {
+    if (
+      this.props.entitiesFromSchema.entityNames.includes(this.state.entity) &&
+      this.props.entitiesFromSchema.attributesPerEntity[
+        this.state.entity
+      ].textAttributeNames.includes(this.state.attributeName)
+    ) {
+      this.props.handleClose();
+      this.props.addPlot(
+        this.state.entity,
+        this.state.attributeName,
+        this.attributeValue == '' ? '?' : this.attributeValue,
+      );
+    } else
+      this.setState({
+        ...this.state,
+        isButtonEnabled: false,
+      });
+  };
+
+  render(): ReactElement {
+    const { open, anchorEl, entitiesFromSchema, handleClose } = this.props;
+
+    // Retrieve the possible entity options. If none available, set helper message.
+    let entityMenuItems: ReactElement[];
+    if (this.props.entitiesFromSchema.entityNames.length > 0)
+      entityMenuItems = entitiesFromSchema.entityNames.map((entity) => (
+        <MenuItem key={entity} value={entity}>
+          {entity}
+        </MenuItem>
+      ));
+    else
+      entityMenuItems = [
+        <MenuItem key="placeholder" value="" disabled>
+          No schema data available
+        </MenuItem>,
+      ];
+
+    // Retrieve the possible attributeName options. If none available, set helper message.
+    let attributeNameMenuItems: ReactElement[];
+    if (this.attributeNameOptions.length > 0)
+      attributeNameMenuItems = this.attributeNameOptions.map((attribute) => (
+        <MenuItem key={attribute} value={attribute}>
+          {attribute}
+        </MenuItem>
+      ));
+    else
+      attributeNameMenuItems = [
+        <MenuItem key="placeholder" value="" disabled>
+          First select an entity
+        </MenuItem>,
+      ];
+
+    return (
+      <div>
+        <Popover
+          id={'simple-addplot-popover'}
+          open={open}
+          anchorEl={anchorEl}
+          onClose={handleClose}
+          anchorOrigin={{
+            vertical: 'top',
+            horizontal: 'center',
+          }}
+          transformOrigin={{
+            vertical: 'top',
+            horizontal: 'center',
+          }}
+        >
+          <div style={{ padding: 20, paddingTop: 10, display: 'flex' }}>
+            <TextField
+              select
+              id="standard-select-entity"
+              style={{ minWidth: 120 }}
+              label="Entity"
+              value={this.state.entity}
+              onChange={this.entityChanged}
+            >
+              {entityMenuItems}
+            </TextField>
+            <TextField
+              select
+              id="standard-select-attribute"
+              style={{ minWidth: 120, marginLeft: 20, marginRight: 20 }}
+              label="Attribute"
+              value={this.state.attributeName}
+              onChange={this.attrNameChanged}
+            >
+              {attributeNameMenuItems}
+            </TextField>
+            <OptimizedAutocomplete
+              currentValue={this.attributeValue}
+              options={this.possibleAttrValues}
+              onChange={(v) => (this.attributeValue = v)}
+              useMaterialStyle={{ label: 'Value', helperText: '' }}
+            />
+            <div style={{ height: 40, paddingTop: 10, marginLeft: 30 }}>
+              <Button
+                variant="contained"
+                disabled={!this.state.isButtonEnabled}
+                onClick={this.addPlotButtonClicked}
+              >
+                <span style={{ fontWeight: 'bold' }}>Add</span>
+              </Button>
+            </div>
+          </div>
+        </Popover>
+      </div>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/BrushComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/BrushComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..17b1796406b5b0806a88bae2feb3bbf15accac86
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/BrushComponent.tsx
@@ -0,0 +1,246 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { ReactElement } from 'react';
+import {
+  AxisLabel,
+  MinMaxType,
+} from '../Types';
+import { ScaleLinear, brush, scaleLinear, select } from 'd3';
+
+/** The variables in the props of the brush component*/
+type BrushProps = {
+  width: number;
+  height: number;
+
+  xAxisDomain: MinMaxType;
+  yAxisDomain: MinMaxType;
+
+  xAxisLabel: AxisLabel;
+  yAxisLabel: AxisLabel;
+
+  onBrush(xRange: MinMaxType, yRange: MinMaxType): void;
+};
+
+/** The variables in the state of the brush component*/
+type BrushState = {
+  xBrushRange: MinMaxType;
+  yBrushRange: MinMaxType;
+  brushActive: boolean;
+};
+
+/**
+ * Takes care of the brush (square selection) functionality for a plot.
+ * Also renders the x y selections ticks with their values.
+ */
+export default class BrushComponent extends React.Component<BrushProps, BrushState> {
+  ref = React.createRef<SVGGElement>();
+
+  xAxisScale: ScaleLinear<number, number, never> = scaleLinear();
+  yAxisScale: ScaleLinear<number, number, never> = scaleLinear();
+
+  constructor(props: BrushProps) {
+    super(props);
+    this.state = {
+      xBrushRange: { min: 0, max: this.props.width },
+      yBrushRange: { min: 0, max: this.props.height },
+      brushActive: false,
+    };
+
+    this.updateXYAxisScaleFunctions();
+  }
+
+  public componentDidMount(): void {
+    const d3Selected = select(this.ref.current);
+
+    // Add square selection tool to this plot with the width and height. Also called brush.
+    d3Selected.append('g').call(
+      brush()
+        .extent([
+          [0, 0],
+          [this.props.width, this.props.height],
+        ])
+        .on('brush', (e) => {
+          this.onBrush(e.selection);
+        })
+        .on('end', (e) => {
+          this.onBrushEnd(e.selection);
+        }),
+    );
+  }
+
+  public componentDidUpdate(prevProps: BrushProps): void {
+    if (
+      this.props.xAxisDomain != prevProps.xAxisDomain ||
+      this.props.yAxisDomain != prevProps.yAxisDomain
+    ) {
+      this.updateXYAxisScaleFunctions();
+    }
+  }
+
+  /** Updates the X Y axis scale for the plots */
+  private updateXYAxisScaleFunctions(): void {
+    // Create the x axis scale funtion with d3
+    const xAxisDomain = [this.props.xAxisDomain.min, this.props.xAxisDomain.max];
+    this.xAxisScale = scaleLinear().domain(xAxisDomain).range([0, this.props.width]);
+
+    // Create the y axis scale funtion with d3
+    const yAxisDomain = [this.props.yAxisDomain.max, this.props.yAxisDomain.min];
+    this.yAxisScale = scaleLinear().domain(yAxisDomain).range([0, this.props.height]);
+  }
+
+  /**
+   * Called when brushing. Brush will be displayed and the props callback gets called.
+   * @param {number[][]} selection A 2D array where the first row is the topleft coordinates and the second bottomright.
+   */
+  private onBrush(selection: number[][] | undefined): void {
+    if (selection != undefined) {
+      // Set the state with the inverted selection, this will give us the original positions, (the values)
+      const newState: BrushState = {
+        xBrushRange: {
+          min: this.xAxisScale.invert(selection[0][0]),
+          max: this.xAxisScale.invert(selection[1][0]),
+        },
+        yBrushRange: {
+          min: this.yAxisScale.invert(selection[1][1]),
+          max: this.yAxisScale.invert(selection[0][1]),
+        },
+        brushActive: true,
+      };
+      this.setState(newState);
+
+      // Add event when brushing, call the onBrush with start and end positions
+      this.props.onBrush(newState.xBrushRange, newState.yBrushRange);
+    }
+  }
+
+  /**
+   * Called when brush ends. If the selection was empty reset selection to cover whole plot and notify viewmodel.
+   * @param {number[][]} selection A 2D array where the first row is the topleft coordinates and the second bottomright.
+   */
+  private onBrushEnd(selection: number[][] | undefined): void {
+    if (selection == undefined) {
+      // Set the state with the inverted selection, this will give us the original positions, (the values)
+      const newState: BrushState = {
+        xBrushRange: {
+          min: this.xAxisScale.invert(0),
+          max: this.xAxisScale.invert(this.props.width),
+        },
+        yBrushRange: {
+          min: this.yAxisScale.invert(this.props.height),
+          max: this.yAxisScale.invert(0),
+        },
+        brushActive: false,
+      };
+      this.setState(newState);
+
+      // Add event when brushing, call the onBrush with start and end positions
+      this.props.onBrush(newState.xBrushRange, newState.yBrushRange);
+    }
+  }
+
+  /**
+   * Renders the X axis brush ticks depending on the brushActive state.
+   * @param xRangePositions The x positions of the brush ticks.
+   */
+  private renderXAxisBrushTicks(xRangePositions: MinMaxType): ReactElement {
+    if (this.state.brushActive && this.props.xAxisLabel != AxisLabel.evenlySpaced) {
+      return (
+        <g>
+          <line
+            x1={xRangePositions.min}
+            y1={this.props.height - 5}
+            x2={xRangePositions.min}
+            y2={this.props.height}
+            stroke={'red'}
+            strokeWidth={1}
+            fill={'transparent'}
+          />
+          <text x={xRangePositions.min} y={this.props.height - 8} textAnchor="middle" fontSize={10}>
+            {+this.state.xBrushRange.min.toFixed(2)}
+          </text>
+          <line
+            x1={xRangePositions.max}
+            y1={this.props.height - 5}
+            x2={xRangePositions.max}
+            y2={this.props.height}
+            stroke={'red'}
+            strokeWidth={1}
+            fill={'transparent'}
+          />
+          <text x={xRangePositions.max} y={this.props.height - 8} textAnchor="middle" fontSize={10}>
+            {+this.state.xBrushRange.max.toFixed(2)}
+          </text>
+        </g>
+      );
+    }
+
+    return <></>;
+  }
+
+  /**
+   * Renders the Y axis brush ticks depending on the brushActive state.
+   * @param yRangePositions The y positions of the brush ticks.
+   */
+  private renderYAxisBrushTicks(yRangePositions: MinMaxType): ReactElement {
+    if (this.state.brushActive && this.props.yAxisLabel != AxisLabel.evenlySpaced) {
+      return (
+        <g>
+          <line
+            x1={0}
+            y1={yRangePositions.min}
+            x2={5}
+            y2={yRangePositions.min}
+            stroke={'red'}
+            strokeWidth={1}
+            fill={'transparent'}
+          />
+          <text x={8} y={yRangePositions.min + 3} fontSize={10}>
+            {+this.state.yBrushRange.min.toFixed(2)}
+          </text>
+          <line
+            x1={0}
+            y1={yRangePositions.max}
+            x2={5}
+            y2={yRangePositions.max}
+            stroke={'red'}
+            strokeWidth={1}
+            fill={'transparent'}
+          />
+          <text x={8} y={yRangePositions.max + 3} fontSize={10}>
+            {+this.state.yBrushRange.max.toFixed(2)}
+          </text>
+        </g>
+      );
+    }
+
+    return <></>;
+  }
+
+  public render(): ReactElement {
+    // Scale the brush to the scaled range
+    // Used for rendering the positions of the x and y axis brush ticks
+    const xRangePositions: MinMaxType = {
+      min: this.xAxisScale(this.state.xBrushRange.min),
+      max: this.xAxisScale(this.state.xBrushRange.max),
+    };
+    const yRangePositions: MinMaxType = {
+      min: this.yAxisScale(this.state.yBrushRange.min),
+      max: this.yAxisScale(this.state.yBrushRange.max),
+    };
+
+    return (
+      <g ref={this.ref}>
+        {this.renderXAxisBrushTicks(xRangePositions)}
+        {this.renderYAxisBrushTicks(yRangePositions)}
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/LinesBetweenPlotsComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/LinesBetweenPlotsComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..91adb4afcfeda10fb2487d9e056a28d466d84f9a
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/LinesBetweenPlotsComponent.tsx
@@ -0,0 +1,113 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { ReactElement } from 'react';
+import {
+  NodeType,
+  PlotType,
+  RelationType,
+} from '../Types';
+import { XYPosition } from 'reactflow';
+import CalcConnectionLinePositionsUseCase from '../utils/CalcConnectionLinePositionsUseCase';
+
+/** Props of the lines between the plots component */
+type LinesBetweenPlotsProps = {
+  fromPlot: PlotType;
+  toPlot: PlotType;
+  relations: RelationType[];
+  nodeRadius: number;
+  color: string;
+};
+/** Renders all the connection lines between nodes from two plots. */
+export default class LinesBetweenPlots extends React.Component<LinesBetweenPlotsProps> {
+  render(): JSX.Element {
+    const { fromPlot, toPlot, relations, nodeRadius, color } = this.props;
+
+    // The JSX elements to render a connection line with arrow
+    const lines = relations.map((relation) => (
+      <LineBetweenNodesComponent
+        key={
+          fromPlot.nodes[relation.fromIndex].data.text + toPlot.nodes[relation.toIndex].data.text
+        }
+        fromNode={fromPlot.nodes[relation.fromIndex]}
+        fromPlotYOffset={fromPlot.yOffset}
+        toNode={toPlot.nodes[relation.toIndex]}
+        toPlotYOffset={toPlot.yOffset}
+        width={relation.value ?? 1}
+        nodeRadius={nodeRadius}
+        color={relation.colour ?? color}
+      />
+    ));
+
+    return <g>{lines}</g>;
+  }
+}
+
+/** Props of the nodes between the plots component. */
+type LineBetweenNodesProps = {
+  fromNode: NodeType;
+  fromPlotYOffset: number;
+
+  toNode: NodeType;
+  toPlotYOffset: number;
+
+  width: number;
+  color: string;
+  nodeRadius: number;
+};
+/** React component for drawing a single connectionline between nodes for semantic substrates. */
+class LineBetweenNodesComponent extends React.Component<LineBetweenNodesProps> {
+  render(): ReactElement {
+    const { fromNode, toNode, fromPlotYOffset, toPlotYOffset, width, color, nodeRadius } =
+      this.props;
+
+    // The start and end position with their plot offset
+    const startNodePos: XYPosition = {
+      x: fromNode.scaledPosition.x,
+      y: fromNode.scaledPosition.y + fromPlotYOffset,
+    };
+
+    const endNodePos: XYPosition = {
+      x: toNode.scaledPosition.x,
+      y: toNode.scaledPosition.y + toPlotYOffset,
+    };
+
+    // Get the positions to draw the arrow
+    const { start, end, controlPoint, arrowRStart, arrowLStart } =
+      CalcConnectionLinePositionsUseCase.calculatePositions(startNodePos, endNodePos, nodeRadius);
+
+    // Create the curved line path
+    const path = `M ${start.x} ${start.y} Q ${controlPoint.x} ${controlPoint.y} ${end.x} ${end.y}`;
+
+    return (
+      <g pointerEvents={'none'}>
+        <path d={path} stroke={color} strokeWidth={width} fill="transparent" />
+        <line
+          x1={arrowRStart.x}
+          y1={arrowRStart.y}
+          x2={end.x}
+          y2={end.y}
+          stroke={color}
+          strokeWidth={width}
+          fill="transparent"
+        />
+        <line
+          x1={arrowLStart.x}
+          y1={arrowLStart.y}
+          x2={end.x}
+          y2={end.y}
+          stroke={color}
+          strokeWidth={width}
+          fill="transparent"
+        />
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8b0df8b37c0235624b49309d1b533ee75d4c0dbc
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss
@@ -0,0 +1,7 @@
+.listbox {
+  box-sizing: border-box;
+  & ul {
+    padding: 0;
+    margin: 0;
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss.d.ts b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83092e29c47d14efbdaef7a929a911335b0d2925
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.module.scss.d.ts
@@ -0,0 +1,4 @@
+declare const classNames: {
+  readonly listbox: 'listbox';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dba94dfc57efe5c15a650547a7e32132bba7ad46
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx
@@ -0,0 +1,154 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import { Autocomplete, AutocompleteRenderGroupParams, AutocompleteRenderInputParams, ListSubheader, TextField, Typography, makeStyles, useMediaQuery, useTheme } from '@mui/material';
+import React from 'react';
+import { VariableSizeList, ListChildComponentProps } from 'react-window';
+import styles from './OptimizedAutocomplete.module.scss';
+
+const LISTBOX_PADDING = 8; // px
+
+function renderRow(props: ListChildComponentProps) {
+  const { data, index, style } = props;
+  return React.cloneElement(data[index], {
+    style: {
+      ...style,
+      top: (style.top as number) + LISTBOX_PADDING,
+    },
+  });
+}
+
+const OuterElementContext = React.createContext({});
+
+// eslint-disable-next-line react/display-name
+const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => {
+  const outerProps = React.useContext(OuterElementContext);
+  return <div ref={ref} {...props} {...outerProps} />;
+});
+
+function useResetCache(data: any) {
+  const ref = React.useRef<VariableSizeList>(null);
+  React.useEffect(() => {
+    if (ref.current != null) {
+      ref.current.resetAfterIndex(0, true);
+    }
+  }, [data]);
+  return ref;
+}
+
+// Adapter for react-window
+const ListboxComponent = React.forwardRef<HTMLDivElement>(function ListboxComponent(props, ref) {
+  const { children, ...other } = props;
+  const itemData = React.Children.toArray(children);
+  const theme = useTheme();
+  const smUp = useMediaQuery(theme.breakpoints.up('sm'), { noSsr: true });
+  const itemCount = itemData.length;
+  const itemSize = smUp ? 36 : 48;
+
+  const getChildSize = (child: React.ReactNode) => {
+    if (React.isValidElement(child) && child.type === ListSubheader) {
+      return 48;
+    }
+
+    return itemSize;
+  };
+
+  const getHeight = () => {
+    if (itemCount > 8) {
+      return 8 * itemSize;
+    }
+    return itemData.map(getChildSize).reduce((a, b) => a + b, 0);
+  };
+
+  const gridRef = useResetCache(itemCount);
+
+  return (
+    <div ref={ref}>
+      <OuterElementContext.Provider value={other}>
+        <VariableSizeList
+          itemData={itemData}
+          height={getHeight() + 2 * LISTBOX_PADDING}
+          width="100%"
+          ref={gridRef}
+          outerElementType={OuterElementType}
+          innerElementType="ul"
+          itemSize={(index: number) => getChildSize(itemData[index])}
+          overscanCount={5}
+          itemCount={itemCount}
+        >
+          {renderRow}
+        </VariableSizeList>
+      </OuterElementContext.Provider>
+    </div>
+  );
+});
+
+const renderGroup = (params: AutocompleteRenderGroupParams) => [
+  <ListSubheader key={params.key} component="div">
+    {params.group}
+  </ListSubheader>,
+  params.children,
+];
+
+type OptimizedAutocomplete = {
+  currentValue: string;
+  options: string[];
+  useMaterialStyle?: { label: string; helperText: string };
+  /** Called when the value of the input field changes. */
+  onChange?(value: string): void;
+  /** Called when the user leaves focus of the input field. */
+  onLeave?(value: string): void;
+};
+/** Renders the autocomplete input field with the given props. */
+export default function OptimizedAutocomplete(props: OptimizedAutocomplete) {
+  let newValue = props.currentValue;
+
+  return (
+    <Autocomplete
+      id="optimized-autocomplete"
+      style={{ width: 200 }}
+      disableListWrap
+      autoHighlight
+      className={styles.listbox}
+      ListboxComponent={ListboxComponent as React.ComponentType<React.HTMLAttributes<HTMLElement>>}
+      renderGroup={renderGroup}
+      options={props.options}
+      // groupBy={(option: string) => option[0].toUpperCase()}
+      renderInput={(params: AutocompleteRenderInputParams) =>
+        props.useMaterialStyle != undefined ? (
+          <TextField
+            {...params}
+            variant="standard"
+            // size="small"
+            label={props.useMaterialStyle.label}
+            helperText={props.useMaterialStyle.helperText}
+          />
+        ) : (
+          <div ref={params.InputProps.ref}>
+            <input style={{ width: 200 }} type="text" {...params.inputProps} autoFocus />
+          </div>
+        )
+      }
+      // defaultValue={props.currentValue}
+      placeholder={props.currentValue}
+      renderOption={(props, option, state) => <Typography noWrap>{option}</Typography>}
+      onChange={(_: React.ChangeEvent<any>, value: string | null) => {
+        newValue = value || '?';
+        if (props.onChange) props.onChange(newValue);
+      }}
+      onBlur={() => {
+        if (props.onLeave) props.onLeave(newValue);
+      }}
+      onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>): void => {
+        if (e.key == 'Enter' && props.onLeave) props.onLeave(newValue);
+      }}
+    />
+  );
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..5ee3cdfe965cb2d8004be81b7a1c031ce00efb97
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css
@@ -0,0 +1,26 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+/* Contains styling for the plot axis labels. */
+.xLabelText,
+.yLabelText {
+  text-anchor: end;
+  cursor: pointer;
+}
+
+.xLabelText:hover {
+  /* filter: drop-shadow(1px 1px 0.4px #80808078); */
+  text-decoration: underline;
+}
+.yLabelText:hover {
+  /* filter: drop-shadow(1px 1px 0.4px #80808000); */
+  text-decoration: underline;
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css.d.ts b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8dcb2518e41cca01863ef88ab026d9dea6f051c1
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelStyles.module.css.d.ts
@@ -0,0 +1,5 @@
+declare const classNames: {
+  readonly xLabelText: 'xLabelText';
+  readonly yLabelText: 'yLabelText';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelsComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelsComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9776b45d19f83931763cf0895b6645698667550d
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotAxisLabelsComponent.tsx
@@ -0,0 +1,116 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { ReactElement } from 'react';
+import { Menu, MenuItem } from '@mui/material';
+import styles from './PlotAxisLabelStyles.module.css';
+
+/** Props of the axis labels component. */
+type PlotAxisLabelsProps = {
+  xAxisLabel: string;
+  yAxisLabel: string;
+  axisLabelOptions: string[];
+
+  width: number;
+  height: number;
+
+  // Callback when an axis label changed
+  onAxisLabelChanged(axis: 'x' | 'y', value: string): void;
+};
+type PlotAxisLabelsState = {
+  menuAnchor: Element | null;
+  menuOpenForAxis: 'x' | 'y';
+};
+/** Component for rendering the axis labels of an semantic substrates plot.
+ * With functionality to edit the labels.
+ */
+export default class PlotAxisLabelsComponent extends React.Component<
+  PlotAxisLabelsProps,
+  PlotAxisLabelsState
+> {
+  constructor(props: PlotAxisLabelsProps) {
+    super(props);
+    this.state = { menuAnchor: null, menuOpenForAxis: 'x' };
+  }
+
+  render(): ReactElement {
+    const { width, height, xAxisLabel, yAxisLabel, axisLabelOptions } = this.props;
+
+    return (
+      <g>
+        <text
+          className={styles.xLabelText}
+          x={width - 10}
+          y={height + 35}
+          onClick={(event: React.MouseEvent<SVGTextElement>) => {
+            this.setState({
+              menuAnchor: event.currentTarget,
+              menuOpenForAxis: 'x',
+            });
+          }}
+        >
+          {xAxisLabel}
+        </text>
+        <text
+          className={styles.yLabelText}
+          x={-10}
+          y={-50}
+          transform={'rotate(-90)'}
+          onClick={(event: React.MouseEvent<SVGTextElement>) => {
+            this.setState({
+              menuAnchor: event.currentTarget,
+              menuOpenForAxis: 'y',
+            });
+          }}
+        >
+          {yAxisLabel}
+        </text>
+        <Menu
+          id="simple-menu"
+          anchorEl={this.state.menuAnchor}
+          keepMounted
+          // getContentAnchorEl={null}
+          anchorOrigin={
+            this.state.menuOpenForAxis == 'x'
+              ? { vertical: 'bottom', horizontal: 'center' }
+              : { vertical: 'center', horizontal: 'right' }
+          }
+          transformOrigin={
+            this.state.menuOpenForAxis == 'x'
+              ? { vertical: 'top', horizontal: 'center' }
+              : { vertical: 'center', horizontal: 'left' }
+          }
+          open={Boolean(this.state.menuAnchor)}
+          onClose={() => this.setState({ menuAnchor: null })}
+          transitionDuration={150}
+          PaperProps={{
+            style: {
+              maxHeight: 48 * 4.5, // 48 ITEM_HEIGHT
+            },
+          }}
+        >
+          {axisLabelOptions.map((option) => (
+            <MenuItem
+              key={option}
+              selected={option == (this.state.menuOpenForAxis == 'x' ? xAxisLabel : yAxisLabel)}
+              onClick={() => {
+                this.setState({ menuAnchor: null });
+
+                this.props.onAxisLabelChanged(this.state.menuOpenForAxis, option);
+              }}
+            >
+              {option}
+            </MenuItem>
+          ))}
+        </Menu>
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..183fcbcc3016eaa24c13f49586062c984d38dcd4
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotComponent.tsx
@@ -0,0 +1,223 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import React, { ReactElement } from 'react';
+import {
+  EntitiesFromSchema,
+  MinMaxType,
+  PlotType,
+  AxisLabel,
+  PlotSpecifications,
+  NodeType,
+} from '../Types';
+import BrushComponent from './BrushComponent';
+import PlotTitleComponent from './PlotTitleComponent';
+import PlotAxisLabelsComponent from './PlotAxisLabelsComponent';
+import { axisBottom, axisLeft, ScaleLinear, scaleLinear, select, Selection } from 'd3';
+
+/** Props of the plots component */
+type PlotProps = {
+  plotData: PlotType;
+  plotSpecification: PlotSpecifications;
+  entitiesFromSchema: EntitiesFromSchema;
+  nodeColor: string;
+  nodeRadius: number;
+
+  selectedAttributeNumerical: string;
+  selectedAttributeCatecorigal: string;
+  scaleCalculation: (x: number) => number;
+  colourCalculation: (x: string) => string;
+
+  width: number;
+  height: number;
+
+  onBrush(xRange: MinMaxType, yRange: MinMaxType): void;
+  onDelete(): void;
+  onAxisLabelChanged(axis: 'x' | 'y', value: string): void;
+
+  onTitleChanged(entity: string, attrName: string, attrValue: string): void;
+};
+
+/** State for the plots component */
+type PlotState = {
+  menuAnchor: Element | null;
+};
+
+/** React component to render a plot with axis and square selection tool. */
+export default class Plot extends React.Component<PlotProps, PlotState> {
+  ref = React.createRef<SVGGElement>();
+
+  xAxisScale: ScaleLinear<number, number, never> = scaleLinear();
+  yAxisScale: ScaleLinear<number, number, never> = scaleLinear();
+
+  d3XAxis: Selection<SVGGElement, unknown, null, undefined> | undefined;
+  d3YAxis: Selection<SVGGElement, unknown, null, undefined> | undefined;
+
+  constructor(props: PlotProps) {
+    super(props);
+    this.state = {
+      menuAnchor: null,
+    };
+
+    this.updateXYAxisScaleFunctions();
+  }
+
+  public componentDidMount(): void {
+    const d3Selected = select(this.ref.current);
+
+    // Add x axis ticks
+    this.d3XAxis = d3Selected
+      .append('g')
+      .attr('transform', 'translate(0,' + this.props.height + ')')
+      .call(axisBottom(this.xAxisScale));
+
+    // Add y axis ticks
+    this.d3YAxis = d3Selected.append('g').call(axisLeft(this.yAxisScale));
+  }
+
+  public componentDidUpdate(prevProps: PlotProps): void {
+    if (
+      this.props.plotData.minmaxXAxis != prevProps.plotData.minmaxXAxis ||
+      this.props.plotData.minmaxYAxis != prevProps.plotData.minmaxYAxis
+    ) {
+      this.updateXYAxisScaleFunctions();
+
+      // Update x axis ticks
+      if (this.d3XAxis) {
+        this.d3XAxis.transition().duration(1000).call(axisBottom(this.xAxisScale));
+      }
+
+      // Update y axis ticks
+      if (this.d3YAxis) {
+        this.d3YAxis.transition().duration(1000).call(axisLeft(this.yAxisScale));
+      }
+    }
+  }
+
+  /** Update the x and y axis scale functions with the new axis domains. */
+  private updateXYAxisScaleFunctions(): void {
+    // Create the x axis scale funtion with d3
+    if (this.props.plotSpecification.xAxis == AxisLabel.evenlySpaced)
+      this.xAxisScale = scaleLinear().domain([]).range([this.props.width, 0]);
+    else
+      this.xAxisScale = scaleLinear()
+        .domain([this.props.plotData.minmaxXAxis.max, this.props.plotData.minmaxXAxis.min])
+        .range([this.props.width, 0]);
+
+    // Create the y axis scale funtion with d3
+    if (this.props.plotSpecification.yAxis == AxisLabel.evenlySpaced)
+      this.yAxisScale = scaleLinear().domain([]).range([0, this.props.height]);
+    else
+      this.yAxisScale = scaleLinear()
+        .domain([this.props.plotData.minmaxYAxis.max, this.props.plotData.minmaxYAxis.min])
+        .range([0, this.props.height]);
+  }
+
+  public render(): ReactElement {
+    const {
+      width,
+      height,
+      // yOffset,
+      plotData,
+      nodeColor,
+      nodeRadius,
+      entitiesFromSchema,
+      plotSpecification,
+      selectedAttributeNumerical,
+      selectedAttributeCatecorigal,
+      scaleCalculation,
+      colourCalculation,
+      onDelete,
+      onBrush,
+      onAxisLabelChanged,
+      onTitleChanged,
+    } = this.props;
+
+    const circles = plotData.nodes.map((node) => (
+      <circle
+        key={node.data.text}
+        cx={node.scaledPosition.x}
+        cy={node.scaledPosition.y}
+        r={Math.abs(IfAttributeIsNumber(nodeRadius, node))}
+        fill={colourCalculation(node.attributes[selectedAttributeCatecorigal])}
+      />
+    ));
+
+    function IfAttributeIsNumber(nodeRadius: number, node: NodeType): number {
+      let possibleRadius = Number(node.attributes[selectedAttributeNumerical]);
+      if (!isNaN(possibleRadius)) {
+        return scaleCalculation(possibleRadius);
+      }
+      return nodeRadius;
+    }
+
+    // Determine the x y axis label, if it's byAttribute, give it the attribute type.
+    let xAxisLabel: string;
+    if (plotSpecification.xAxis == AxisLabel.byAttribute)
+      xAxisLabel = plotSpecification.xAxisAttributeType;
+    else xAxisLabel = plotSpecification.xAxis;
+    let yAxisLabel: string;
+    if (plotSpecification.yAxis == AxisLabel.byAttribute)
+      yAxisLabel = plotSpecification.yAxisAttributeType;
+    else yAxisLabel = plotSpecification.yAxis;
+
+    const axisLabelOptions = [
+      ...entitiesFromSchema.attributesPerEntity[plotSpecification.entity].numberAttributeNames,
+      AxisLabel.inboundConnections,
+      AxisLabel.outboundConnections,
+      AxisLabel.evenlySpaced,
+    ];
+
+    return (
+      <g ref={this.ref} transform={'translate(0,' + plotData.yOffset + ')'}>
+        <PlotTitleComponent
+          pos={{ x: 0, y: -5 }}
+          nodeColor={nodeColor}
+          entity={plotSpecification.entity}
+          attributeName={plotSpecification.labelAttributeType}
+          attributeValue={plotSpecification.labelAttributeValue}
+          entitiesFromSchema={entitiesFromSchema}
+          onTitleChanged={onTitleChanged}
+          possibleAttrValues={plotData.possibleTitleAttributeValues}
+        />
+        {circles}
+        <PlotAxisLabelsComponent
+          width={width}
+          height={height}
+          xAxisLabel={xAxisLabel}
+          yAxisLabel={yAxisLabel}
+          axisLabelOptions={axisLabelOptions}
+          onAxisLabelChanged={onAxisLabelChanged}
+        />
+        <g
+          onClick={() => onDelete()}
+          transform={`translate(${width + 3},${height - 30})`}
+          className="delete-plot-icon"
+        >
+          <rect x1="-1" y1="-1" width="26" height="26" fill="transparent" />
+          <path d="M3,6V24H21V6ZM8,20a1,1,0,0,1-2,0V10a1,1,0,0,1,2,0Zm5,0a1,1,0,0,1-2,0V10a1,1,0,0,1,2,0Zm5,0a1,1,0,0,1-2,0V10a1,1,0,0,1,2,0Z" />
+          <path
+            className="lid"
+            d="M22,2V4H2V2H7.711c.9,0,1.631-1.1,1.631-2h5.315c0,.9.73,2,1.631,2Z"
+          />
+        </g>
+        <BrushComponent
+          width={width}
+          height={height}
+          xAxisDomain={plotData.minmaxXAxis}
+          yAxisDomain={plotData.minmaxYAxis}
+          xAxisLabel={plotSpecification.xAxis}
+          yAxisLabel={plotSpecification.yAxis}
+          onBrush={onBrush}
+        />
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d2152829423998303a1310991325bd88dff3fde2
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleComponent.tsx
@@ -0,0 +1,207 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import { Menu, MenuItem } from '@mui/material';
+import Color from 'color';
+import React, { ReactElement } from 'react';
+import { EntitiesFromSchema } from '../Types';
+import styles from './PlotTitleStyles.module.css';
+import { XYPosition } from 'reactflow';
+import { getWidthOfText } from '@graphpolaris/shared/lib/schema/schema-utils';
+import OptimizedAutocomplete from './OptimizedAutocomplete';
+
+/** Props of the plots title component. */
+type PlotTitleProps = {
+  entity: string;
+  attributeName: string;
+  attributeValue: string;
+  entitiesFromSchema: EntitiesFromSchema;
+  nodeColor: string;
+  pos: XYPosition;
+  possibleAttrValues: string[];
+  onTitleChanged(entity: string, attrName: string, attrValue: string): void;
+};
+
+/** State of the plots title component. */
+type PlotTitleState = {
+  isEditingAttrValue: boolean;
+
+  // Dropdown when editing entity or attributeName
+  menuAnchor: Element | null;
+  menuItems: string[];
+  menuItemsOnClick(value: string): void;
+};
+/**
+ * A semantic substrates React component for rendering a plot title.
+ * With functionality to change the entity, attributeType and attributeName.
+ */
+export default class PlotTitleComponent extends React.Component<PlotTitleProps, PlotTitleState> {
+  constructor(props: PlotTitleProps) {
+    super(props);
+
+    this.state = {
+      isEditingAttrValue: false,
+      menuAnchor: null,
+      menuItems: [],
+      menuItemsOnClick: () => true,
+    };
+  }
+
+  /**
+   * Will be called when the value of the entity changes.
+   * @param {string} value The new entity value.
+   */
+  private onEntityChanged(value: string): void {
+    // Get the first attribute name for this entity.
+    const attrName =
+      this.props.entitiesFromSchema.attributesPerEntity[value].textAttributeNames.length > 0
+        ? this.props.entitiesFromSchema.attributesPerEntity[value].textAttributeNames[0]
+        : '?';
+
+    this.props.onTitleChanged(value, attrName, '?');
+  }
+
+  /**
+   * Will be called when the attribute name changes.
+   * @param {string} value The new attribute name value.
+   */
+  private onAttrNameChanged(value: string): void {
+    this.props.onTitleChanged(this.props.entity, value, '?');
+  }
+
+  /**
+   * Will be called when the attribute value changes.
+   * @param {string} value The new value of the attribute value.
+   */
+  private onAttrValueChanged(value: string): void {
+    if (value == '') value = '?';
+
+    // only update the state if the attribute value didn't change
+    // If the attribute value did change, this component will be rerendered anyway
+    if (value != this.props.attributeValue)
+      this.props.onTitleChanged(this.props.entity, this.props.attributeName, value);
+    else
+      this.setState({
+        isEditingAttrValue: false,
+      });
+  }
+
+  /**
+   * Renders the attribute value, either plain svg <text> element or, <input> if the user is editing this field.
+   * @param {number} xOffset The x position offset.
+   * @returns {ReactElement} The svg elements to render for the attribute value.
+   */
+  private renderAttributeValue(xOffset: number): ReactElement {
+    if (this.state.isEditingAttrValue)
+      return (
+        <foreignObject x={xOffset} y="-16" width="200" height="150">
+          <div>
+            <OptimizedAutocomplete
+              options={this.props.possibleAttrValues}
+              currentValue={this.props.attributeValue}
+              onLeave={(v: string) => this.onAttrValueChanged(v)}
+            />
+          </div>
+        </foreignObject>
+      );
+    else
+      return (
+        <text
+          x={xOffset}
+          className={styles.clickable}
+          onClick={() => this.setState({ isEditingAttrValue: true })}
+        >
+          {this.props.attributeValue}
+        </text>
+      );
+  }
+
+  render(): ReactElement {
+    const { entity, attributeName, entitiesFromSchema, pos, nodeColor } = this.props;
+
+    const withOffset = getWidthOfText(entity + ' ', 'arial', '15px', 'bold');
+    const attrNameOffset = getWidthOfText('with ', 'arial', '15px') + withOffset;
+    const colonOffset =
+      getWidthOfText(attributeName + ' ', 'arial', '15px', 'bold') + attrNameOffset;
+    const attrValueOffset = getWidthOfText(': ', 'arial', '15px', 'bold') + colonOffset;
+
+    const nodeColorDarkened = Color(nodeColor).darken(0.3).hex();
+
+    return (
+      <g transform={`translate(${pos.x},${pos.y})`}>
+        <text
+          className={styles.clickable}
+          fill={nodeColorDarkened}
+          onClick={(event: React.MouseEvent<SVGTextElement>) => {
+            this.setState({
+              menuAnchor: event.currentTarget,
+              menuItems: entitiesFromSchema.entityNames,
+              menuItemsOnClick: (v: string) => this.onEntityChanged(v),
+            });
+          }}
+        >
+          {entity}
+        </text>
+        <text x={withOffset} fontSize={15}>
+          with
+        </text>
+        <text
+          className={styles.clickable}
+          x={attrNameOffset}
+          onClick={(event: React.MouseEvent<SVGTextElement>) => {
+            this.setState({
+              menuAnchor: event.currentTarget,
+              menuItems: entitiesFromSchema.attributesPerEntity[entity].textAttributeNames,
+              menuItemsOnClick: (v: string) => this.onAttrNameChanged(v),
+            });
+          }}
+        >
+          {attributeName}
+        </text>
+        <text x={colonOffset} fontWeight="bold" fontSize={15}>
+          :
+        </text>
+
+        {this.renderAttributeValue(attrValueOffset)}
+
+        <Menu
+          id="simple-menu"
+          anchorEl={this.state.menuAnchor}
+          keepMounted
+          // getContentAnchorEl={null}
+          anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+          transformOrigin={{ vertical: 'top', horizontal: 'center' }}
+          open={Boolean(this.state.menuAnchor)}
+          onClose={() => this.setState({ menuAnchor: null })}
+          transitionDuration={150}
+          PaperProps={{
+            style: {
+              maxHeight: 48 * 4.5, // 48 ITEM_HEIGHT
+            },
+          }}
+        >
+          {this.state.menuItems.map((option) => (
+            <MenuItem
+              key={option}
+              selected={option == this.state.menuAnchor?.innerHTML}
+              onClick={() => {
+                this.setState({ menuAnchor: null, menuItems: [] });
+
+                this.state.menuItemsOnClick(option);
+              }}
+            >
+              {option}
+            </MenuItem>
+          ))}
+        </Menu>
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..35a50d6d96f000588f412cab7ae74ce2fd823fbd
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css
@@ -0,0 +1,22 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+
+/* Contains styling for the plot title text. */
+.clickable {
+  font-weight: bold;
+  cursor: pointer;
+  font-size: 15px;
+  transition: 2s;
+}
+.clickable:hover {
+  /* filter: drop-shadow(1px 1px 0.4px #80808078); */
+  text-decoration: underline;
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css.d.ts b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..59be279512012689a5541129ebbb3a57c950b833
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/PlotTitleStyles.module.css.d.ts
@@ -0,0 +1,4 @@
+declare const classNames: {
+  readonly clickable: 'clickable';
+};
+export = classNames;
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/SVGCheckBoxComponent.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/SVGCheckBoxComponent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d1f8a425a209e851b8290d9c9002fda82fe8c193
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/SVGCheckBoxComponent.tsx
@@ -0,0 +1,155 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* istanbul ignore file */
+/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
+ * We do not test components/renderfunctions/styling files.
+ * See testing plan for more details.*/
+import Color from 'color';
+import React, { ReactElement } from 'react';
+import {
+  PlotType,
+  RelationType,
+} from '../Types';
+
+/** All the props needed to visualize all the semantic substrates checkboxes. */
+type SVGCheckboxesWithSemanticSubstrLabelProps = {
+  plots: PlotType[];
+  relations: RelationType[][][];
+  visibleRelations: boolean[][];
+  nodeColors: string[];
+  onCheckboxChanged(fromPlot: number, toPlot: number, value: boolean): void;
+};
+
+/** Renders all the checkboxes for a semantic substrates visualisation. */
+export default class SVGCheckboxesWithSemanticSubstrLabel extends React.Component<SVGCheckboxesWithSemanticSubstrLabelProps> {
+  render(): ReactElement {
+    const { plots, relations, visibleRelations, nodeColors, onCheckboxChanged } = this.props;
+
+    const checkboxGroups: ReactElement[] = [];
+
+    // Go through each relation.
+    for (let fromPlot = 0; fromPlot < relations.length; fromPlot++) {
+      const checkboxes: JSX.Element[] = [];
+      // The from- and toPlot title color will be a bit darker than the node colors for their plots.
+      const fromColor = Color(nodeColors[fromPlot]).darken(0.3).hex();
+      for (let toPlot = 0; toPlot < relations[fromPlot].length; toPlot++) {
+        if (!!relations?.[fromPlot]?.[toPlot] && visibleRelations?.[fromPlot]?.[toPlot] !== undefined && relations[fromPlot][toPlot].length > 0) {
+          // Add a checkbox for the connections between fromPlot and toPlot.
+          checkboxes.push(
+            <g
+              key={plots[fromPlot].title + fromPlot + '-' + plots[toPlot].title + toPlot}
+              transform={'translate(0,' + toPlot * 30 + ')'}
+            >
+              <text x={20} y={12} fill={fromColor} style={{ fontWeight: 'bold' }}>
+                {plots[fromPlot].title}
+              </text>
+              <path
+                fill="transparent"
+                stroke={'black'}
+                d={`M${110} 7 l40 0 l-0 0 l-15 -5 m15 5 l-15 5`}
+              />
+              <text
+                x={170}
+                y={12}
+                fill={Color(nodeColors[toPlot]).darken(0.3).hex()}
+                style={{ fontWeight: 'bold' }}
+              >
+                {plots[toPlot].title}
+              </text>
+              <SVGCheckboxComponent
+                x={0}
+                y={0}
+                key={plots[fromPlot].title + plots[toPlot].title + fromPlot + toPlot}
+                width={15}
+                value={visibleRelations[fromPlot][toPlot]}
+                onChange={(value: boolean) => {
+                  onCheckboxChanged(fromPlot, toPlot, value);
+                }}
+              />
+            </g>,
+          );
+        }
+      }
+
+      // Offset the height of the checkboxes for each plot.
+      checkboxGroups.push(
+        <g
+          key={plots[fromPlot].title + fromPlot}
+          transform={
+            'translate(' + (plots[fromPlot].width + 30) + ',' + plots[fromPlot].yOffset + ')'
+          }
+        >
+          {checkboxes}
+        </g>,
+      );
+    }
+
+    return <g>{checkboxGroups}</g>;
+  }
+}
+
+/** The props for the SVG checkbox component */
+type SVGCheckBoxProps = {
+  x: number;
+  y: number;
+  width: number;
+  value: boolean;
+  // Called when the the checkbox state changes.
+  onChange(value: boolean): void;
+};
+
+/** The state for the SVG checkbox component  */
+type SVGCheckBoxState = {
+  value: boolean;
+};
+/** Renders a simple checkbox in SVG elements. */
+export class SVGCheckboxComponent extends React.Component<SVGCheckBoxProps, SVGCheckBoxState> {
+  static defaultProps = {
+    width: 15,
+    value: false,
+    onChange: () => true,
+  };
+  constructor(props: SVGCheckBoxProps) {
+    super(props);
+    this.state = { value: this.props.value };
+  }
+
+  render(): ReactElement {
+    return (
+      <g>
+        <rect
+          x={this.props.x}
+          y={this.props.y}
+          rx="0"
+          ry="0"
+          width={this.props.width}
+          height={this.props.width}
+          style={{ fill: 'transparent', stroke: 'grey', strokeWidth: 2 }}
+          onClick={() => {
+            const newVal = this.state.value ? false : true;
+            this.props.onChange(newVal);
+            this.setState({ value: newVal });
+          }}
+        />
+        <rect
+          x={this.props.x + 3}
+          y={this.props.y + 3}
+          rx="0"
+          ry="0"
+          width={this.props.width - 6}
+          height={this.props.width - 6}
+          style={{ fill: this.state.value ? '#00a300' : 'white' }}
+          onClick={() => {
+            const newVal = this.state.value ? false : true;
+            this.props.onChange(newVal);
+            this.setState({ value: newVal });
+          }}
+        />
+      </g>
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcConnectionLinePositionsUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcConnectionLinePositionsUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..857aad24b83e05269c5041a997f4072c832eab35
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcConnectionLinePositionsUseCase.tsx
@@ -0,0 +1,169 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import { CalcDistance } from './CalcDistance';
+import { RotateVectorByDeg } from '../utils/RotateVec';
+import { XYPosition } from 'reactflow';
+
+/** The return type for this usecase. controlPoint is used for the curvature of the line. */
+type ConnecionLinePositions = {
+  start: XYPosition;
+  end: XYPosition;
+  controlPoint: XYPosition;
+
+  arrowRStart: XYPosition;
+  arrowLStart: XYPosition;
+};
+
+/** The use case that calculates the line positions */
+export default class CalcConnectionLinePositionsUseCase {
+  /**
+   * Calculates the positions for the points needed to draw the curved line with the arrow.
+   * Also offsets the start and end point so they touch the edge of the node, instead of going to the center.
+   * @param {XYPosition} startNodePos The position of the start node.
+   * @param {XYPosition} endNodePos The position of the end node.
+   * @param {number} nodeRadius The node radius, used to calculate the start and end offset.
+   * @returns {ConnecionLinePositions} The positions for drawing the curved line.
+   */
+  public static calculatePositions(
+    startNodePos: XYPosition,
+    endNodePos: XYPosition,
+    nodeRadius: number,
+  ): ConnecionLinePositions {
+    // Calculate the control point for the quadratic curve path, see https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
+    const distance = CalcDistance(startNodePos, endNodePos);
+    if (distance == 0) return this.returnStartPosition(startNodePos);
+    const controlPoint = this.calculateControlPoint(endNodePos, startNodePos, distance);
+
+    // move the start and end point so they are on the edge of their node
+    const movedStartPos = this.calculateMovedStartPos(startNodePos, controlPoint, nodeRadius);
+    const endToControlPointDist = CalcDistance(endNodePos, controlPoint);
+    const endToControlPointVec = {
+      x: (endNodePos.x - controlPoint.x) / endToControlPointDist,
+      y: (endNodePos.y - controlPoint.y) / endToControlPointDist,
+    };
+
+    const movedEndPos = this.calculateMovedEndPos(endNodePos, endToControlPointVec, nodeRadius);
+
+    // Create arrowhead start points
+    const arrowRStart = this.calculateArrowStart(
+      movedEndPos,
+      RotateVectorByDeg(endToControlPointVec, 30),
+    );
+    const arrowLStart = this.calculateArrowStart(
+      movedEndPos,
+      RotateVectorByDeg(endToControlPointVec, -30),
+    );
+
+    return {
+      start: movedStartPos,
+      end: movedEndPos,
+      controlPoint,
+      arrowRStart,
+      arrowLStart,
+    };
+  }
+
+  /**
+   * Creates the connection line positions for a distance of zero.
+   * In this special case all values are set to the start node position.
+   * @param {XYPosition} startNodePos The position of the start node.
+   * @returns {ConnectionLinePosition} The positions for drawing the curved line.
+   */
+  private static returnStartPosition(startNodePos: XYPosition): ConnecionLinePositions {
+    return {
+      start: startNodePos,
+      end: startNodePos,
+      controlPoint: startNodePos,
+      arrowRStart: startNodePos,
+      arrowLStart: startNodePos,
+    };
+  }
+
+  /**
+   * Calculate the control point for the quadratic curve path.
+   * @param {XYPosition} startNodePos The position of the start node.
+   * @param {XYPosition} endNodePos The position of the end node.
+   * @param {number} distance The distance between the two nodes.
+   * @returns {XYPosition} The control point.
+   */
+  private static calculateControlPoint(
+    endNodePos: XYPosition,
+    startNodePos: XYPosition,
+    distance: number,
+  ): XYPosition {
+    // Normalized vector from start to end
+    const vec: XYPosition = {
+      x: (endNodePos.x - startNodePos.x) / distance,
+      y: (endNodePos.y - startNodePos.y) / distance,
+    };
+
+    // The point between the start and end, moved 15% of the distance closer to the start
+    const pointBetween = {
+      x: (startNodePos.x + endNodePos.x) / 2 - vec.x * distance * 0.15,
+      y: (startNodePos.y + endNodePos.y) / 2 - vec.y * distance * 0.15,
+    };
+
+    // The control point for th quadratic curve
+    // Move this point 25% of the distance away from the line between the start and end, at a 90 deg. angle
+    return {
+      x: pointBetween.x + -vec.y * distance * 0.25,
+      y: pointBetween.y + vec.x * distance * 0.25,
+    };
+  }
+
+  /**
+   * Calculates the moved start position.
+   * @param {XYPosition} startNodePos The position of the start node.
+   * @param {XYPosition} controlPoint The control point for the quadratic curve path.
+   * @param {number} nodeRadius The node radius, used to calculate the start and end offset.
+   * @returns {XYPosition} The moved start position.
+   */
+  private static calculateMovedStartPos(
+    startNodePos: XYPosition,
+    controlPoint: XYPosition,
+    nodeRadius: number,
+  ): XYPosition {
+    const startToControlPointDist = CalcDistance(startNodePos, controlPoint);
+    return {
+      x:
+        startNodePos.x + ((controlPoint.x - startNodePos.x) / startToControlPointDist) * nodeRadius,
+      y:
+        startNodePos.y + ((controlPoint.y - startNodePos.y) / startToControlPointDist) * nodeRadius,
+    };
+  }
+
+  /**
+   * Calculates the moved end position
+   * @param {XYPosition} endNodePos The position of the end node.
+   * @param {XYPosition} endToControlPointVec The control point vector.
+   * @param {number} nodeRadius The node radius, used to calculate the start and end offset.
+   * @returns {XYPosition} The moved end position.
+   */
+  private static calculateMovedEndPos(
+    endNodePos: XYPosition,
+    endToControlPointVec: XYPosition,
+    nodeRadius: number,
+  ): XYPosition {
+    return {
+      x: endNodePos.x - endToControlPointVec.x * nodeRadius,
+      y: endNodePos.y - endToControlPointVec.y * nodeRadius,
+    };
+  }
+
+  /**
+   * Calculates the start position of the arrow.
+   * @param {XYPosition} movedEndPos The position of the moved end node.
+   * @param {XYPosition} rotatedVec The rotated arrow vector.
+   * @returns {XYPosition} The arrow's start position.
+   */
+  private static calculateArrowStart(movedEndPos: XYPosition, rotatedVec: XYPosition): XYPosition {
+    const arrowLength = 7;
+    return {
+      x: movedEndPos.x - rotatedVec.x * arrowLength,
+      y: movedEndPos.y - rotatedVec.y * arrowLength,
+    };
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dac2a940f3786c24c4cf4de7660a6d2292f55dd8
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx
@@ -0,0 +1,70 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { NodeLinkResultType } from "../../shared/ResultNodeLinkParserUseCase";
+import { AxisLabel, PlotSpecifications } from "../Types";
+
+/** UseCase for calculating default plots from node link query result data. */
+export default class CalcDefaultPlotSpecsUseCase {
+  /**
+   * Calculates default plot specifications for incoming query result data.
+   * This determines what the default plots will be after executing a new query.
+   * @param {NodeLinkResultType} nodeLinkResult Query result data in the node link format.
+   * @returns {PlotSpecifications[]} PlotSpecifications to generate the plots with the result data.
+   */
+  public static calculate(nodeLinkResult: NodeLinkResultType): PlotSpecifications[] {
+    // Search through the first nodes' attributes for the shortest attribute value
+    const plotSpecifications: PlotSpecifications[] = [];
+    if (nodeLinkResult.nodes.length > 0) {
+      const firstNodeAttributes = nodeLinkResult.nodes[0].attributes;
+      const firstNodeEntity = nodeLinkResult.nodes[0].id.split('/')[0];
+
+      let shortestStringKey = '';
+      let shortestStringValueLength: number = Number.MAX_VALUE;
+      for (let key in firstNodeAttributes) {
+        if (typeof firstNodeAttributes[key] == 'string') {
+          const v = firstNodeAttributes[key];
+          if (v.length < shortestStringValueLength) {
+            shortestStringKey = key;
+            shortestStringValueLength = v.length;
+          }
+        }
+      }
+
+      // The key with the shortest attribute value, will be used to filter nodes for max 3 plots
+      const values: string[] = [];
+      for (let i = 0; i < nodeLinkResult.nodes.length; i++) {
+        // Search for the first three nodes with different attribute values with the given attributekey
+
+        if (nodeLinkResult.nodes[i].id.split('/')[0] != firstNodeEntity) continue;
+
+        const v = nodeLinkResult.nodes[i].attributes[shortestStringKey];
+        if (values.includes(v)) continue;
+
+        values.push(v);
+        let entity = nodeLinkResult.nodes[i].id;
+        if (nodeLinkResult.nodes[i].id.includes('/')) entity = entity.split('/')[0];
+        else if (!!nodeLinkResult.nodes[i]?.attributes?.labels) entity = nodeLinkResult.nodes[i]?.attributes?.labels[0];
+
+        plotSpecifications.push({
+          entity: nodeLinkResult.nodes[i].id.split('/')[0],
+          labelAttributeType: shortestStringKey,
+          labelAttributeValue: nodeLinkResult.nodes[i].attributes[shortestStringKey],
+          xAxis: AxisLabel.evenlySpaced, // Use default evenly spaced and # outbound connections on the x and y axis.
+          yAxis: AxisLabel.outboundConnections,
+          xAxisAttributeType: '',
+          yAxisAttributeType: '',
+          width: 800,
+          height: 200,
+        });
+
+        if (values.length >= 3) break;
+      }
+    }
+
+    return plotSpecifications;
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcDistance.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDistance.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d071418227fa3de805cd2cc9ad220d3aa35e858f
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDistance.tsx
@@ -0,0 +1,20 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { XYPosition } from "reactflow";
+
+/**
+ * Calculates the difference between two positions.
+ * @param {XYPosition} posA The first position.
+ * @param {XYPosition} posB The second position.
+ * @return {number} The distance between the first and second position.
+ */
+export function CalcDistance(posA: XYPosition, posB: XYPosition): number {
+  const diffX = posA.x - posB.x;
+  const diffY = posA.y - posB.y;
+
+  return Math.sqrt(diffX * diffX + diffY * diffY);
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7120f0e0395e7ca7f23eaf5a90e7b97e56579af3
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx
@@ -0,0 +1,36 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { NodeLinkResultType } from "../../shared/ResultNodeLinkParserUseCase";
+import { EntitiesFromSchema } from "../Types";
+
+
+/** Use case for retrieving entity names and attribute names from a schema result. */
+export default class CalcEntityAttrNamesFromResultUseCase {
+  /**
+   * Takes a schema result and calculates all the entity names and attribute names per datatype.
+   * Used by semantic substrates for the plot titles and add plot selections.
+   * @param {NodeLinkResultType} schemaResult A new schema result from the backend.
+   * @param {EntitiesFromSchema} All entity names and attribute names per datatype. So we know what is in the Schema.
+   * @returns {EntitiesFromSchema} All entity names and attribute names per datatype.
+   */
+  public static CalcEntityAttrNamesFromResult(nodeLinkResult: NodeLinkResultType, entitiesFromSchema: EntitiesFromSchema): EntitiesFromSchema {
+    const listOfNodeTypes: string[] = []
+
+    nodeLinkResult.nodes.forEach((node) => {
+      let entityName = node.id.split('/')[0]
+      if (!listOfNodeTypes.includes(entityName)) {
+        listOfNodeTypes.push(entityName);
+      }
+    })
+
+    let entitiesFromSchemaPruned: EntitiesFromSchema = { entityNames: [], attributesPerEntity: {} };;
+    Object.assign(entitiesFromSchemaPruned, entitiesFromSchema);
+
+    entitiesFromSchemaPruned.entityNames = listOfNodeTypes;
+    return entitiesFromSchemaPruned;
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromSchemaUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromSchemaUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6e84d932cd3eee6b06467cbeb8e9727eb0fbdabc
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromSchemaUseCase.tsx
@@ -0,0 +1,49 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import { SchemaGraph } from '@graphpolaris/shared/lib/schema';
+import { Schema } from '../../shared/InputDataTypes';
+import { AttributeNames, EntitiesFromSchema } from '../Types';
+
+/** Use case for retrieving entity names and attribute names from a schema result. */
+export default class CalcEntityAttrNamesFromSchemaUseCase {
+  /**
+   * Takes a schema result and calculates all the entity names and attribute names per datatype.
+   * Used by semantic substrates for the plot titles and add plot selections.
+   * @param {SchemaResultType} schemaResult A new schema result from the backend.
+   * @returns {EntitiesFromSchema} All entity names and attribute names per datatype.
+   */
+  public static calculate(schemaResult: SchemaGraph): EntitiesFromSchema {
+    const attributesPerEntity: Record<string, AttributeNames> = {};
+    // Go through each entity.
+    schemaResult.nodes.forEach((node) => {
+      if (!node.attributes) return;
+
+      // Extract the attribute names per datatype for each entity.
+      const textAttributeNames: string[] = [];
+      const boolAttributeNames: string[] = [];
+      const numberAttributeNames: string[] = [];
+
+      node.attributes.attributes.forEach((attr) => {
+        if (attr.type == 'string') textAttributeNames.push(attr.name);
+        else if (attr.type == 'int') numberAttributeNames.push(attr.name);
+        else if (attr.type == 'bool') boolAttributeNames.push(attr.name);
+      });
+
+      // Create a new object with the arrays with attribute names per datatype.
+      attributesPerEntity[node.attributes.name] = {
+        textAttributeNames,
+        boolAttributeNames,
+        numberAttributeNames,
+      };
+    });
+
+    // Create the object with entity names and attribute names.
+    return {
+      entityNames: schemaResult.nodes.map((node) => (node?.attributes?.name || 'ERROR')),
+      attributesPerEntity,
+    };
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcScaledPositionsUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcScaledPositionsUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d850b3152de1bb6871f308a068127104ae0f3de6
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcScaledPositionsUseCase.tsx
@@ -0,0 +1,73 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { XYPosition } from "reactflow";
+import { MinMaxType, PlotInputData } from "../Types";
+import { scaleLinear } from "d3";
+
+/**
+ * Calculates the scaled positions using d3's scaleLinear functions.
+ * Also calculates the min and max x y values.
+ */
+export default class CalcScaledPosUseCase {
+  /**
+   * Linearly scales positions to the width and height of the plot
+   * @param {PlotInputData} plot The plot for which to scale the nodes positions.
+   * @param {MinMaxType} minmaxXAxis The min and max for the x axis.
+   * @param {MinMaxType} minmaxYAxis The min and max for the y axis.
+   * @returns {Position[]} The scaled positions.
+   */
+  public static calculate(
+    plot: PlotInputData,
+    minmaxXAxis: MinMaxType,
+    minmaxYAxis: MinMaxType,
+  ): XYPosition[] {
+    // Create the scale functions with the minmax and width and height of the plot
+    const scaleFunctions = CalcScaledPosUseCase.createScaleFunctions(
+      minmaxXAxis,
+      minmaxYAxis,
+      plot.width,
+      plot.height,
+    );
+
+    // Use the scale functions to scale the nodes positions.
+    const scaledPositions: XYPosition[] = [];
+    plot.nodes.forEach((node) => {
+      scaledPositions.push({
+        x: scaleFunctions.xAxis(node.originalPosition.x),
+        y: scaleFunctions.yAxis(node.originalPosition.y),
+      });
+    });
+
+    return scaledPositions;
+  }
+
+  /** Uses D3 to create linear scale functions. */
+  private static createScaleFunctions = (
+    minmaxXAxis: MinMaxType,
+    minmaxYAxis: MinMaxType,
+    plotWidth: number,
+    plotHeight: number,
+  ): {
+    xAxis: d3.ScaleLinear<number, number, never>;
+    yAxis: d3.ScaleLinear<number, number, never>;
+  } => {
+    // Create the x axis scale funtion with d3
+    const xAxisScale = scaleLinear()
+      .domain([minmaxXAxis.min, minmaxXAxis.max])
+      .range([0, plotWidth]);
+
+    // Create the y axis scale funtion with d3
+    const yAxisScale = scaleLinear()
+      .domain([minmaxYAxis.max, minmaxYAxis.min])
+      .range([0, plotHeight]);
+
+    return {
+      xAxis: xAxisScale,
+      yAxis: yAxisScale,
+    };
+  };
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcXYMinMaxUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcXYMinMaxUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..11f9dcdd96a0284f1a00e40856112a59eb904126
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcXYMinMaxUseCase.tsx
@@ -0,0 +1,71 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+import { MinMaxType, PlotInputData } from "../Types";
+
+/** UseCase for calculating the min and max for the x and y values of the node positions. */
+export default class CalcXYMinMaxUseCase {
+  /**
+   * Calculates the min and max for the x and y values of the node positions.
+   * @param {PlotInputData} plot The input data plot with all the nodes.
+   * @returns {{x: MinMaxType; y: MinMaxType}} An object with the min and max for x and y.
+   */
+  public static calculate(plot: PlotInputData): { x: MinMaxType; y: MinMaxType } {
+    // If there are no nodes in the plot, set the min and max to [-10, 10]
+    if (plot.nodes.length == 0) {
+      return {
+        x: {
+          min: -10,
+          max: 10,
+        },
+        y: {
+          min: -10,
+          max: 10,
+        },
+      };
+    }
+    // Calculate the min and max values for the x and y positions
+    // Start the x and y values of the first node as min and max
+    const minmaxX: MinMaxType = {
+      min: plot.nodes[0].originalPosition.x,
+      max: plot.nodes[0].originalPosition.x,
+    };
+    const minmaxY: MinMaxType = {
+      min: plot.nodes[0].originalPosition.y,
+      max: plot.nodes[0].originalPosition.y,
+    };
+    for (let i = 1; i < plot.nodes.length; i++) {
+      const position = plot.nodes[i].originalPosition;
+
+      if (position.x > minmaxX.max) minmaxX.max = position.x;
+      else if (position.x < minmaxX.min) minmaxX.min = position.x;
+
+      if (position.y > minmaxY.max) minmaxY.max = position.y;
+      else if (position.y < minmaxY.min) minmaxY.min = position.y;
+    }
+
+    // Add 20%, so there are no nodes whose position is exactly on the axis
+    let xDiff = (minmaxX.max - minmaxX.min) * 0.2;
+
+    minmaxX.min -= xDiff;
+    minmaxX.max += xDiff;
+    let yDiff = (minmaxY.max - minmaxY.min) * 0.2;
+
+    minmaxY.min -= yDiff;
+    minmaxY.max += yDiff;
+
+    // If the min and max are the same, add and subtract 10
+    if (minmaxX.min == minmaxX.max) {
+      minmaxX.min -= 1;
+      minmaxX.max += 1;
+    }
+    if (minmaxY.min == minmaxY.max) {
+      minmaxY.min -= 1;
+      minmaxY.max += 1;
+    }
+    return { x: minmaxX, y: minmaxY };
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/FilterUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/FilterUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..532a3c8fe52473dfc1a1f6e5c09341a167af4366
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/FilterUseCase.tsx
@@ -0,0 +1,140 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import {
+  MinMaxType,
+  NodeType,
+  PlotType,
+  RelationType,
+} from '../Types';
+
+type Filter = { x: MinMaxType; y: MinMaxType };
+
+/** A use case for applying filters on nodes and filter relations. */
+export default class FilterUseCase {
+  /**
+   * Checks if a node matches the given filters.
+   * @param {NodeType} node The node to be matched.
+   * @param {Filter} filter The filters to match the node to.
+   * @returns True when the node matches the constraints of the filter.
+   */
+  public static checkIfNodeMatchesFilter(node: NodeType, filter: Filter): boolean {
+    return (
+      node.originalPosition.x >= filter.x.min &&
+      node.originalPosition.x <= filter.x.max &&
+      node.originalPosition.y >= filter.y.min &&
+      node.originalPosition.y <= filter.y.max
+    );
+  }
+
+  /**
+   * Applies the filter that changed to the filteredRelations list.
+   * @param {RelationType[][][]} relations The original unmodified relations. All relations.
+   * @param {RelationType[][][]} filteredRelations The current filtered relations. This will be updated with the new filter.
+   * Note that initially relations should be a clone of filteredRelations.
+   * @param {PlotType[]} plots The plots.
+   * @param {Filter[]} filtersPerPlot The filters for each plot, with a filter at the filterIndexThatChanged that is different from the previous call to this function.
+   * @param {number} filterIndexThatChanged index for the filter that changed.
+   */
+  public static filterRelations(
+    relations: RelationType[][][],
+    filteredRelations: RelationType[][][],
+    plots: PlotType[],
+    filtersPerPlot: Filter[],
+    filterIndexThatChanged: number,
+  ): void {
+    this.checkOutboundConnections(
+      relations,
+      filteredRelations,
+      plots,
+      filtersPerPlot,
+      filterIndexThatChanged,
+    );
+    for (let fromPlot = 0; fromPlot < relations.length; fromPlot++) {
+      this.checkInboundConnection(
+        relations,
+        filteredRelations,
+        plots,
+        filtersPerPlot,
+        filterIndexThatChanged,
+        fromPlot,
+      );
+    }
+  }
+
+  /**
+   * Check for the inbound connections if the filter has changed
+   * @param {RelationType[][][]} relations The original unmodified relations. All relations.
+   * @param {RelationType[][][]} filteredRelations The current filtered relations. This will be updated with the new filter.
+   * Note that initially relations should be a clone of filteredRelations.
+   * @param {PlotType[]} plots The plots.
+   * @param {Filter[]} filtersPerPlot The filters for each plot, with a filter at the filterIndexThatChanged that is different from the previous call to this function.
+   * @param {number} filterIndexThatChanged index for the filter that changed.
+   */
+  private static checkOutboundConnections(
+    relations: RelationType[][][],
+    filteredRelations: RelationType[][][],
+    plots: PlotType[],
+    filtersPerPlot: Filter[],
+    filterIndexThatChanged: number,
+  ) {
+    // Check all the outbound connections for nodes in the plot for which the filter changed.
+    relations[filterIndexThatChanged].forEach((relations, toPlot) => {
+      filteredRelations[filterIndexThatChanged][toPlot] = relations.filter((relation) => {
+        const fromNode = plots[filterIndexThatChanged].nodes[relation.fromIndex];
+        const fromNodeFilter = filtersPerPlot[filterIndexThatChanged];
+        const fromNodeMatches = this.checkIfNodeMatchesFilter(fromNode, fromNodeFilter);
+
+        const toNode = plots[toPlot].nodes[relation.toIndex];
+        const toNodeFilter = filtersPerPlot[toPlot];
+        const toNodeMatches = this.checkIfNodeMatchesFilter(toNode, toNodeFilter);
+
+        // Check if the from- and to-node match their plot filters.
+        return fromNodeMatches && toNodeMatches;
+      });
+    });
+  }
+
+  /**
+   * Check for the inbound connections if the filter has changed
+   * @param {RelationType[][][]} relations The original unmodified relations. All relations.
+   * @param {RelationType[][][]} filteredRelations The current filtered relations. This will be updated with the new filter.
+   * Note that initially relations should be a clone of filteredRelations.
+   * @param {PlotType[]} plots The plots.
+   * @param {Filter[]} filtersPerPlot The filters for each plot, with a filter at the filterIndexThatChanged that is different from the previous call to this function.
+   * @param {number} filterIndexThatChanged index for the filter that changed.
+   * @param {number} fromPlot The index of the current from plot.
+   */
+  private static checkInboundConnection(
+    relations: RelationType[][][],
+    filteredRelations: RelationType[][][],
+    plots: PlotType[],
+    filtersPerPlot: Filter[],
+    filterIndexThatChanged: number,
+    fromPlot: number,
+  ) {
+    // Check all the inbound connections for nodes in the plot for which the filter changed.
+    const relationsBetweenPlots = relations[fromPlot][filterIndexThatChanged];
+
+    filteredRelations[fromPlot][filterIndexThatChanged] = relationsBetweenPlots.filter(
+      (relation) => {
+        const toNode = plots[filterIndexThatChanged].nodes[relation.toIndex];
+        const toNodeFilter = filtersPerPlot[filterIndexThatChanged];
+        const toNodeMatches = this.checkIfNodeMatchesFilter(toNode, toNodeFilter);
+
+        const fromNode = plots[fromPlot].nodes[relation.fromIndex];
+        const fromNodeFilter = filtersPerPlot[fromPlot];
+        const fromNodeMatches = this.checkIfNodeMatchesFilter(fromNode, fromNodeFilter);
+
+        // Here we also check for connections within the same plot.
+        // For these connections we only need to check if the from- or the to-node matches the filter.
+        return (
+          (fromNodeMatches && toNodeMatches) ||
+          (fromPlot == filterIndexThatChanged && (fromNodeMatches || toNodeMatches))
+        );
+      },
+    );
+  }
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/RotateVec.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/RotateVec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5677ebb1fd2f3bdc86d802995556d04fa8cb354a
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/RotateVec.tsx
@@ -0,0 +1,23 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/**
+ * Rotates a vector by given degrees, clockwise.
+ * @param {number, number} vec The vector to be rotated.
+ * @param {number} degrees The amount of degrees the vector needs to be rotated.
+ * @return {number, number} The rotated vector.
+ */
+export function RotateVectorByDeg(
+  vec: { x: number; y: number },
+  degrees: number,
+): { x: number; y: number } {
+  const radians = -(degrees % 360) * 0.01745329251; // degrees * (PI/180)
+
+  return {
+    x: vec.x * Math.cos(radians) - vec.y * Math.sin(radians),
+    y: vec.x * Math.sin(radians) + vec.y * Math.cos(radians),
+  };
+}
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7c09b475f5987a3d1a02e08a2a2b92788e32fac7
--- /dev/null
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx
@@ -0,0 +1,267 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access';
+import { NodeLinkResultType, Node, ParseToUniqueEdges } from '../../shared/ResultNodeLinkParserUseCase';
+import {
+  AxisLabel,
+  PlotInputData,
+  PlotSpecifications,
+  RelationType,
+} from '../Types';
+
+/** A use case for parsing incoming node-link data to plot data */
+export default class ToPlotDataParserUseCase {
+  /**
+   * Parses incoming node link data from the backend to plotdata.
+   * @param {NodeLinkResultType} queryResult The query result coming from the backend.
+   * @param {NodeAttributesForPlot[]} plotSpecifications The attribute types to use for label and x y values.
+   * @returns Plotdata with relations.
+   */
+  public static parseQueryResult(
+    queryResult: GraphQueryResult,
+    plotSpecifications: PlotSpecifications[],
+    relationSelectedAttributeNumerical: string,
+    relationSelectedAttributeCatecorigal: string,
+    relationScaleCalculation: (x: number) => number,
+    relationColourCalculation: (x: string) => string,
+  ): { plots: PlotInputData[]; relations: RelationType[][][] } {
+    const { plots, nodePlotIndexDict } = this.filterNodesWithPlotSpecs(
+      queryResult.nodes,
+      plotSpecifications,
+    );
+
+    // Initialize the relations with empty arrays, an |plots| x |plots| matrix with empty arrays
+    const relations: RelationType[][][] = new Array(plots.length)
+      .fill([])
+      .map(() => new Array(plots.length).fill([]).map(() => []));
+
+    // 2D arrays used to collect the number of out- and inbound connections
+    // indexed like so: [plotIndex][nodeIndex in that plot]
+    const nOutboundConnsPerNode: number[][] = new Array(plots.length)
+      .fill([])
+      .map((_, i) => new Array(plots[i].nodes.length).fill(0));
+    const nInboundConnsPerNode: number[][] = new Array(plots.length)
+      .fill([])
+      .map((_, i) => new Array(plots[i].nodes.length).fill(0));
+
+    // Only use unique edges
+    const uniqueEdges = ParseToUniqueEdges.parse(queryResult.edges, false);
+
+    /* Search for the maximum value of the value attribute for each edge.
+     Using epsilon as first max value so we do never divide by zero.
+     */
+    let maxValue = Number.EPSILON;
+    uniqueEdges.forEach((e) => {
+      if (e.count > maxValue) maxValue = e.count;
+    });
+
+    // Calculate the # in- and outbound connections and fill the relations.
+    uniqueEdges.forEach((edge) => {
+      // Check if the from and to are present in any plots
+      if (nodePlotIndexDict[edge.from] && nodePlotIndexDict[edge.to]) {
+        // Retrieve the from and to indices of the plotindex and nodeindex from the dictionary
+        const fromPlotsIndices = nodePlotIndexDict[edge.from];
+        const toPlotsIndices = nodePlotIndexDict[edge.to];
+
+        // The same node can be in multiple plots
+        // We need to add all its connections
+        fromPlotsIndices.forEach((fromPlot) => {
+          nOutboundConnsPerNode[fromPlot.plotIndex][fromPlot.nodeIndex]++;
+
+          toPlotsIndices.forEach((toPlot) => {
+            // Add the connection to the relations list, with the from and to indices
+            relations[fromPlot.plotIndex][toPlot.plotIndex].push({
+              fromIndex: fromPlot.nodeIndex,
+              toIndex: toPlot.nodeIndex,
+              value: relationScaleCalculation(edge.attributes[relationSelectedAttributeNumerical]),
+              colour: relationColourCalculation(
+                edge.attributes[relationSelectedAttributeCatecorigal],
+              ),
+            });
+
+            nInboundConnsPerNode[toPlot.plotIndex][toPlot.nodeIndex]++;
+          });
+        });
+      }
+    });
+
+    // Determine the node position if plotSpecification had an out- or inboundconnection as x or y axis specified
+    this.determineNodePosForInOutboundConn(
+      plots,
+      plotSpecifications,
+      nInboundConnsPerNode,
+      nOutboundConnsPerNode,
+    );
+
+    return { plots, relations };
+  }
+
+  /**
+   * If the node position is of # in- or outbound connections, set this as the original node position.
+   * @param plots {PlotInputData[]} The plots.
+   * @param plotSpecs {PlotSpecifications[]} The plot specifications.
+   * @param nInboundConnsPerNode {number[][]} The number of inbound connections for plots.
+   * @param nOutboundConnsPerNode {number[][]} The number of outbound connections for plots.
+   */
+  private static determineNodePosForInOutboundConn(
+    plots: PlotInputData[],
+    plotSpecs: PlotSpecifications[],
+    nInboundConnsPerNode: number[][],
+    nOutboundConnsPerNode: number[][],
+  ): void {
+    // Determine the node position if plotSpecification had an out- or inboundconnection as x or y axis specified
+    plots.forEach((plot, plotIndex) => {
+      plot.nodes.forEach((node, nodeIndex) => {
+        // Determine the x pos if the plotspecXAxis is of enum out- or inboundConnections
+        const x = this.getAxisPosOutOrInboundConns(
+          plotSpecs[plotIndex].xAxis,
+          plotIndex,
+          nodeIndex,
+          nOutboundConnsPerNode,
+          nInboundConnsPerNode,
+        );
+        if (x != undefined) node.originalPosition.x = x;
+        // Same for the y pos
+        const y = this.getAxisPosOutOrInboundConns(
+          plotSpecs[plotIndex].yAxis,
+          plotIndex,
+          nodeIndex,
+          nOutboundConnsPerNode,
+          nInboundConnsPerNode,
+        );
+        if (y != undefined) node.originalPosition.y = y;
+      });
+    });
+  }
+
+  /**
+   * Filters the nodes with the plotSpecifications.
+   * @param nodes {Node[]} The query result nodes to filter on.
+   * @param plotSpecs {PlotSpecifications[]} The plot specifications used for filtering.
+   * @returns plots with the filtered nodes in them, and a nodePlotIndexDict used for getting the plotIndex and nodeIndex for a node.
+   */
+  private static filterNodesWithPlotSpecs(
+    nodes: Node[],
+    plotSpecs: PlotSpecifications[],
+  ): {
+    plots: PlotInputData[];
+    nodePlotIndexDict: Record<string, { plotIndex: number; nodeIndex: number }[]>;
+  } {
+    // Initialze the plots with a title and the default witdh and height
+    const plots: PlotInputData[] = [];
+    plotSpecs.forEach((plotSpec) => {
+      plots.push({
+        title: plotSpec.labelAttributeType + ':' + plotSpec.labelAttributeValue,
+        nodes: [],
+        width: plotSpec.width,
+        height: plotSpec.height,
+      });
+    });
+
+    // Dictionary used for getting the plotIndex and nodeIndex for a node
+    // plotIndex: in which plot the node is
+    // nodeIndex: the index of the node in the list of nodes for that plot
+    // A node could be in multiple plots so that is why the value is an array.
+    const nodePlotIndexDict: Record<string, { plotIndex: number; nodeIndex: number }[]> = {};
+
+    // Add all nodes to their plot if they satisfy its corresponding plotspec
+    nodes.forEach((node) => {
+      for (let i = 0; i < plotSpecs.length; i++) {
+        // Check if the node has an label attributeType value that matches the filter
+        if (
+          plotSpecs[i].entity == node.id.split('/')[0] &&
+          plotSpecs[i].labelAttributeValue == node.attributes[plotSpecs[i].labelAttributeType]
+        ) {
+          // Check if the axisPositioning spec is of equalDistance or attributeType
+          const x = this.getAxisPosEqualDistanceOrAttrType(
+            plotSpecs[i].xAxis,
+            plots[i].nodes.length + 1,
+            plotSpecs[i].xAxisAttributeType,
+            node.attributes,
+          );
+          const y = this.getAxisPosEqualDistanceOrAttrType(
+            plotSpecs[i].yAxis,
+            plots[i].nodes.length + 1,
+            plotSpecs[i].yAxisAttributeType,
+            node.attributes,
+          );
+
+          // Add the node to the correct plot
+          plots[i].nodes.push({
+            id: node.id,
+            data: { text: node.id },
+            originalPosition: { x, y },
+            attributes: node.attributes,
+          });
+
+          if (!nodePlotIndexDict[node.id]) nodePlotIndexDict[node.id] = [];
+
+          nodePlotIndexDict[node.id].push({
+            plotIndex: i,
+            nodeIndex: plots[i].nodes.length - 1,
+          });
+        }
+      }
+    });
+
+    return { plots, nodePlotIndexDict };
+  }
+
+  /**
+   * Determine the position based on the provided axispositioning specification
+   * Check for enums equalDistance and attributeType
+   * @param plotSpecAxis
+   * @param nodeIndex
+   * @param xAxisAttrType
+   * @param attributes
+   * @returns
+   */
+  private static getAxisPosEqualDistanceOrAttrType(
+    plotSpecAxis: AxisLabel,
+    nodeIndex: number,
+    xAxisAttrType: string,
+    attributes: Record<string, any>,
+  ): number {
+    switch (plotSpecAxis) {
+      case AxisLabel.byAttribute:
+        if (attributes[xAxisAttrType] && !isNaN(attributes[xAxisAttrType]))
+          return +attributes[xAxisAttrType];
+        else return nodeIndex;
+
+      default:
+        // plotSpecAxis == equalDistance
+        return nodeIndex;
+    }
+  }
+
+  /**
+   *
+   * @param plotSpecAxis
+   * @param plotIndex
+   * @param nodeIndex
+   * @param nOutboundConnsPerNode
+   * @param nInboundConnsPerNode
+   * @returns
+   */
+  private static getAxisPosOutOrInboundConns(
+    plotSpecAxis: AxisLabel,
+    plotIndex: number,
+    nodeIndex: number,
+    nOutboundConnsPerNode: number[][],
+    nInboundConnsPerNode: number[][],
+  ): number | undefined {
+    switch (plotSpecAxis) {
+      case AxisLabel.outboundConnections:
+        return nOutboundConnsPerNode[plotIndex][nodeIndex];
+
+      case AxisLabel.inboundConnections:
+        return nInboundConnsPerNode[plotIndex][nodeIndex];
+
+      default:
+        return undefined;
+    }
+  }
+}
diff --git a/libs/shared/lib/vis/shared/InputDataTypes.tsx b/libs/shared/lib/vis/shared/InputDataTypes.tsx
index b675a1fe3c08b3c9d7a7e27c5607149e753ad906..fdc0a35a50a34e0802ef853ad87b393bb1f76754 100644
--- a/libs/shared/lib/vis/shared/InputDataTypes.tsx
+++ b/libs/shared/lib/vis/shared/InputDataTypes.tsx
@@ -6,10 +6,10 @@
 import { AttributeCategory } from './Types';
 
 /** Schema type, consist of nodes and edges */
-export type Schema = {
-  edges: Edge[];
-  nodes: Node[];
-};
+// export type Schema = { DEPRECATED USE SCHEMAGRAPH INSTEAD
+//   edges: Edge[];
+//   nodes: Node[];
+// };
 
 /** Attribute type, consist of a name */
 export type Attribute = {
diff --git a/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx b/libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx
similarity index 91%
rename from libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx
rename to libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx
index dc8cbdca68e19128d7a12f40d922e1631f321488..eadcc972c5380100828af81049c25e40e2978218 100644
--- a/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx
+++ b/libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx
@@ -3,8 +3,8 @@
  * Utrecht University within the Software Project course.
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
-import { GraphType, LinkType, NodeType } from './Types';
-import { Edge, GraphQueryResult } from '../../data-access/store';
+import { GraphType, LinkType, NodeType } from '../nodelink/Types';
+import { Edge, Node, GraphQueryResult } from '../../data-access/store';
 /** ResultNodeLinkParserUseCase implements methods to parse and translate websocket messages from the backend into a GraphType. */
 
 /**
@@ -13,31 +13,34 @@ import { Edge, GraphQueryResult } from '../../data-access/store';
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 /** A node link data-type for a query result object from the backend. */
-export type NodeLinkResultType = {
-  nodes: Node[];
-  edges: Link[];
-  mlEdges?: Link[];
-};
+// export type NodeLinkResultType = { DEPRECATED USE GraphQueryResult
+//   nodes: Node[];
+//   edges: Link[];
+//   mlEdges?: Link[];
+// };
 
 /** Typing for nodes and links in the node-link result. Nodes and links should always have an id and attributes. */
-export interface AxisType {
-  id: string;
-  attributes: Record<string, any>;
-  mldata?: Record<string, string[]> | number; // This is shortest path data . This name is needs to be changed together with backend TODO: Change this.
-}
+// export interface AxisType {
+//   id: string;
+//   attributes: Record<string, any>;
+//   mldata?: Record<string, string[]> | number; // This is shortest path data . This name is needs to be changed together with backend TODO: Change this.
+// }
 
 /** Typing for a node in the node-link result */
-export type Node = AxisType;
+// export type Node = AxisType;
 
 /** Typing for a link in the node-link result */
-export interface Link extends AxisType {
-  from: string;
-  to: string;
-}
+// export interface Link extends AxisType {
+//   from: string;
+//   to: string;
+// }
+
+export type AxisType = Node | Edge;
 
 /** Gets the group to which the node/edge belongs */
 export function getGroupName(axisType: AxisType): string {
-  return axisType.id.split('/')[0];
+  // FIXME: only works in arangodb
+  return (axisType.id as string).split('/')[0];
 }
 
 /** Returns true if the given id belongs to the target group. */
@@ -54,7 +57,7 @@ export function isNotInGroup(
  */
 export function isNodeLinkResult(
   jsonObject: any
-): jsonObject is NodeLinkResultType {
+): jsonObject is GraphQueryResult {
   if (
     typeof jsonObject === 'object' &&
     jsonObject !== null &&
@@ -77,7 +80,7 @@ export function isNodeLinkResult(
 
 /** Returns a record with a type of the nodes as key and a number that represents how many times this type is present in the nodeLinkResult as value. */
 export function getNodeTypes(
-  nodeLinkResult: NodeLinkResultType
+  nodeLinkResult: GraphQueryResult
 ): Record<string, number> {
   const types: Record<string, number> = {};
 
diff --git a/libs/shared/package.json b/libs/shared/package.json
index 717ce13fa739af63f20a7c5a57a870df30cf00e2..a96e25e0a37bcdec274a603db6f9bd3fc47cf7b5 100644
--- a/libs/shared/package.json
+++ b/libs/shared/package.json
@@ -37,18 +37,21 @@
     "graphology-layout-forceatlas2": "^0.10.1",
     "graphology-layout-noverlap": "^0.4.2",
     "graphology-types": "^0.24.7",
+    "immer": "^10.0.2",
     "jspdf": "^2.5.1",
     "pixi.js": "^7.1.4",
     "react-cookie": "^4.1.1",
     "react-grid-layout": "^1.3.4",
     "react-json-view": "^1.21.3",
     "react-router-dom": "^6.8.1",
+    "react-window": "^1.8.9",
     "reactflow": "^11.7.0",
     "regenerator-runtime": "0.13.11",
     "sass": "^1.59.3",
     "scss": "^0.2.4",
     "styled-components": "^5.3.6",
     "tslib": "^2.5.0",
+    "use-immer": "^0.9.0",
     "web-worker": "^1.2.0"
   },
   "devDependencies": {
@@ -60,7 +63,6 @@
     "@types/color": "^3.0.3",
     "@types/d3": "^7.4.0",
     "@types/node": "18.13.0",
-    "@types/pixi.js": "^5.0.0",
     "@types/react": "^18.0.27",
     "@types/react-dom": "^18.0.10",
     "@typescript-eslint/eslint-plugin": "~5.52.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72c5b4c3bb3c6d8af999781af7fb7c763d2dc743..6514d4a8268a3199c6268c08312d01fb3f635b12 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,7 +24,7 @@ importers:
         version: 2.8.8
       turbo:
         specifier: latest
-        version: 1.9.3
+        version: 1.9.9
 
   apps/docs:
     dependencies:
@@ -182,7 +182,7 @@ importers:
         version: 5.11.13(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.0.28)(react@18.2.0)
       '@reactflow/node-resizer':
         specifier: ^2.0.1
-        version: 2.1.0(react-dom@18.2.0)(react@18.2.0)
+        version: 2.1.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       '@reduxjs/toolkit':
         specifier: ^1.9.2
         version: 1.9.3(react-redux@8.0.5)(react@18.2.0)
@@ -225,6 +225,9 @@ importers:
       graphology-types:
         specifier: ^0.24.7
         version: 0.24.7
+      immer:
+        specifier: ^10.0.2
+        version: 10.0.2
       jspdf:
         specifier: ^2.5.1
         version: 2.5.1
@@ -243,9 +246,12 @@ importers:
       react-router-dom:
         specifier: ^6.8.1
         version: 6.9.0(react-dom@18.2.0)(react@18.2.0)
+      react-window:
+        specifier: ^1.8.9
+        version: 1.8.9(react-dom@18.2.0)(react@18.2.0)
       reactflow:
         specifier: ^11.7.0
-        version: 11.7.0(react-dom@18.2.0)(react@18.2.0)
+        version: 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       regenerator-runtime:
         specifier: 0.13.11
         version: 0.13.11
@@ -261,6 +267,9 @@ importers:
       tslib:
         specifier: ^2.5.0
         version: 2.5.0
+      use-immer:
+        specifier: ^0.9.0
+        version: 0.9.0(immer@10.0.2)(react@18.2.0)
       web-worker:
         specifier: ^1.2.0
         version: 1.2.0
@@ -289,9 +298,6 @@ importers:
       '@types/node':
         specifier: 18.13.0
         version: 18.13.0
-      '@types/pixi.js':
-        specifier: ^5.0.0
-        version: 5.0.0(@pixi/utils@7.2.1)
       '@types/react':
         specifier: ^18.0.27
         version: 18.0.28
@@ -339,7 +345,7 @@ importers:
         version: 8.7.0(eslint@7.32.0)
       eslint-config-turbo:
         specifier: latest
-        version: 1.9.3(eslint@7.32.0)
+        version: 1.10.0(eslint@7.32.0)
       eslint-plugin-import:
         specifier: 2.27.5
         version: 2.27.5(@typescript-eslint/parser@5.52.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
@@ -442,10 +448,10 @@ importers:
     devDependencies:
       '@storybook/addon-essentials':
         specifier: next
-        version: 7.0.7(react-dom@18.2.0)(react@18.2.0)
+        version: 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-interactions':
         specifier: next
-        version: 7.0.7(react-dom@18.2.0)(react@18.2.0)
+        version: 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
       '@storybook/addon-links':
         specifier: ^7.0.7
         version: 7.0.7(react-dom@18.2.0)(react@18.2.0)
@@ -529,7 +535,7 @@ importers:
         version: 8.7.0(eslint@7.32.0)
       eslint-config-turbo:
         specifier: latest
-        version: 1.9.3(eslint@7.32.0)
+        version: 1.10.0(eslint@7.32.0)
       eslint-plugin-react:
         specifier: 7.31.8
         version: 7.31.8(eslint@7.32.0)
@@ -3196,6 +3202,7 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/events': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/utils@7.2.1)
+    dev: false
 
   /@pixi/app@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1):
     resolution: {integrity: sha512-I6YLs+JTUQkmZBeO8YzK/9T6P9TpwULyfoMsUh77MiAI0tnJobortXf1eMEgMbq5rVk44M5gtl1MJL9QclxcBA==}
@@ -3205,6 +3212,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
+    dev: false
 
   /@pixi/assets@7.2.1(@pixi/core@7.2.1)(@pixi/utils@7.2.1):
     resolution: {integrity: sha512-6JFm5ZQpE5n2scj60UFN5z8dr/1SDKFmPEgE0HNsqSuIhHlA0yS2ARvgHGRjVJdBPsOSUHsCnVYG9HOlM1pSJA==}
@@ -3215,11 +3223,13 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/utils': 7.2.1
       '@types/css-font-loading-module': 0.0.7
+    dev: false
 
   /@pixi/color@7.2.1:
     resolution: {integrity: sha512-rzJy1Bu6x+LuADSxUImvH49hIVLEl/1bGH+WjfcPOuxPQYZzHTGQJeISQYSJrTE+kdU2+GvRLH8cUlLijyFFqA==}
     dependencies:
       colord: 2.9.3
+    dev: false
 
   /@pixi/compressed-textures@7.2.1(@pixi/assets@7.2.1)(@pixi/core@7.2.1):
     resolution: {integrity: sha512-U5SDgy/JsieMrB45GQDoC9Y055LKh52/EYV8TFlf5OcRvwLBOqjJLXXVwwkDF/79O/5WT2vcR6KSRKjB9gXxgw==}
@@ -3229,9 +3239,11 @@ packages:
     dependencies:
       '@pixi/assets': 7.2.1(@pixi/core@7.2.1)(@pixi/utils@7.2.1)
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/constants@7.2.1:
     resolution: {integrity: sha512-Tyu7Ue59ZYPyhBtP0gneMgixlAvM9+XXBscoICLyompA09NVsarul1czFfP+GnwiW5jVzXvd4kNDPAayNmKRFg==}
+    dev: false
 
   /@pixi/core@7.2.1:
     resolution: {integrity: sha512-ASoedGRUnirpHGP7Axgnarb2v7FHlfHByuFhkWDn+6IXVenQJNgwtC9wRPyzJQV5S0OhxYGDtvw/BsRjUZX3tA==}
@@ -3245,6 +3257,7 @@ packages:
       '@pixi/ticker': 7.2.1
       '@pixi/utils': 7.2.1
       '@types/offscreencanvas': 2019.7.0
+    dev: false
 
   /@pixi/display@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-S8rw+OaP3z4js73z1XmHV8eJnlFfdxwTIwdBoJqu6UPoTowFL5aBaObaNmraQXbCpSIimziG/txaOxrltc2y6Q==}
@@ -3252,6 +3265,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/events@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/utils@7.2.1):
     resolution: {integrity: sha512-kFT3l3g1DwJnDn2xLVct0n2qaerqNdiDOS3njVX/YBp7+IX5xD6gpXxLgy7M7fYOislUwIMorfc6oMUSlpXcKg==}
@@ -3263,9 +3277,11 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/utils': 7.2.1
+    dev: false
 
   /@pixi/extensions@7.2.1:
     resolution: {integrity: sha512-bVPhu+adqW1gsBABX3ZtQ6jpwUaM+Bz9lSv3+kXoKUY+mPlDpXDHDdmJsnGh1yktHvE+WU1tdFv1gSaXA0IfGQ==}
+    dev: false
 
   /@pixi/extract@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-bXnRHBvYBRvmLeTiQvv1ZYVkxqK4RXZTwGsFPsKo39MIzdrtPrGeyzstFGAb3kUTIlsFn5plO+12iKnkxZBlvg==}
@@ -3273,6 +3289,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-alpha@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-qBpp/hvJ5BoqPEXUz5Q93ZSLL5lom7xsdvmr4CLwV5dJLe49a8m3xrbEDRETl7lDOq/SULALTghjdmqdtu/DLQ==}
@@ -3280,6 +3297,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-blur@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-y9bw5g64P7hHq+UFxgEbkFHLK4wIwXHPAoN5RBqee66IMDKpwy+FcLUJMkrh9nJu+1XAS4gzL/4Ewyvq16Z8MA==}
@@ -3287,6 +3305,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-color-matrix@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-VUzl9NDejEHbt7VGyqkNaQLNYk9VLCiksXPQrcWEoRuLUHMhB7BoarPR7esy+QUhzmUwla/H4rR+jwHyi4dRjA==}
@@ -3294,6 +3313,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-displacement@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-/vSegvfnV5XCtu5WImzDAbpXztMYEctVAb3pzc/hUD3ZEfuEIvaYbYljgYEphD++lTOozxTV9DXEfl7MQtWCiw==}
@@ -3301,6 +3321,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-fxaa@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-sKi7yKUSue2coV/O63RaUUr1vLiJjG2xt3iw7v6oP6xZK87q564Y/z5p6NiyxK7L6/fT9SsgpHr79cOE24c3yw==}
@@ -3308,6 +3329,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/filter-noise@7.2.1(@pixi/core@7.2.1):
     resolution: {integrity: sha512-wgDwBGxWhrNW55oJr/OSO/hfcEo6h/k2OTe+82ytUCywONNwFUsY5Z+wO1U4qwHVufMwm2CCMGQdcYYn/BPNfQ==}
@@ -3315,6 +3337,7 @@ packages:
       '@pixi/core': 7.2.1
     dependencies:
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/graphics@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-4smA746hs4aUcgCR9QDa5k8/fXgT/lo+0QG0BGr86OdIspt6UpvtAQ5bO09tFUWeTtXbOGmBnRKIgIjMgqJe8A==}
@@ -3326,9 +3349,11 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/math@7.2.1:
     resolution: {integrity: sha512-hKML8rG6iWYutAczy5Fo5zssWU7JNThHo9QhBYQbcR7ijPO00Xz/phTHpbu5l2KGwGN0Umpn1zYiogiJ82WPTA==}
+    dev: false
 
   /@pixi/mesh-extras@7.2.1(@pixi/core@7.2.1)(@pixi/mesh@7.2.1):
     resolution: {integrity: sha512-kc744QIpI92iCUV+UCQFNiUwX1BPmJvuYnolSfHHI9B3edGVgMgxQyBuNA2YUUC1i1JAE3aFLXvPVf1hVMbMNA==}
@@ -3338,6 +3363,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/mesh': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/mesh@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1):
     resolution: {integrity: sha512-IpyCZoVIc78X+aflPST3CNHPovKzmsDIIigqeQrUnIrSldA/W9TdbW3z5v6bOB2fQmy+cfYXfN2pO+EvDiqviA==}
@@ -3347,6 +3373,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
+    dev: false
 
   /@pixi/mixin-cache-as-bitmap@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-C6d1y1GkvHtrE8hRtBqvyMek6g8V6p9h63EgQ3oMBxY1zhKCRdka1bsjvn6nK07ScVL23bYW1r8IkQhrPRmUoA==}
@@ -3358,6 +3385,7 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/mixin-get-child-by-name@7.2.1(@pixi/display@7.2.1):
     resolution: {integrity: sha512-xFr2/iDN4Q0lWEZI8MvqTRD1o5V7Rvh0/aF7Id5HiJeeh2QeYxPIDFleZ0Wy5U+3u65ddHgG2lCYveDkLyy6TA==}
@@ -3365,6 +3393,7 @@ packages:
       '@pixi/display': 7.2.1
     dependencies:
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
+    dev: false
 
   /@pixi/mixin-get-global-position@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1):
     resolution: {integrity: sha512-+yG5iUgoPT0y2LjxTPwaa33Sb/CdmhC+15z7qVMjzDMRSRIss1JeF8k8aAepXBHNQUDv0eLOy3je1M3fHUhJaw==}
@@ -3374,6 +3403,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
+    dev: false
 
   /@pixi/particle-container@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-aR1ieuB1kmOT4nidIO6oy08g/T/fvKYoDdSzONVz7qCPksNBHVIJBa/D3ig7VukiAwsJm+k12yOzKM7oku2pmg==}
@@ -3385,6 +3415,7 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/prepare@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/graphics@7.2.1)(@pixi/text@7.2.1):
     resolution: {integrity: sha512-Tc06tQFJdRntI7pOV8UFUpbBLOEiuiIpfFHVTdadCECuO5IR1JT3OIEcAF+2OYws1+QVPYxafB1TMw5ZIuanHg==}
@@ -3398,9 +3429,11 @@ packages:
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/graphics': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1)
       '@pixi/text': 7.2.1(@pixi/core@7.2.1)(@pixi/sprite@7.2.1)
+    dev: false
 
   /@pixi/runner@7.2.1:
     resolution: {integrity: sha512-sGfUdqKY7KIEhKdTT5gOLT7n1y/Satr9GJpDjVyQCABqDPvGTRynmAhVDEjiVaw9pd4Q2P5ZEJZm0+VVJOVirw==}
+    dev: false
 
   /@pixi/settings@7.2.1:
     resolution: {integrity: sha512-PmDMnJWFowbC5qgRWS+AtpuXpSCmoo22NlfGL5Rfu1Hz7c6U3+XFhMrQTdBGw7HITI/8wkRhD23+A4eMU0ALZg==}
@@ -3408,6 +3441,7 @@ packages:
       '@pixi/constants': 7.2.1
       '@types/css-font-loading-module': 0.0.7
       ismobilejs: 1.1.1
+    dev: false
 
   /@pixi/sprite-animated@7.2.1(@pixi/core@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-dm5JeiVplpx5ibClw4VtBu/XbOSxGV9GGSa2wAX8awXSU+a8MKudqP+w0D2a96LNbkOr8wxfueelybSNRFbJTw==}
@@ -3417,6 +3451,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/sprite-tiling@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-1CTLkN9HNIcT1TiMarEcJa6B2JXX3Cr3me8DfLR96A1H4AZXSL3aNdYpZ1Asc3Ktx7lDNDMuAtTN78ksaI0oLw==}
@@ -3428,6 +3463,7 @@ packages:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/sprite@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1):
     resolution: {integrity: sha512-roo4R15Z9ilR+oKJkoepJGMUY31lWIW2iFiR+HlfMbUgvxeTzpbRi9gRMCSIAisow8Jz8UEY7nkQmdB83uDv1g==}
@@ -3437,6 +3473,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
+    dev: false
 
   /@pixi/spritesheet@7.2.1(@pixi/assets@7.2.1)(@pixi/core@7.2.1):
     resolution: {integrity: sha512-cUa+k0kk9K+9H5e/HH8+7E5b2MbgrqYgAylW4H/7BTVoBRreofVamltjtWtFr5m8M7oHDwuNFtPEci1ppLV3vQ==}
@@ -3446,6 +3483,7 @@ packages:
     dependencies:
       '@pixi/assets': 7.2.1(@pixi/core@7.2.1)(@pixi/utils@7.2.1)
       '@pixi/core': 7.2.1
+    dev: false
 
   /@pixi/text-bitmap@7.2.1(@pixi/assets@7.2.1)(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/mesh@7.2.1)(@pixi/text@7.2.1):
     resolution: {integrity: sha512-C9i1+kgyKmcoMqK401BsQh8CuN4u78gYDLO9EdUQgReOi31qXKbTohA5ffjdPwgkqU8VYIvXbbUqDA+/mwX56g==}
@@ -3461,6 +3499,7 @@ packages:
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/mesh': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
       '@pixi/text': 7.2.1(@pixi/core@7.2.1)(@pixi/sprite@7.2.1)
+    dev: false
 
   /@pixi/text-html@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1)(@pixi/text@7.2.1):
     resolution: {integrity: sha512-iXY73/0LGr27q5hJuOaBCWM2yvY/0xKfbSE/m7dohhF+p8gFzxbi/Z+lVk/dQyc2KtSl3KboML/hTuxsnVCQPw==}
@@ -3474,6 +3513,7 @@ packages:
       '@pixi/display': 7.2.1(@pixi/core@7.2.1)
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
       '@pixi/text': 7.2.1(@pixi/core@7.2.1)(@pixi/sprite@7.2.1)
+    dev: false
 
   /@pixi/text@7.2.1(@pixi/core@7.2.1)(@pixi/sprite@7.2.1):
     resolution: {integrity: sha512-keshF4UswmyQzgjWN7cFxxqY+yoabtpu9UCWkW6XtfMLBAlKRyjUpx7tmBUGTFrHoDEek77iR9+XCYoFE7U/VQ==}
@@ -3483,6 +3523,7 @@ packages:
     dependencies:
       '@pixi/core': 7.2.1
       '@pixi/sprite': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)
+    dev: false
 
   /@pixi/ticker@7.2.1:
     resolution: {integrity: sha512-fDnaSASY55FtMggGiLxu8AR26i4MyWo/5jpuhsK5zMTKGMFbk6L3C+BwYxWPADMppbdeNdeZj+L6dABSXdKLZQ==}
@@ -3490,6 +3531,7 @@ packages:
       '@pixi/extensions': 7.2.1
       '@pixi/settings': 7.2.1
       '@pixi/utils': 7.2.1
+    dev: false
 
   /@pixi/utils@7.2.1:
     resolution: {integrity: sha512-oslw8PWwCrbrd8GeIa9DP9cEpDebWekIzIPyEVlmua+PGYu7x/P5lGGhSeLpNXarnbW9wva4SQmLF3kwWTmaRw==}
@@ -3501,6 +3543,7 @@ packages:
       earcut: 2.2.4
       eventemitter3: 4.0.7
       url: 0.11.0
+    dev: false
 
   /@popperjs/core@2.11.6:
     resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
@@ -3517,22 +3560,22 @@ packages:
       classcat: 5.0.4
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/background@11.2.0(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/background@11.2.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-Fd8Few2JsLuE/2GaIM6fkxEBaAJvfzi2Lc106HKi/ddX+dZs8NUsSwMsJy1Ajs8b4GbiX8v8axfKpbK6qFMV8w==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/core': 11.7.0(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       classcat: 5.0.4
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
@@ -3552,13 +3595,13 @@ packages:
       - immer
     dev: false
 
-  /@reactflow/controls@11.1.11(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/controls@11.1.11(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-g6WrsszhNkQjzkJ9HbVUBkGGoUy2z8dQVgH6CYQEjuoonD15cWAPGvjyg8vx8oGG7CuktUhWu5JPivL6qjECow==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/core': 11.7.0(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       classcat: 5.0.4
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -3582,12 +3625,12 @@ packages:
       d3-zoom: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/core@11.6.1(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/core@11.6.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-qKmXi9z1n6aycAGv+GYCNHgsGznCAyY73DsvOvPH99V/+2M3FtpOnQf6Am5HHfDsCk+h5vuRiT6hlv7Oi2Xn0w==}
     peerDependencies:
       react: '>=17'
@@ -3603,12 +3646,12 @@ packages:
       d3-zoom: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/core@11.7.0(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/core@11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-UJcpbNRSupSSoMWh5UmRp6UUr0ug7xVKmMvadnkKKiNi9584q57nz4HMfkqwN3/ESbre7LD043yh2n678d/5FQ==}
     peerDependencies:
       react: '>=17'
@@ -3624,7 +3667,7 @@ packages:
       d3-zoom: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
@@ -3644,18 +3687,18 @@ packages:
       d3-zoom: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/minimap@11.5.0(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/minimap@11.5.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-n/3tlaknLpi3zaqCC+tDDPvUTOjd6jglto9V3RB1F2wlaUEbCwmuoR2GYTkiRyZMvuskKyAoQW8+0DX0+cWwsA==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/core': 11.7.0(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       '@types/d3-selection': 3.0.5
       '@types/d3-zoom': 3.0.2
       classcat: 5.0.4
@@ -3663,24 +3706,24 @@ packages:
       d3-zoom: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/node-resizer@2.1.0(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/node-resizer@2.1.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-DVL8nnWsltP8/iANadAcTaDB4wsEkx2mOLlBEPNE3yc5loSm3u9l5m4enXRcBym61MiMuTtDPzZMyYYQUjuYIg==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/core': 11.6.1(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.6.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       classcat: 5.0.4
       d3-drag: 3.0.0
       d3-selection: 3.0.0
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
@@ -3696,22 +3739,22 @@ packages:
       classcat: 5.0.4
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
 
-  /@reactflow/node-toolbar@1.1.11(react-dom@18.2.0)(react@18.2.0):
+  /@reactflow/node-toolbar@1.1.11(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-+hKtx+cvXwfCa9paGxE+G34rWRIIVEh68ZOqAtivClVmfqGzH/sEoGWtIOUyg9OEDNE1nEmZ1NrnpBGSmHHXFg==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/core': 11.7.0(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       classcat: 5.0.4
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
-      zustand: 4.3.6(react@18.2.0)
+      zustand: 4.3.6(immer@10.0.2)(react@18.2.0)
     transitivePeerDependencies:
       - immer
     dev: false
@@ -3821,8 +3864,8 @@ packages:
     resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==}
     dev: true
 
-  /@storybook/addon-actions@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-WxsnSjAvdf6NhUfTqcwV+FJmsJV56gh2cY4QnGfqfwO5zoBWTUYnhz57TgxSMhJY0kspyX9Q1Kc//r1d5lt1qA==}
+  /@storybook/addon-actions@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-qEvhnGeFb9c2TXdSgcCm+LQsZC+8yj1xXv+xfXu/maEcf3DoFU7iF4pBQJRsmawLP+m/yNaXujUbg/aty4fSng==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -3832,14 +3875,14 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.7
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
       dequal: 2.0.3
       lodash: 4.17.21
       polished: 4.2.2
@@ -3849,11 +3892,11 @@ packages:
       react-inspector: 6.0.1(react@18.2.0)
       telejson: 7.0.4
       ts-dedent: 2.2.0
-      uuid: 9.0.0
+      uuid-browser: 3.1.0
     dev: true
 
-  /@storybook/addon-backgrounds@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-DhT32K1+ti7MXY9oqt36b9jlg7iY68IP0ZQbR3gjShcsIXZpFqh18TQo0vwDY1ldqnBvkTk6Jd5vcxA8tfyshw==}
+  /@storybook/addon-backgrounds@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-vjuPvgZjM1IFVCMSvbrAPO0piY+xgzh5433JqZuYGnIPOtqLuRpq1/xE7aSMNKC7bXIczukydo184p+rfqUUgw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -3863,22 +3906,22 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.7
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
       memoizerific: 1.11.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/addon-controls@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-/QEzleKoWRQ3i7KB32QvqDGcGMw4kG2BxEf0d+ymxd2SjoeL6kX2eHE0b4OxFPXiWUyTfXBFwmcI2Re3fRUJnQ==}
+  /@storybook/addon-controls@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-dV7ljOjZsxtPJ4jlzurnEOQ15opPelKmcEAN6Tl0Id4gW0ouAkb7f++/TfSeI9+BDd0+JPvsw6w3SCCD0t+46A==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -3888,15 +3931,15 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/blocks': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.7
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/node-logger': 7.0.7
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.7
+      '@storybook/blocks': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.0-rc.5
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/node-logger': 7.0.0-rc.5
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
       lodash: 4.17.21
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -3905,29 +3948,33 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-docs@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-5PT7aiTD6QPH+4CZLcv4PiUgWucD9JNGHVMRbQMEyFW6qbs87dHmu1m1uXIvx3BF5h3mTo4FHNAf8IQIq5HH9w==}
+  /@storybook/addon-docs@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-MhxJmZ/Pxi57+SGhpKhrNMxeP4Bj5UM1dmHYk49cwOZBIG0NuWr8lOvMvL8tRjvq7u0jHKqNSa0reSPCBZvvxg==}
     peerDependencies:
+      '@storybook/mdx1-csf': '>=1.0.0-0'
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@storybook/mdx1-csf':
+        optional: true
     dependencies:
       '@babel/core': 7.21.3
       '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3)
       '@jest/transform': 29.5.0
       '@mdx-js/react': 2.3.0(react@18.2.0)
-      '@storybook/blocks': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/csf-plugin': 7.0.7
-      '@storybook/csf-tools': 7.0.7
+      '@storybook/blocks': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/csf-plugin': 7.0.0-rc.5
+      '@storybook/csf-tools': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/mdx2-csf': 1.0.0
-      '@storybook/node-logger': 7.0.7
-      '@storybook/postinstall': 7.0.7
-      '@storybook/preview-api': 7.0.7
-      '@storybook/react-dom-shim': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.7
+      '@storybook/mdx2-csf': 1.1.0-next.1
+      '@storybook/node-logger': 7.0.0-rc.5
+      '@storybook/postinstall': 7.0.0-rc.5
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/react-dom-shim': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
       fs-extra: 11.1.1
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
@@ -3938,42 +3985,43 @@ packages:
       - supports-color
     dev: true
 
-  /@storybook/addon-essentials@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-uNx0BvN1XP7cNnk/L4oiFQlEB/KABqOeIyI8/mhfIyTvvwo9uAYIQAyiwWuz9MFmofCNm7CgLNOUaEwNDkM4CA==}
+  /@storybook/addon-essentials@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-dfBOZ5odCzki6F7tMcbX9x6fpuGsz4Owxw5iIL7FUCjxUrlClwTMpvwqqZniHPFAEWnISLcKgkPX7D7oRrWCig==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/addon-actions': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-backgrounds': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-controls': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-docs': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-highlight': 7.0.7
-      '@storybook/addon-measure': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-outline': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-toolbars': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/addon-viewport': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.7
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/node-logger': 7.0.7
-      '@storybook/preview-api': 7.0.7
+      '@storybook/addon-actions': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-backgrounds': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-controls': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-docs': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-highlight': 7.0.0-rc.5
+      '@storybook/addon-measure': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-outline': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-toolbars': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/addon-viewport': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.0-rc.5
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/node-logger': 7.0.0-rc.5
+      '@storybook/preview-api': 7.0.0-rc.5
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
     transitivePeerDependencies:
+      - '@storybook/mdx1-csf'
       - supports-color
     dev: true
 
-  /@storybook/addon-highlight@7.0.7:
-    resolution: {integrity: sha512-expme2GzzCXX7/lL7UjCDi1Tfj+4LeNsAdWiurVLH7glK7yKPPeXXkIldbLP/XjJv4NKlqCwnNRHQx0vDLlE6g==}
+  /@storybook/addon-highlight@7.0.0-rc.5:
+    resolution: {integrity: sha512-Dx4xObuDMQHJ/Et83HuzXI1g4LDJmw36Zgke09wdNta7CbvJG3eyDyiA+JrHRs+4eXYi1IWDhztpM5uQ/Chtaw==}
     dependencies:
-      '@storybook/core-events': 7.0.7
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/preview-api': 7.0.7
+      '@storybook/preview-api': 7.0.0-rc.5
     dev: true
 
-  /@storybook/addon-interactions@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-jBl6O5sSbix0X1G9dFuWvvu4qefgLP9dAB/utVdDadZxlbPfa5B2C2q2YIqjcKZoX8DS8Fh8SUhlX1mdW5tu5w==}
+  /@storybook/addon-interactions@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-OPAp+0LS+vtFcBvfrY+5/xFyXfihLCWJauFmMI02g0tsHObB4Ua6juAnOYSwNSKdea0uW5GGTkVRxS7zEgqr3Q==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -3983,16 +4031,16 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-common': 7.0.7
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-common': 7.0.0-rc.5
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/instrumenter': 7.0.7
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.0.7
+      '@storybook/instrumenter': 7.0.0-rc.5
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
       jest-mock: 27.5.1
       polished: 4.2.2
       react: 18.2.0
@@ -4027,8 +4075,8 @@ packages:
       ts-dedent: 2.2.0
     dev: true
 
-  /@storybook/addon-measure@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-lb4wEIvIVF+ePx1sC+n9rDI0+49sRa6MWbcvZ+BhbAoCeGcX7uACQFdW6HyXolmBuZASsTnzVQ4KqzzvY1dSWw==}
+  /@storybook/addon-measure@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-m9CCcMpSrV7psZ9z6FaekdY0m7XNh+XRpiLLWn/TwQONHrUb0UBQGKloITNKE4QxCSDKpqCOUl/yJTxkCRCsrg==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4038,19 +4086,19 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/types': 7.0.7
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/types': 7.0.0-rc.5
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/addon-outline@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-AxbNZ4N1fXBTeMYM9tFudfW+Gzq7UikCjPxn5ax3Pde+zZjaEMppUxv5EMz4g5GIJupLYRmKH5pN0YcYoRLY6w==}
+  /@storybook/addon-outline@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-CkwW6b9gzIqQFw68cdAYbaY15DzLhBSpCRsccl/Mnm83xxm2MeC3Z5yxvi+3fGyuV6iyJxDsyxn4y4MD/Zho9w==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4060,13 +4108,13 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/types': 7.0.7
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/types': 7.0.0-rc.5
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       ts-dedent: 2.2.0
@@ -4143,8 +4191,8 @@ packages:
       - webpack
     dev: true
 
-  /@storybook/addon-toolbars@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-/NkYHhU1VAz5lXjWuV8+ADWB84HzktvZv4jfiKX7Zzu6JVzrBu7FotQSWh3pDqqVwCB50RClUGtcHmSSac9CAQ==}
+  /@storybook/addon-toolbars@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-GErLxEBVh3HaQEvUNmKlNDcNuEYpGNVT1Nr1Tsc4J8EKG1ivEfQfVu6/5fduPZE8Vt1IUAzrVEp9NYzSELH49Q==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4154,17 +4202,17 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/addon-viewport@7.0.7(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-znqhd8JFEFoXcAdwYhz1CwrCpVAzhuSyUVBUNDsDs+mgBEfGth4D4abIdWWGcfP6+CmI5ebFHtk443cExZebag==}
+  /@storybook/addon-viewport@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-EzjGyi0s6VvwZvCuN6E8zgc6RcIOUz85G1Zt5U59as4GwhvezwiJdM9IjtX0/I17hdKS7vL36Gli67PJZKb/Bw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -4174,13 +4222,13 @@ packages:
       react-dom:
         optional: true
     dependencies:
-      '@storybook/client-logger': 7.0.7
-      '@storybook/components': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/core-events': 7.0.7
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
       '@storybook/global': 5.0.0
-      '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/preview-api': 7.0.7
-      '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
       memoizerific: 1.11.3
       prop-types: 15.8.1
       react: 18.2.0
@@ -4252,6 +4300,40 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
+  /@storybook/blocks@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-9ZGDExwA6DgR/BsFSk2aCe7p/AIIQAiCemV1W1Djp7lt6OOALWfLZ7r1sFUqY9ZgNkfD1N41JpmqJtPDLXejGQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@storybook/channels': 7.0.0-rc.5
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/components': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/core-events': 7.0.0-rc.5
+      '@storybook/csf': 0.0.2-next.11
+      '@storybook/docs-tools': 7.0.0-rc.5
+      '@storybook/global': 5.0.0
+      '@storybook/manager-api': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/preview-api': 7.0.0-rc.5
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
+      '@types/lodash': 4.14.191
+      color-convert: 2.0.1
+      dequal: 2.0.3
+      lodash: 4.17.21
+      markdown-to-jsx: 7.2.0(react@18.2.0)
+      memoizerific: 1.11.3
+      polished: 4.2.2
+      react: 18.2.0
+      react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0)
+      react-dom: 18.2.0(react@18.2.0)
+      telejson: 7.0.4
+      ts-dedent: 2.2.0
+      util-deprecate: 1.0.2
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@storybook/blocks@7.0.7(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-ehR0hAFWNHHqmrmbwYPKhLpgbIBKtyMbeoGClTRSnrVBGONciYJdmxegkCTReUklCY+HBJjtlwNowT+7+5sSaw==}
     peerDependencies:
@@ -4513,6 +4595,24 @@ packages:
       util-deprecate: 1.0.2
     dev: true
 
+  /@storybook/components@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-zuKQ0+uOtRbmnF0trJ4LpWZ5w9Dzcs5dZjF3Uu4ka4F4vJ/fUWKL2spxAIsRalu2jyk2XVp6/mz/NiWQnrophw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/csf': 0.0.2-next.11
+      '@storybook/global': 5.0.0
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
+      memoizerific: 1.11.3
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0)
+      util-deprecate: 1.0.2
+    dev: true
+
   /@storybook/components@7.0.5(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-SHftxNH3FG3RZwJ5nbyBZwn5pkI3Ei2xjD7zDwxztI8bCp5hPnOTDwAnQZZCkeW7atSQUe7xFkYqlCgNmXR4PQ==}
     peerDependencies:
@@ -4682,6 +4782,15 @@ packages:
       - utf-8-validate
     dev: true
 
+  /@storybook/csf-plugin@7.0.0-rc.5:
+    resolution: {integrity: sha512-sgIEqV1MfhybvODcjtG0Ce/XlzWv2Sg5Prg5Qqsr5sMU7aET+yLHmr1umbM5L8ieRjsXS4CsxZCqZMrY9hDdNw==}
+    dependencies:
+      '@storybook/csf-tools': 7.0.0-rc.5
+      unplugin: 0.10.2
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@storybook/csf-plugin@7.0.7:
     resolution: {integrity: sha512-uhf2g077gXA6ZEMXIPQ0RnX+IoOTBJbj+6+VQfT7K5tvJeop1z0Fvk0FoknNXcUe7aUA0nzA/cUQ1v4vXqbY3Q==}
     dependencies:
@@ -4691,6 +4800,22 @@ packages:
       - supports-color
     dev: true
 
+  /@storybook/csf-tools@7.0.0-rc.5:
+    resolution: {integrity: sha512-DvcAygIZMZIL30j7WxMXeJ6a+A2/Y/FuatZItmW+3sNv0FK1J9wH2SKw7QjzEw75LsgjvO07lU2cgcsPDFhXoA==}
+    dependencies:
+      '@babel/generator': 7.21.3
+      '@babel/parser': 7.21.3
+      '@babel/traverse': 7.21.3(supports-color@5.5.0)
+      '@babel/types': 7.21.4
+      '@storybook/csf': 0.0.2-next.11
+      '@storybook/types': 7.0.0-rc.5
+      fs-extra: 11.1.1
+      recast: 0.23.1
+      ts-dedent: 2.2.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@storybook/csf-tools@7.0.7:
     resolution: {integrity: sha512-KbO5K2RS0oFm94eR49bAPvoyXY3Q6+ozvBek/F05RP7iAV790icQc59Xci9YDM1ONgb3afS+gSJGFBsE0h4pmg==}
     dependencies:
@@ -4761,6 +4886,16 @@ packages:
     resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
     dev: true
 
+  /@storybook/instrumenter@7.0.0-rc.5:
+    resolution: {integrity: sha512-e9AtV1hNTs4ppmqKfst/cInmRnhkK9VcGf3xB/d9Qqm0Sqo+sNXu6ywK5KpAURdCzsUEOPXbJ9H52yTrU4f74A==}
+    dependencies:
+      '@storybook/channels': 7.0.0-rc.5
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/core-events': 7.0.0-rc.5
+      '@storybook/global': 5.0.0
+      '@storybook/preview-api': 7.0.0-rc.5
+    dev: true
+
   /@storybook/instrumenter@7.0.7:
     resolution: {integrity: sha512-0zE5lM3laKvCT4GW/XKKw8kakvI4catqK8PObZolRhfxbtGufW4VJZ2E8vXLtgA/+K3zikypjuWE6d45NLbh9w==}
     dependencies:
@@ -4771,6 +4906,31 @@ packages:
       '@storybook/preview-api': 7.0.7
     dev: true
 
+  /@storybook/manager-api@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-MsNj/cPIOlL7HJ8ReYahUvJVfvZDtNfacUYSFuQjQwdnp0u3pbC5mGZPd32tAGj7lLaLzcqqo1yR+NAgwpZUBw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@storybook/channels': 7.0.0-rc.5
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/core-events': 7.0.0-rc.5
+      '@storybook/csf': 0.0.2-next.11
+      '@storybook/global': 5.0.0
+      '@storybook/router': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/theming': 7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.0.0-rc.5
+      dequal: 2.0.3
+      lodash: 4.17.21
+      memoizerific: 1.11.3
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      semver: 7.3.8
+      store2: 2.14.2
+      telejson: 7.0.4
+      ts-dedent: 2.2.0
+    dev: true
+
   /@storybook/manager-api@7.0.7(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-QTd/P72peAhofKqK+8yzIO9iWAEfPn8WUGGveV2KGaTlSlgbr87RLHEKilcXMZcYhBWC9izFRmjKum9ROdskrQ==}
     peerDependencies:
@@ -4804,6 +4964,10 @@ packages:
     resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==}
     dev: true
 
+  /@storybook/mdx2-csf@1.1.0-next.1:
+    resolution: {integrity: sha512-ONvFBZySHsBIkUYGrUM8FCG2tDKf663TIErztPSOghOpmBGyFLjSsXJHkNWiRi4c740PoemLqJd2XZZVlXRVLQ==}
+    dev: true
+
   /@storybook/node-logger@7.0.0-rc.5:
     resolution: {integrity: sha512-3DpM988ndfbwc/03doFVP/HUJgoCp4eKVFMmSqnKVUd6qWx/dhsrTv+jqLt43wNZCgL/N/8QE+Q+FhVwefh6Tg==}
     dependencies:
@@ -4822,8 +4986,8 @@ packages:
       pretty-hrtime: 1.0.3
     dev: true
 
-  /@storybook/postinstall@7.0.7:
-    resolution: {integrity: sha512-APcZ2KaR7z1aJje3pID4Ywmt1/aVcP3Sc4ltzNdH9mCkEsuq0fZHHQrYSa9Ya1IPRmSeLZ5/23q1iyqmGU3zoQ==}
+  /@storybook/postinstall@7.0.0-rc.5:
+    resolution: {integrity: sha512-F23wxKEJ2XoVnHT7oAMjCXtANWvNq7M+FmIowgI98b3FT1dxt9fFPKKY+3Lcqp0Xa6Pzezd03KR9vAxXvvK/iQ==}
     dev: true
 
   /@storybook/preset-scss@1.0.3(css-loader@6.7.3)(sass-loader@13.2.2)(style-loader@3.3.2):
@@ -5022,6 +5186,19 @@ packages:
       regenerator-runtime: 0.13.11
     dev: true
 
+  /@storybook/router@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-s23O2OOQ4+CvySk3QC/PXhDJChc4jjyQu/h3gLMKF7bfWx0bd5KR4LnP3rCKLIMkxoJYFPUayPMgwEEeN/ENSw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@storybook/client-logger': 7.0.0-rc.5
+      memoizerific: 1.11.3
+      qs: 6.11.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
   /@storybook/router@7.0.7(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-/lM8/NHQKeshfnC3ayFuO8Y9TCSHnCAPRhIsVxvanBzcj+ILbCIyZ+TspvB3hT4MbX/Ez+JR8VrMbjXIGwmH8w==}
     peerDependencies:
@@ -5085,6 +5262,20 @@ packages:
       regenerator-runtime: 0.13.11
     dev: true
 
+  /@storybook/theming@7.0.0-rc.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-OzwybDA2+4FWg85tcTNQkVI0JnHkwCRG9HM1qx9hOZJHNRfxmJFjJePOnBoXM6CjVlz0S1PJUwCmMHNH8OTvEw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0)
+      '@storybook/client-logger': 7.0.0-rc.5
+      '@storybook/global': 5.0.0
+      memoizerific: 1.11.3
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
   /@storybook/theming@7.0.5(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-XgQXKktcVBOkJT5gXjqtjH7C2pjdreDy0BTVTaEmFzggyyw+cgFrkJ7tuB27oKwYe+svx26c/olVMSHYf+KqhA==}
     peerDependencies:
@@ -5441,6 +5632,7 @@ packages:
 
   /@types/css-font-loading-module@0.0.7:
     resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
+    dev: false
 
   /@types/cytoscape@3.19.9:
     resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==}
@@ -5604,6 +5796,7 @@ packages:
 
   /@types/earcut@2.1.1:
     resolution: {integrity: sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==}
+    dev: false
 
   /@types/ejs@3.1.2:
     resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==}
@@ -5765,20 +5958,12 @@ packages:
 
   /@types/offscreencanvas@2019.7.0:
     resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==}
+    dev: false
 
   /@types/parse-json@4.0.0:
     resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
     dev: false
 
-  /@types/pixi.js@5.0.0(@pixi/utils@7.2.1):
-    resolution: {integrity: sha512-yZqQBR043lRBlBZci2cx6hgmX0fvBfYIqFm6VThlnueXEjitxd3coy+BGsqsZ7+ary7O//+ks4aJRhC5MJoHqA==}
-    deprecated: This is a stub types definition. pixi.js provides its own type definitions, so you do not need this installed.
-    dependencies:
-      pixi.js: 7.2.1(@pixi/utils@7.2.1)
-    transitivePeerDependencies:
-      - '@pixi/utils'
-    dev: true
-
   /@types/pretty-hrtime@1.0.1:
     resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==}
     dev: true
@@ -7075,6 +7260,7 @@ packages:
 
   /colord@2.9.3:
     resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+    dev: false
 
   /colorette@2.0.19:
     resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
@@ -7146,7 +7332,7 @@ packages:
     dev: true
 
   /concat-map@0.0.1:
-    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+    resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
 
   /concat-stream@1.6.2:
     resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
@@ -8016,6 +8202,7 @@ packages:
 
   /earcut@2.2.4:
     resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
+    dev: false
 
   /eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -8329,13 +8516,13 @@ packages:
     dependencies:
       eslint: 7.32.0
 
-  /eslint-config-turbo@1.9.3(eslint@7.32.0):
-    resolution: {integrity: sha512-QG6jxFQkrGSpQqlFKefPdtgUfr20EbU0s4tGGIuGFOcPuJEdsY6VYZpZUxNJvmMcTGqPgMyOPjAFBKhy/DPHLA==}
+  /eslint-config-turbo@1.10.0(eslint@7.32.0):
+    resolution: {integrity: sha512-iJsjoOb0vjnpV65rL+SAOIgbOluEibFqo/ke4UzXA9bi13cNJOT9k6FCQA7RXBHwChz+6BzYq5ZyoE3WcUg+0g==}
     peerDependencies:
       eslint: '>6.6.0'
     dependencies:
       eslint: 7.32.0
-      eslint-plugin-turbo: 1.9.3(eslint@7.32.0)
+      eslint-plugin-turbo: 1.10.0(eslint@7.32.0)
 
   /eslint-import-resolver-node@0.3.7:
     resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
@@ -8478,8 +8665,8 @@ packages:
       semver: 6.3.0
       string.prototype.matchall: 4.0.8
 
-  /eslint-plugin-turbo@1.9.3(eslint@7.32.0):
-    resolution: {integrity: sha512-ZsRtksdzk3v+z5/I/K4E50E4lfZ7oYmLX395gkrUMBz4/spJlYbr+GC8hP9oVNLj9s5Pvnm9rLv/zoj5PVYaVw==}
+  /eslint-plugin-turbo@1.10.0(eslint@7.32.0):
+    resolution: {integrity: sha512-7eDeOjZDx/GcrZj4XYX1puaxsDtm6FE53qlbb4JyJhhkKC8OVYTJHTpu8kSmFTNsVoIgDFo/SEUIMwKHLYRM3w==}
     peerDependencies:
       eslint: '>6.6.0'
     dependencies:
@@ -8627,6 +8814,7 @@ packages:
 
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+    dev: false
 
   /events@3.3.0:
     resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
@@ -9551,6 +9739,10 @@ packages:
     dev: true
     optional: true
 
+  /immer@10.0.2:
+    resolution: {integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==}
+    dev: false
+
   /immer@9.0.19:
     resolution: {integrity: sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==}
     dev: false
@@ -9869,6 +10061,7 @@ packages:
 
   /ismobilejs@1.1.1:
     resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==}
+    dev: false
 
   /isobject@3.0.1:
     resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
@@ -10491,6 +10684,10 @@ packages:
     engines: {node: '>= 0.6'}
     dev: true
 
+  /memoize-one@5.2.1:
+    resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+    dev: false
+
   /memoizerific@1.11.3:
     resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
     dependencies:
@@ -11211,6 +11408,7 @@ packages:
       '@pixi/text-html': 7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/sprite@7.2.1)(@pixi/text@7.2.1)
     transitivePeerDependencies:
       - '@pixi/utils'
+    dev: false
 
   /pkg-dir@3.0.0:
     resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
@@ -11548,6 +11746,7 @@ packages:
 
   /punycode@1.3.2:
     resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==}
+    dev: false
 
   /punycode@2.3.0:
     resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
@@ -11600,6 +11799,7 @@ packages:
     resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
     engines: {node: '>=0.4.x'}
     deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
+    dev: false
 
   /querystringify@2.2.0:
     resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
@@ -11931,6 +12131,19 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
+  /react-window@1.8.9(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==}
+    engines: {node: '>8.0.0'}
+    peerDependencies:
+      react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@babel/runtime': 7.21.0
+      memoize-one: 5.2.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
   /react@18.2.0:
     resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
     engines: {node: '>=0.10.0'}
@@ -11954,18 +12167,18 @@ packages:
       - immer
     dev: false
 
-  /reactflow@11.7.0(react-dom@18.2.0)(react@18.2.0):
+  /reactflow@11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-bjfJV1iQZ+VwIlvsnd4TbXNs6kuJ5ONscud6fNkF8RY/oU2VUZpdjA3q1zwmgnjmJcIhxuBiBI5VLGajYx/Ozg==}
     peerDependencies:
       react: '>=17'
       react-dom: '>=17'
     dependencies:
-      '@reactflow/background': 11.2.0(react-dom@18.2.0)(react@18.2.0)
-      '@reactflow/controls': 11.1.11(react-dom@18.2.0)(react@18.2.0)
-      '@reactflow/core': 11.7.0(react-dom@18.2.0)(react@18.2.0)
-      '@reactflow/minimap': 11.5.0(react-dom@18.2.0)(react@18.2.0)
-      '@reactflow/node-resizer': 2.1.0(react-dom@18.2.0)(react@18.2.0)
-      '@reactflow/node-toolbar': 1.1.11(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/background': 11.2.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/controls': 11.1.11(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/core': 11.7.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/minimap': 11.5.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/node-resizer': 2.1.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
+      '@reactflow/node-toolbar': 1.1.11(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
     transitivePeerDependencies:
@@ -13255,65 +13468,65 @@ packages:
       tslib: 1.14.1
       typescript: 4.9.5
 
-  /turbo-darwin-64@1.9.3:
-    resolution: {integrity: sha512-0dFc2cWXl82kRE4Z+QqPHhbEFEpUZho1msHXHWbz5+PqLxn8FY0lEVOHkq5tgKNNEd5KnGyj33gC/bHhpZOk5g==}
+  /turbo-darwin-64@1.9.9:
+    resolution: {integrity: sha512-UDGM9E21eCDzF5t1F4rzrjwWutcup33e7ZjNJcW/mJDPorazZzqXGKEPIy9kXwKhamUUXfC7668r6ZuA1WXF2Q==}
     cpu: [x64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo-darwin-arm64@1.9.3:
-    resolution: {integrity: sha512-1cYbjqLBA2zYE1nbf/qVnEkrHa4PkJJbLo7hnuMuGM0bPzh4+AnTNe98gELhqI1mkTWBu/XAEeF5u6dgz0jLNA==}
+  /turbo-darwin-arm64@1.9.9:
+    resolution: {integrity: sha512-VyfkXzTJpYLTAQ9krq2myyEq7RPObilpS04lgJ4OO1piq76RNmSpX9F/t9JCaY9Pj/4TL7i0d8PM7NGhwEA5Ag==}
     cpu: [arm64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo-linux-64@1.9.3:
-    resolution: {integrity: sha512-UuBPFefawEwpuxh5pM9Jqq3q4C8M0vYxVYlB3qea/nHQ80pxYq7ZcaLGEpb10SGnr3oMUUs1zZvkXWDNKCJb8Q==}
+  /turbo-linux-64@1.9.9:
+    resolution: {integrity: sha512-Fu1MY29Odg8dHOqXcpIIGC3T63XLOGgnGfbobXMKdrC7JQDvtJv8TUCYciRsyknZYjyyKK1z6zKuYIiDjf3KeQ==}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo-linux-arm64@1.9.3:
-    resolution: {integrity: sha512-vUrNGa3hyDtRh9W0MkO+l1dzP8Co2gKnOVmlJQW0hdpOlWlIh22nHNGGlICg+xFa2f9j4PbQlWTsc22c019s8Q==}
+  /turbo-linux-arm64@1.9.9:
+    resolution: {integrity: sha512-50LI8NafPuJxdnMCBeDdzgyt1cgjQG7FwkyY336v4e95WJPUVjrHdrKH6jYXhOUyrv9+jCJxwX1Yrg02t5yJ1g==}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo-windows-64@1.9.3:
-    resolution: {integrity: sha512-0BZ7YaHs6r+K4ksqWus1GKK3W45DuDqlmfjm/yuUbTEVc8szmMCs12vugU2Zi5GdrdJSYfoKfEJ/PeegSLIQGQ==}
+  /turbo-windows-64@1.9.9:
+    resolution: {integrity: sha512-9IsTReoLmQl1IRsy3WExe2j2RKWXQyXujfJ4fXF+jp08KxjVF4/tYP2CIRJx/A7UP/7keBta27bZqzAjsmbSTA==}
     cpu: [x64]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo-windows-arm64@1.9.3:
-    resolution: {integrity: sha512-QJUYLSsxdXOsR1TquiOmLdAgtYcQ/RuSRpScGvnZb1hY0oLc7JWU0llkYB81wVtWs469y8H9O0cxbKwCZGR4RQ==}
+  /turbo-windows-arm64@1.9.9:
+    resolution: {integrity: sha512-CUu4hpeQo68JjDr0V0ygTQRLbS+/sNfdqEVV+Xz9136vpKn2WMQLAuUBVZV0Sp0S/7i+zGnplskT0fED+W46wQ==}
     cpu: [arm64]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /turbo@1.9.3:
-    resolution: {integrity: sha512-ID7mxmaLUPKG/hVkp+h0VuucB1U99RPCJD9cEuSEOdIPoSIuomcIClEJtKamUsdPLhLCud+BvapBNnhgh58Nzw==}
+  /turbo@1.9.9:
+    resolution: {integrity: sha512-+ZS66LOT7ahKHxh6XrIdcmf2Yk9mNpAbPEj4iF2cs0cAeaDU3xLVPZFF0HbSho89Uxwhx7b5HBgPbdcjQTwQkg==}
     hasBin: true
     requiresBuild: true
     optionalDependencies:
-      turbo-darwin-64: 1.9.3
-      turbo-darwin-arm64: 1.9.3
-      turbo-linux-64: 1.9.3
-      turbo-linux-arm64: 1.9.3
-      turbo-windows-64: 1.9.3
-      turbo-windows-arm64: 1.9.3
+      turbo-darwin-64: 1.9.9
+      turbo-darwin-arm64: 1.9.9
+      turbo-linux-64: 1.9.9
+      turbo-linux-arm64: 1.9.9
+      turbo-windows-64: 1.9.9
+      turbo-windows-arm64: 1.9.9
     dev: true
 
   /type-check@0.3.2:
@@ -13554,6 +13767,7 @@ packages:
     dependencies:
       punycode: 1.3.2
       querystring: 0.2.0
+    dev: false
 
   /use-composed-ref@1.3.0(react@18.2.0):
     resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==}
@@ -13563,6 +13777,16 @@ packages:
       react: 18.2.0
     dev: false
 
+  /use-immer@0.9.0(immer@10.0.2)(react@18.2.0):
+    resolution: {integrity: sha512-/L+enLi0nvuZ6j4WlyK0US9/ECUtV5v9RUbtxnn5+WbtaXYUaOBoKHDNL9I5AETdurQ4rIFIj/s+Z5X80ATyKw==}
+    peerDependencies:
+      immer: '>=2.0.0'
+      react: ^16.8.0 || ^17.0.1 || ^18.0.0
+    dependencies:
+      immer: 10.0.2
+      react: 18.2.0
+    dev: false
+
   /use-isomorphic-layout-effect@1.1.2(@types/react@18.0.28)(react@18.2.0):
     resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
     peerDependencies:
@@ -13634,9 +13858,9 @@ packages:
     dev: false
     optional: true
 
-  /uuid@9.0.0:
-    resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
-    hasBin: true
+  /uuid-browser@3.1.0:
+    resolution: {integrity: sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==}
+    deprecated: Package no longer supported and required. Use the uuid package or crypto.randomUUID instead
     dev: true
 
   /v8-compile-cache-lib@3.0.1:
@@ -14259,7 +14483,7 @@ packages:
       commander: 9.5.0
     dev: true
 
-  /zustand@4.3.6(react@18.2.0):
+  /zustand@4.3.6(immer@10.0.2)(react@18.2.0):
     resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
     engines: {node: '>=12.7.0'}
     peerDependencies:
@@ -14271,6 +14495,7 @@ packages:
       react:
         optional: true
     dependencies:
+      immer: 10.0.2
       react: 18.2.0
       use-sync-external-store: 1.2.0(react@18.2.0)
     dev: false