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/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
index b60cae901b460fa549491631671748a5b89f0609..1835b16975232ce3a318310ba34bb7c08da85f77 100644
--- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
+++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
@@ -28,6 +28,7 @@ export interface Edge {
   attributes: { [key: string]: unknown };
   from: string;
   to: string;
+  id?: string;
   /* type: string; */
 }
 
@@ -63,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/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/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/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/schema/model/graphology.ts b/libs/shared/lib/schema/model/graphology.ts
index eb516f7255ab562823ddf92ad64c1e20c8baaa7f..e335f845a87d3ae5f810a46b50054cbdc3bffb47 100644
--- a/libs/shared/lib/schema/model/graphology.ts
+++ b/libs/shared/lib/schema/model/graphology.ts
@@ -4,9 +4,10 @@ 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, GAttributes, GAttributes> { };
-export type SchemaGraph = SerializedGraph<SchemaGraphologyNode, GAttributes, GAttributes>;
+export class SchemaGraphology extends MultiGraph<SchemaGraphologyNode, SchemaGraphologyEdge, GAttributes> { };
+export type SchemaGraph = SerializedGraph<SchemaGraphologyNode, SchemaGraphologyEdge, GAttributes>;
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.tsx b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx
index 21e3687a7b4b51d1d391c7ff5dc5bd116aeca715..04ea7bf0c8887aeb015a14a35123cc2a9ba57fac 100644
--- a/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/SemanticSubstratesViewModel.tsx
@@ -19,7 +19,7 @@ import ToPlotDataParserUseCase from './utils/ToPlotDataParserUseCase';
 import CalcXYMinMaxUseCase from './utils/CalcXYMinMaxUseCase';
 
 import { ClassNameMap } from '@mui/material';
-import { NodeLinkResultType, isNodeLinkResult } from '../nodelink/ResultNodeLinkParserUseCase';
+import { NodeLinkResultType, isNodeLinkResult } from '../shared/ResultNodeLinkParserUseCase';
 import CalcEntityAttrNamesFromSchemaUseCase from './utils/CalcEntityAttrNamesFromSchemaUseCase';
 import CalcEntityAttrNamesFromResultUseCase from './utils/CalcEntityAttrNamesFromResultUseCase';
 import { isSchemaResult } from '../shared/SchemaResultType';
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx
index 5c23d9dfb13c01b10bade50d12c23f7ee9eeeab6..4f257a6af7a3753be55996c9f374bc2d77226e4a 100644
--- a/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/SemanticSubstratesConfigPanelViewModel.tsx
@@ -4,7 +4,7 @@ import {
   EntityWithAttributes,
   FSSConfigPanelProps,
 } from './Types';
-import { Link, Node } from '../../nodelink/ResultNodeLinkParserUseCase';
+import { Link, Node } from '../../shared/ResultNodeLinkParserUseCase';
 import SemanticSubstratesViewModel from '../SemanticSubstratesViewModel';
 
 /** Viewmodel for rendering config input fields for Faceted Semantic Substrate attributes. */
diff --git a/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx b/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx
index 7956ba611639ffd0fd78c8540dc6337f7b5f0efa..3a7462d70c02e0b0505ecfe11b0962d9fa7c4a3e 100644
--- a/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/configpanel/Types.tsx
@@ -4,7 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 
-import { NodeLinkResultType } from "../../nodelink/ResultNodeLinkParserUseCase";
+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)
diff --git a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
index 92de4a05f6f11a94ae4f1a65b5c1c93ac0b8c1fb..f5149f537fa7466c34a26b14beb913e7e7a59211 100644
--- a/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/semanticsubstrates.tsx
@@ -4,7 +4,7 @@ import {
 } from '@graphpolaris/shared/lib/data-access/store';
 import { useEffect, useRef, useState } from 'react';
 import { AxisLabel, EntitiesFromSchema, MinMaxType, PlotSpecifications, PlotType, RelationType } from './Types';
-import { NodeLinkResultType, isNodeLinkResult } from '../nodelink/ResultNodeLinkParserUseCase';
+import { NodeLinkResultType, isNodeLinkResult } from '../shared/ResultNodeLinkParserUseCase';
 import styles from './semanticsubstrates.module.scss';
 import AddPlotButtonComponent from './subcomponents/AddPlotButtonComponent';
 import SVGCheckboxesWithSemanticSubstrLabel from './subcomponents/SVGCheckBoxComponent';
@@ -179,8 +179,7 @@ const SemanticSubstrates = () => {
    * @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 {
+  function applyNewPlotSpecifications(): void {
 
     // Parse the incoming data to plotdata with the auto generated plot specifications
     const { plots, relations } = ToPlotDataParserUseCase.parseQueryResult(
diff --git a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx
index 076e744ecf7aabf67d97a8992ef0eca000acbe3a..f53468bc4fe77f355f9d5cbb3dcee2ebf4cdff93 100644
--- a/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/subcomponents/AddPlotPopup.tsx
@@ -8,7 +8,7 @@
 /* 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 '../../nodelink/ResultNodeLinkParserUseCase';
+import { Link, Node } from '../../shared/ResultNodeLinkParserUseCase';
 import { Button, MenuItem, Popover, TextField } from '@mui/material';
 import React, { ReactElement } from 'react';
 import { EntitiesFromSchema } from '../Types';
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx
index 87c1071af31f929269fa822e8d1fbb9484ffbe16..dac2a940f3786c24c4cf4de7660a6d2292f55dd8 100644
--- a/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcDefaultPlotSpecsUseCase.tsx
@@ -4,7 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 
-import { NodeLinkResultType } from "../../nodelink/ResultNodeLinkParserUseCase";
+import { NodeLinkResultType } from "../../shared/ResultNodeLinkParserUseCase";
 import { AxisLabel, PlotSpecifications } from "../Types";
 
 /** UseCase for calculating default plots from node link query result data. */
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx
index 698cdc1592d1e2b0c3df3f5044bfddb2f4c80bd7..7120f0e0395e7ca7f23eaf5a90e7b97e56579af3 100644
--- a/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/CalcEntityAttrNamesFromResultUseCase.tsx
@@ -4,7 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 
-import { NodeLinkResultType } from "../../nodelink/ResultNodeLinkParserUseCase";
+import { NodeLinkResultType } from "../../shared/ResultNodeLinkParserUseCase";
 import { EntitiesFromSchema } from "../Types";
 
 
diff --git a/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx b/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx
index ba1e50404eb104291c86ca190b5d462230370aa6..7c09b475f5987a3d1a02e08a2a2b92788e32fac7 100644
--- a/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx
+++ b/libs/shared/lib/vis/semanticsubstrates/utils/ToPlotDataParserUseCase.tsx
@@ -4,7 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access';
-import { NodeLinkResultType, Node, ParseToUniqueEdges } from '../../nodelink/ResultNodeLinkParserUseCase';
+import { NodeLinkResultType, Node, ParseToUniqueEdges } from '../../shared/ResultNodeLinkParserUseCase';
 import {
   AxisLabel,
   PlotInputData,
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> = {};