From 8556f2b45d36bba6e1cabd724330d8cca61b3197 Mon Sep 17 00:00:00 2001
From: "Behrisch, M. (Michael)" <m.behrisch@uu.nl>
Date: Wed, 14 Feb 2024 17:32:03 +0000
Subject: [PATCH] feat: add Matrix visualization and initial reordering
 functionality

---
 .gitignore                                    |   7 +-
 libs/config/src/colors.js                     |  24 +-
 .../data-access/store/visualizationSlice.ts   |   1 +
 libs/shared/lib/vis/index.tsx                 |   3 +-
 libs/shared/lib/vis/visualizations/index.tsx  |   1 +
 .../lib/vis/visualizations/matrix/Types.tsx   | 102 +++
 .../components/ColumnGraphicsComponent.tsx    |  83 +++
 .../components/ColumnSpriteComponent.tsx      |  48 ++
 .../matrix/components/MatrixExport.tsx        |  35 ++
 .../matrix/components/MatrixPixi.tsx          | 593 ++++++++++++++++++
 .../matrix/components/MatrixPopup.tsx         |  46 ++
 .../matrix/components/ReorderingManager.tsx   | 227 +++++++
 .../visualizations/matrix/matrix.stories.tsx  | 130 ++++
 .../matrix/matrixvis.module.scss              |  71 +++
 .../vis/visualizations/matrix/matrixvis.tsx   |  51 ++
 libs/shared/package.json                      |   5 +-
 pnpm-lock.yaml                                |  28 +
 17 files changed, 1440 insertions(+), 15 deletions(-)
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/Types.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/MatrixExport.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/MatrixPopup.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/matrixvis.module.scss
 create mode 100644 libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx

diff --git a/.gitignore b/.gitignore
index cf4d02514..7749c1bd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,5 +80,10 @@ node_modules
 tsconfig.tsbuildinfo
 vite.config.ts.*
 
-# autogenerated scss type files 
+
+# Generated ts definitions files for CSS modules
+*.d.ts
+*.module.scss.d.ts
+libs/shared/lib/components/buttons/buttons.module.scss.d.ts
+libs/shared/lib/vis/visualizations/table_vis/components/table.module.scss.d.ts
 *.scss.d.ts
diff --git a/libs/config/src/colors.js b/libs/config/src/colors.js
index 5dab6bc1d..ae2aabc80 100644
--- a/libs/config/src/colors.js
+++ b/libs/config/src/colors.js
@@ -229,18 +229,18 @@ export const dataColors = {
     100: 'hsl(334 62% 10%)',
   },
   neutral: {
-    5: 'hsl(0 0 98%)',
-    10: 'hsl(0 0 95%)',
-    20: 'hsl(0 0 89%)',
-    30: 'hsl(0 0 79%)',
-    40: 'hsl(0 0 66%)',
-    50: 'hsl(0 0 55%)',
-    60: 'hsl(0 0 44%)',
-    70: 'hsl(0 0 33%)',
-    80: 'hsl(0 0 25%)',
-    90: 'hsl(0 0 17%)',
-    95: 'hsl(0 0 12%)',
-    100: 'hsl(0 0 8%)',
+    5: 'hsl(0 0% 98%)',
+    10: 'hsl(0 0% 95%)',
+    20: 'hsl(0 0% 89%)',
+    30: 'hsl(0 0% 79%)',
+    40: 'hsl(0 0% 66%)',
+    50: 'hsl(0 0% 55%)',
+    60: 'hsl(0 0% 44%)',
+    70: 'hsl(0 0% 33%)',
+    80: 'hsl(0 0% 25%)',
+    90: 'hsl(0 0% 17%)',
+    95: 'hsl(0 0% 12%)',
+    100: 'hsl(0 0% 8%)',
   },
   orange: {
     5: 'hsl(29 100% 98%)',
diff --git a/libs/shared/lib/data-access/store/visualizationSlice.ts b/libs/shared/lib/data-access/store/visualizationSlice.ts
index 9213b9e68..bbb2e9671 100644
--- a/libs/shared/lib/data-access/store/visualizationSlice.ts
+++ b/libs/shared/lib/data-access/store/visualizationSlice.ts
@@ -8,6 +8,7 @@ export enum Visualizations {
   Paohvis = 'PaohVis',
   RawJSON = 'RawJSONVis',
   Table = 'TableVis',
+  Matrix = 'MatrixVis',
 }
 
 type VisState = {
diff --git a/libs/shared/lib/vis/index.tsx b/libs/shared/lib/vis/index.tsx
index 3abc7b697..ca5a61cde 100644
--- a/libs/shared/lib/vis/index.tsx
+++ b/libs/shared/lib/vis/index.tsx
@@ -9,7 +9,7 @@ import {
   useVisualizationState,
 } from '@graphpolaris/shared/lib/data-access/store/hooks';
 import { localConfigPropTypes, globalConfigPropTypes, VISComponentType } from './Types';
-import { NodeLinkComponent, PaohVisComponent, RawJSONComponent, TableComponent } from './visualizations';
+import {MatrixVisComponent, NodeLinkComponent, PaohVisComponent, RawJSONComponent, TableComponent } from './visualizations';
 
 // export * from './rawjsonvis';
 // export * from './nodelink/nodelinkvis';
@@ -24,6 +24,7 @@ export const Visualizations = {
   RawJSONVis: RawJSONComponent,
   NodeLinkVis: NodeLinkComponent,
   MapVis: '',
+  MatrixVis: MatrixVisComponent,
 };
 
 export const createVisualizationComponent = () => {
diff --git a/libs/shared/lib/vis/visualizations/index.tsx b/libs/shared/lib/vis/visualizations/index.tsx
index f47266be8..0009a7afe 100644
--- a/libs/shared/lib/vis/visualizations/index.tsx
+++ b/libs/shared/lib/vis/visualizations/index.tsx
@@ -3,3 +3,4 @@ export * from './nodelink/nodelinkvis';
 export * from './paohvis/paohvis';
 export * from './semanticsubstrates/semanticsubstrates';
 export * from './table_vis/tableVis';
+export * from './matrix/matrixvis';
\ No newline at end of file
diff --git a/libs/shared/lib/vis/visualizations/matrix/Types.tsx b/libs/shared/lib/vis/visualizations/matrix/Types.tsx
new file mode 100644
index 000000000..4103fa516
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/Types.tsx
@@ -0,0 +1,102 @@
+/**
+ * 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 * as PIXI from 'pixi.js';
+
+/** Types for the nodes and links in the node-link diagram. */
+export type GraphType = {
+  nodes: NodeType[];
+  links: LinkType[];
+  // linkPrediction?: boolean;
+  // shortestPath?: boolean;
+  // communityDetection?: boolean;
+  // numberOfMlClusters?: number;
+};
+
+/** The interface for a node in the node-link diagram */
+export interface NodeType extends d3.SimulationNodeDatum {
+  id: string;
+
+  // Number to determine the color of the node
+  label?: string;
+  type: number;
+  attributes?: Record<string, any>;
+  cluster?: number;
+  clusterAccoringToMLData?: number;
+  shortestPathData?: Record<string, string[]>;
+
+  // Node that is drawn.
+  radius: number;
+  // Text to be displayed on top of the node.
+  gfxtext?: PIXI.Text;
+  gfxAttributes?: PIXI.Graphics;
+  selected?: boolean;
+  isShortestPathSource?: boolean;
+  isShortestPathTarget?: boolean;
+  index?: number;
+
+  // The text that will be shown on top of the node if selected.
+  displayInfo?: string;
+
+  // Node’s current x-position.
+  x?: number;
+
+  // Node’s current y-position.
+  y?: number;
+
+  // Node’s current x-velocity
+  vx?: number;
+
+  // Node’s current y-velocity
+  vy?: number;
+
+  // Node’s fixed x-position (if position was fixed)
+  fx?: number | null;
+
+  // Node’s fixed y-position (if position was fixed)
+  fy?: number | null;
+}
+
+/** The interface for a link in the node-link diagram */
+export interface LinkType extends d3.SimulationLinkDatum<NodeType> {
+  // The thickness of a line
+  id: string;
+  value: number;
+  // To check if an edge is calculated based on a ML algorithm
+  mlEdge: boolean;
+  color: number;
+  alpha?: number;
+}
+
+/**collectionNode holds 1 entry per node kind (so for example a MockNode with name "parties" and all associated attributes,) */
+export type TypeNode = {
+  name: string; //Collection name
+  attributes: string[]; //attributes. This includes all attributes found in the collection
+  type: number | undefined; //number that represents collection of node, for colorscheme
+  visualisations: Visualisation[]; //The way to visualize attributes of this Node kind
+};
+
+export type CommunityDetectionNode = {
+  cluster: number; //group as used by colouring scheme
+};
+
+/**Visualisation holds the visualisation method for an attribute */
+export type Visualisation = {
+  attribute: string; //attribute type      (e.g. 'age')
+  vis: string; //visualisation type  (e.g. 'radius')
+};
+
+/** possible colors to pick from*/
+export type Colors = {
+  name: string;
+};
+
+/**AssignedColors is a simple holder for color selection  */
+export type AssignedColors = {
+  collection: number | undefined; //number of the collection (type or group)
+  color: string; //color in hex
+  default: string; //default color, for easy switching back
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx
new file mode 100644
index 000000000..7226749f9
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx
@@ -0,0 +1,83 @@
+import { Edge } from '@graphpolaris/shared/lib/data-access';
+import { dataColors } from 'config';
+import { Graphics } from 'pixi.js';
+
+export const createColumn = (
+  orderRow: string[],
+  edgesForThisColumn: Edge[],
+  visMapping: any[], // TODO type
+  cellWidth: number,
+  cellHeight: number
+) => {
+  const currentVisMapping = visMapping[0];
+  let gfx = new Graphics();
+  gfx.eventMode = 'static';
+  if (!currentVisMapping) {
+    return gfx;
+  }
+
+  let color = null;
+  for (let i = 0; i < orderRow.length; i++) {
+    const rowOrderID = orderRow[i];
+
+    const inOutEdge = edgesForThisColumn.filter((edge) => {
+      return edge.from === rowOrderID || edge.to === rowOrderID;
+    });
+
+    const inEdges = edgesForThisColumn.filter((edge) => {
+      return edge.to === rowOrderID;
+    });
+
+    const outEdges = edgesForThisColumn.filter((edge) => {
+      return edge.from === rowOrderID;
+    });
+
+    /**
+     * maybe this should inverted: make it a visitor
+     * pattern and let the visMapping decide how ot implment all this
+     **/
+    if (currentVisMapping.attribute === 'adjacency') {
+      color = currentVisMapping.colorScale(Math.min(inOutEdge.length, 1));
+      gfx = currentVisMapping.renderFunction(i, color, gfx);
+    } else if (currentVisMapping.attribute === 'label') {
+      if (outEdges.length > 0) {
+        const thisEdge = outEdges[0];
+        color = currentVisMapping.colorScale(thisEdge.label);
+      }
+      gfx.beginFill(color, 1);
+      // gfx.beginFill(colorScale(node.type)));
+      gfx.lineStyle(2, 0xffffff);
+      gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight);
+      gfx.endFill();
+    } else {
+      if (outEdges.length > 0) {
+        const thisEdge = outEdges[0];
+        const value = byString(thisEdge, currentVisMapping.attribute);
+
+        color = currentVisMapping.colorScale(value);
+      }
+      gfx.beginFill(color, 1);
+      // gfx.beginFill(colorScale(node.type)));
+      gfx.lineStyle(2, 0xffffff);
+      gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight);
+      gfx.endFill();
+    }
+  }
+
+  return gfx;
+};
+
+const byString = (o: any, accessor: string) => {
+  accessor = accessor.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
+  accessor = accessor.replace(/^\./, ''); // strip a leading dot
+  var a = accessor.split('.');
+  for (var i = 0, n = a.length; i < n; ++i) {
+    var k = a[i];
+    if (k in o) {
+      o = o[k];
+    } else {
+      return;
+    }
+  }
+  return o;
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx
new file mode 100644
index 000000000..6176f1d5b
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx
@@ -0,0 +1,48 @@
+import { Edge, Node } from '@graphpolaris/shared/lib/data-access';
+import { dataColors, tailwindColors } from 'config';
+import { Graphics, Sprite, Texture } from 'pixi.js';
+
+export const createColumn = (
+  id: number,
+  nodesCol: Node[],
+  nodesRow: Node[],
+  edges: Edge[],
+  colorScale: any,
+  cellWidth: number,
+  cellHeight: number
+) => {
+  const sprite = new Sprite(Texture.WHITE);
+  sprite.name = 'col_' + id;
+  sprite.eventMode = 'static';
+  // button.eventMode = 'static';
+  sprite.cursor = 'pointer';
+  const edgesForThisColumn = edges.filter((edge) => {
+    return edge.from === nodesCol[id].id || edge.to === nodesCol[id].id;
+  });
+  const bgCellColor = dataColors.neutral['5'];
+  const fgCellColor = tailwindColors.entity.DEFAULT;
+  let color = bgCellColor;
+  for (let i = 0; i < nodesRow.length; i++) {
+    const node = nodesRow[i];
+    if (
+      edgesForThisColumn.filter((edge) => {
+        return edge.from === node.id || edge.to === node.id;
+      }).length > 0
+    ) {
+      color = fgCellColor;
+    } else {
+      color = bgCellColor;
+    }
+    sprite.tint = color;
+    sprite.width = cellWidth;
+    sprite.height = cellHeight;
+    sprite.position.set(id * cellHeight, i * cellWidth);
+    // sprite.acceleration = new Point(0);
+    // TODO get
+    // gfx.beginFill(colorScale(node.type)));
+    // sprite.lineStyle(2, 0xffffff);
+    // sprite.drawRect(0, i * cellWidth, cellWidth, cellHeight);
+    // sprite.endFill();
+  }
+  return sprite;
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/MatrixExport.tsx b/libs/shared/lib/vis/visualizations/matrix/components/MatrixExport.tsx
new file mode 100644
index 000000000..e6907ecc3
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/MatrixExport.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import jsPDF from 'jspdf';
+import { Container, IRenderer } from 'pixi.js';
+
+/** Exports the nodelink diagram as a pdf for downloading. */
+async function exportToPDF(renderer: IRenderer, stage: Container): Promise<void> {
+  const b = await renderer.extract.canvas(stage).convertToBlob?.({ type: 'image/png' });
+  if (!b) return;
+  const pdf = new jsPDF();
+  if (b) {
+    pdf.addImage(URL.createObjectURL(b), 'JPEG', 0, 0, 100, 100, '', 'NONE', 0);
+    pdf.save('diagram.pdf');
+  } else {
+    console.log('null blob in exportToPDF');
+  }
+}
+
+/** Exports the nodelink diagram as a png for downloading. */
+async function exportToPNG(renderer: IRenderer, stage: Container): Promise<void> {
+  const b = await renderer.extract.canvas(stage).convertToBlob?.({ type: 'image/png' });
+  if (b) {
+    const a = document.createElement('a');
+    document.body.append(a);
+    a.download = 'diagram';
+    a.href = URL.createObjectURL(b);
+    a.click();
+    a.remove();
+  } else {
+    console.log('null blob in exportToPNG');
+  }
+}
+
+const useExport = () => {
+  return { exportToPDF, exportToPNG };
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx
new file mode 100644
index 000000000..550a1cef5
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx
@@ -0,0 +1,593 @@
+import { Edge, GraphQueryResult, Node, useML, useSearchResultData } from '@graphpolaris/shared/lib/data-access';
+import { dataColors, tailwindColors } from 'config';
+import * as d3 from 'd3';
+import { Viewport } from 'pixi-viewport';
+import {
+  Application,
+  ColorSource,
+  Container,
+  DisplayObject,
+  FederatedPointerEvent,
+  Graphics,
+  IPointData,
+  Point,
+  Sprite,
+  Text,
+  Texture,
+} from 'pixi.js';
+import { useEffect, useRef, useState } from 'react';
+import { LinkType, NodeType } from '../Types';
+import { NLPopup } from './MatrixPopup';
+
+import { Actions, Interpolations } from 'pixi-actions';
+
+import Color from 'color';
+import { localConfigSchemaType } from '../../../Types';
+import { createColumn } from './ColumnGraphicsComponent';
+import { ReorderingManager } from './ReorderingManager';
+
+type Props = {
+  // onClick: (node: NodeType, pos: IPointData) => void;
+  // onHover: (data: { node: NodeType; pos: IPointData }) => void;
+  // onUnHover: (data: { node: NodeType; pos: IPointData }) => void;
+  highlightNodes: NodeType[];
+  currentShortestPathEdges?: LinkType[];
+  highlightedLinks?: LinkType[];
+  graph?: GraphQueryResult;
+  localConfig: localConfigSchemaType;
+};
+
+const app = new Application({ background: 0xffffff, antialias: true, autoDensity: true, eventMode: 'auto' });
+const columnsContainer = new Container();
+const columnAxisContainer = new Container();
+const rowsContainer = new Container();
+
+//////////////////
+// MAIN COMPONENT
+//////////////////
+
+export const MatrixPixi = (props: Props) => {
+  let config = {
+    textOffsetX: 100,
+    textOffsetY: 100,
+    visMapping: [] as any[], // TODO type
+
+    cellHeight: 100,
+    cellWidth: 100,
+  };
+
+  let columnOrder: string[] = [];
+  let rowOrder: string[] = [];
+
+  const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
+  const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]);
+  // const [columnOrder, setColumnOrder] = useState<string[]>([]);
+
+  const nodeMap = useRef(new Map<string, Graphics>());
+  const linkMap = useRef(new Map<string, Graphics>());
+  const viewport = useRef<Viewport>();
+  const ref = useRef<HTMLDivElement>(null);
+  const isSetup = useRef(false);
+  const ml = useML();
+  const searchResults = useSearchResultData();
+
+  // const imperative = useRef<any>(null);
+
+  function resize() {
+    const width = ref?.current?.clientWidth || 1000;
+    const height = ref?.current?.clientHeight || 1000;
+    app.renderer.resize(width, height);
+    if (viewport.current) {
+      viewport.current.screenWidth = width;
+      viewport.current.worldWidth = width;
+      viewport.current.worldHeight = height;
+      viewport.current.screenHeight = height;
+    }
+
+    if (props.graph) {
+      setup();
+    }
+
+    app.render();
+  }
+
+  useEffect(() => {
+    if (!ref.current) return;
+    const resizeObserver = new ResizeObserver(() => {
+      resize();
+    });
+    resizeObserver.observe(ref.current);
+    return () => resizeObserver.disconnect(); // clean up
+  }, []);
+
+  useEffect(() => {
+    if (ref.current && ref.current.children.length === 0) {
+      ref.current.appendChild(app.view as HTMLCanvasElement);
+      resize();
+    }
+  }, [ref]);
+
+  useEffect(() => {
+    console.log('graph change');
+    // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current);
+    if (props.graph && ref.current && ref.current.children.length > 0) {
+      if (!isSetup.current) setup();
+      else update();
+    }
+  }, [props.graph]);
+
+  useEffect(() => {
+    console.log('config change');
+    // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current);
+    if (props.graph && ref.current && ref.current.children.length > 0) {
+      setup();
+    }
+  }, [props.localConfig]);
+
+  // TODO implement search results
+  // useEffect(() => {
+  //   if (props.graph) {
+  //     props.graph.nodes.forEach((node: NodeType) => {
+  //       const gfx = nodeMap.current.get(node.id);
+  //       if (!gfx) return;
+  //       const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node.id);
+  //       gfx.alpha = isNodeInSearchResults || searchResults.nodes.length === 0 ? 1 : 0.05;
+  //     });
+
+  //     props.graph.links.forEach((link: LinkType) => {
+  //       const gfx = linkMap.current.get(link.id);
+  //       if (!gfx) return;
+  //       const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id);
+  //       gfx.alpha = isLinkInSearchResults || searchResults.edges.length === 0 ? 1 : 0.05;
+  //     });
+  //   }
+  // }, [searchResults]);
+
+  function onButtonDown(event: FederatedPointerEvent) {
+    console.log(
+      event.currentTarget
+      // graph.nodes.find((node) => node.id === event.currentTarget.name)
+    );
+  }
+
+  const update = (forceClear = false) => {
+    setPopups([]);
+    if (!props.graph || !ref.current) return;
+
+    if (props.graph) {
+      if (forceClear) {
+        nodeMap.current.clear();
+        linkMap.current.clear();
+        rowsContainer.removeChildren();
+        columnsContainer.removeChildren();
+      }
+    }
+  };
+
+  const onHoverColumn = (event: any, currentNode: Node | null) => {
+    app.renderer.plugins.interaction.cursorStyles.pointer = 'crosshair';
+    if (!currentNode) {
+      rowsContainer.children.forEach((row) => {
+        row.alpha = 1;
+      });
+      columnsContainer.children.forEach((col) => {
+        col.alpha = 1;
+      });
+      columnAxisContainer.children.forEach((colText) => {
+        colText.alpha = 1;
+      });
+
+      return;
+    }
+
+    columnsContainer.children.forEach((col) => {
+      col.alpha = 0.25;
+    });
+    event.currentTarget.alpha = 1;
+
+    columnAxisContainer.children.forEach((colText) => {
+      colText.alpha = 0.25;
+      if (colText.name?.endsWith(currentNode.id)) colText.alpha = 1;
+    });
+
+    if (!props.graph) return;
+    rowsContainer.children.forEach((col) => {
+      col.alpha = 0.25;
+    });
+
+    if (!props.graph) return;
+    rowsContainer.children.forEach((col) => {
+      col.alpha = 0.25;
+    });
+
+    // console.log(event.currentTarget);
+    const edgesForThisColumn = props.graph.edges.filter((edge) => {
+      return edge.from === currentNode.id || edge.to === currentNode.id;
+    });
+
+    edgesForThisColumn.forEach((edge) => {
+      let row = rowsContainer.getChildByName(edge.to);
+      if (row) row.alpha = 1;
+
+      row = rowsContainer.getChildByName(edge.from);
+      if (row) row.alpha = 1;
+    });
+  };
+
+  const reorderColumns = (columnOrder: string[]) => {
+    const columns = columnsContainer.children;
+    const columnsText = columnAxisContainer.children;
+    let columnPositions: Point[] = [];
+    for (let i = 0; i < columns.length; i++) {
+      const col = columns[i];
+      columnPositions.push(new Point(col.position.x, col.position.y));
+    }
+
+    let columnTextPositions: Point[] = [];
+    for (let i = 0; i < columnsText.length; i++) {
+      const col = columnsText[i];
+      columnTextPositions.push(new Point(col.position.x, col.position.y));
+    }
+
+    if (columnTextPositions.length !== columnPositions.length)
+      throw new Error(
+        'columnTextPositions and columnPositions have different length ' + columnTextPositions.length + ' / ' + columnPositions.length
+      );
+
+    for (let i = 0; i < columns.length; i++) {
+      const column = columns[i];
+      if (!column.name) throw new Error('column has no name');
+      const index = columnOrder.indexOf(column.name);
+      if (index === -1) throw new Error('column not found in columnOrder');
+
+      const newPosition = columnPositions[index];
+      // move column to new position
+      Actions.moveTo(columns[i], newPosition.x, newPosition.y, 1, Interpolations.pow2out).play();
+
+      const newPositionText = columnTextPositions[index];
+      // move column to new position
+      Actions.moveTo(columnsText[i], newPositionText.x, newPositionText.y, 1, Interpolations.pow2out).play();
+    }
+  };
+
+  const reorderRows = (
+    nodesCol: Node[],
+    nodesRow: Node[],
+    edges: Edge[],
+    rowOrder: string[],
+    colorScale: any,
+    cellWidth: number,
+    cellHeight: number
+  ) => {
+    const rows = rowsContainer.children;
+    let rowPositions: Point[] = [];
+    for (let i = 0; i < rows.length; i++) {
+      const row = rows[i];
+      rowPositions.push(new Point(row.position.x, row.position.y));
+    }
+
+    for (let i = 0; i < rows.length; i++) {
+      const row = rows[i] as DisplayObject;
+      if (!row.name) throw new Error('row has no name');
+      const index = rowOrder.indexOf(row.name);
+      if (index === -1) throw new Error('row not found in rowOrder');
+
+      const newPosition = rowPositions[index];
+      // move column to new position
+      Actions.moveTo(row, newPosition.x, newPosition.y, 1, Interpolations.pow2out).play();
+    }
+
+    const colOrder = columnsContainer.children.map((col) => col.name) as string[];
+    console.log('colOrder', colOrder);
+    if (!colOrder) throw new Error('colOrder is undefined');
+    columnsContainer.removeChildren();
+    setupColumns(edges, colOrder, rowOrder);
+  };
+
+  const setup = () => {
+    if (!props.graph) throw Error('Graph is undefined; setup not possible');
+    columnsContainer.removeChildren();
+    rowsContainer.removeChildren();
+    columnAxisContainer.removeChildren();
+    app.stage.removeChildren();
+
+    const size = ref.current?.getBoundingClientRect();
+    viewport.current = new Viewport({
+      screenWidth: size?.width || 1000,
+      screenHeight: size?.height || 1000,
+      worldWidth: size?.width || 1000,
+      worldHeight: size?.height || 1000,
+      stopPropagation: true,
+      events: app.renderer.events, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled
+    });
+
+    app.stage.addChild(viewport.current);
+
+    viewport.current.addChild(columnsContainer);
+    viewport.current.addChild(rowsContainer);
+
+    const groupByType = props.graph.nodes.reduce((group: any, node: Node) => {
+      if (!group[node.label]) group[node.label] = [];
+      group[node.label].push(node);
+      return group;
+    }, {} as { [key: string]: Node[] });
+
+    // order groupByType by size
+    const ordered = Object.entries(groupByType).sort((a: any[], b: any[]) => b[1].length - a[1].length);
+
+    // console.log('ordered', ordered);
+    // console.log('edges', props.graph.edges);
+
+    let cols = [] as Node[];
+    let rows = [] as Node[];
+    if (ordered.length == 2) {
+      cols = ordered[0][1] as Node[];
+      rows = ordered[1][1] as Node[];
+    } else if (ordered.length == 1) {
+      cols = ordered[0][1] as Node[];
+      rows = ordered[0][1] as Node[];
+    } else {
+      // show text that there are no nodes on the viewport
+      const finalTextSize = 25;
+      const bigRandomScaleFactor = 10;
+      const basicText = new Text('Result is empty', { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'center' });
+      basicText.position.set(viewport.current?.screenWidth / 2 || 1000, viewport.current?.screenHeight / 2 || 1000);
+      basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor));
+      basicText.eventMode = 'none';
+      viewport.current.addChild(basicText);
+      return;
+    }
+
+    columnOrder = d3.range(0, cols.length).map((d) => cols[d].id);
+    rowOrder = d3.range(0, rows.length).map((d) => rows[d].id);
+    const reorderingManager = new ReorderingManager(props.graph);
+    const newOrdering = reorderingManager.reorderMatrix('count', columnOrder, rowOrder);
+    columnOrder = newOrdering.columnOrder;
+    rowOrder = newOrdering.rowOrder;
+
+    config.cellWidth = Math.max((size?.width || 1000) / props.graph.nodes.length, (size?.height || 1000) / props.graph.nodes.length);
+    config.cellHeight = config.cellWidth;
+
+    // console.log('currentColumnOrder', columnOrder);
+
+    setupVisualizationEncodingMapping(props.localConfig);
+
+    setupColumns(props.graph.edges, columnOrder, rowOrder);
+    setupColumnLegend(columnOrder);
+    setupColumnInteractivity(cols);
+
+    setupRowLegend(rows, rowOrder);
+    setupRowInteractivity(cols, rows, rowOrder, d3.scaleOrdinal(d3.schemeCategory10));
+
+    console.log('setup matrixvis with graph:', props.graph);
+
+    // activate plugins
+    viewport.current.drag().pinch().wheel().animate({}).decelerate({ friction: 0.75 });
+
+    app.ticker.add(tick);
+    update();
+    isSetup.current = true;
+  };
+
+  const setupVisualizationEncodingMapping = (localConfig: localConfigSchemaType) => {
+    if (!props.graph) throw new Error('Graph is undefined; cannot setup matrix');
+    const visMapping = []; // TODO type
+
+    let colorNeutralString = dataColors.neutral['5'];
+    const colorNeutral = Color(colorNeutralString);
+
+    // make adjacency
+    // const adjacenyScale = d3.scaleLinear([dataColors.neutral['5'], tailwindColors.entity.DEFAULT]);
+    const adjacenyScale = d3.scaleLinear([colorNeutral, tailwindColors.entity.DEFAULT]);
+    visMapping.push({
+      attribute: 'adjacency',
+      encoding: localConfig.marks,
+      colorScale: adjacenyScale,
+      renderFunction: function (i: number, color: ColorSource, gfxContext: Graphics) {
+        gfxContext.beginFill(color, 1);
+
+        // TODO get
+        // gfx.beginFill(colorScale(node.type)));
+
+        gfxContext.lineStyle(2, 0xffffff);
+        if (this.encoding === 'rect') {
+          gfxContext.drawRect(0, i * config.cellHeight, config.cellWidth, config.cellHeight);
+        }
+        if (this.encoding === 'circle') {
+          gfxContext.drawCircle(config.cellWidth / 2, i * config.cellHeight + config.cellHeight / 2, config.cellWidth / 2);
+        }
+        gfxContext.endFill();
+
+        return gfxContext;
+      },
+    });
+
+    // make label
+    // const labels = props.graph.edges.map((edge: Edge) => {
+    //   return edge.label;
+    // });
+    // const tailwindColorsArray = Object.values(tailwindColors.entity).slice(0, labels.length);
+    // const scale = d3.scaleOrdinal(tailwindColorsArray.reverse());
+    // scale.domain(Array.from(labels));
+    // console.log('labels', labels, props.graph.edges);
+    // visMapping.push({ attribute: 'label', encoding: 'rect', colorScale: scale });
+
+    // make value
+    // const values = props.graph.edges.map((edge: Edge) => {
+    //   return edge?.attributes?.Distance as number;
+    // });
+
+    // const extent = d3.extent(values);
+    // if (extent[0] === undefined || extent[1] === undefined) throw new Error('Extent is undefined');
+    // console.log('labels', values, extent, props.graph.edges);
+
+    // const scale = d3.scaleLinear(['red', 'blue']);
+    // scale.domain(extent as [number, number]);
+    // visMapping.push({ attribute: 'attributes.Distance', encoding: 'rect', colorScale: scale });
+
+    config.visMapping = visMapping;
+  };
+
+  function setupColumns(edges: Edge[], columnOrder: string[], rowOrder: string[]) {
+    const visMapping = config.visMapping[0];
+
+    if (!visMapping) throw new Error('Cannot setup matrix without visMapping');
+
+    for (let j = 0; j < columnOrder.length; j++) {
+      const oneColumn = new Container();
+      oneColumn.name = columnOrder[j];
+
+      const edgesForThisColumn = edges.filter((edge) => {
+        return edge.from === oneColumn.name || edge.to === oneColumn.name;
+      });
+      // console.log('edgesForThisColumn', oneColumn.name, edgesForThisColumn);
+
+      const col = createColumn(rowOrder, edgesForThisColumn, config.visMapping, config.cellWidth, config.cellHeight);
+      oneColumn.addChild(col);
+      oneColumn.position.set(j * config.cellWidth + config.textOffsetX, config.textOffsetY);
+
+      columnsContainer.addChild(oneColumn);
+      oneColumn.alpha = 0;
+      Actions.sequence(Actions.delay(j * 0.005 * Math.random()), Actions.fadeIn(oneColumn, 0.8, Interpolations.pow2out)).play();
+    }
+  }
+
+  const setupColumnInteractivity = (cols: Node[]) => {
+    for (let j = 0; j < columnsContainer.children.length; j++) {
+      const oneColumn = columnsContainer.children[j];
+      oneColumn.interactive = true;
+      oneColumn.on('pointerdown', onButtonDown);
+      oneColumn.on('pointerover', (event) => {
+        const col = cols.find((col) => col.id === oneColumn.name);
+        if (col) onHoverColumn(event, col);
+      });
+      oneColumn.on('pointerout', (event) => {
+        onHoverColumn(event, null as any);
+      });
+    }
+  };
+
+  const setupColumnLegend = (columnOrder: string[]) => {
+    // console.log('setupColumnLegend');
+    // columnAxisContainer.removeChildren();
+    columnAxisContainer.position.set(config.textOffsetX, 0);
+    for (let j = 0; j < columnOrder.length; j++) {
+      // from the PixiJS documention: Setting a text object's scale to > 1.0 will result in blurry/pixely display,
+      // because the text is not re-rendered at the higher resolution needed to look sharp -
+      // it's still the same resolution it was when generated. To deal with this, you can render at a higher
+      // initial size and down-scale, instead.
+      const finalTextSize = 10;
+      const bigRandomScaleFactor = 10;
+      const basicText = new Text(columnOrder[j], { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'center' });
+      basicText.position.set(j * config.cellWidth + config.cellWidth / 4, config.textOffsetY - 5);
+      basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor));
+      basicText.rotation = (Math.PI * 3) / 2;
+      basicText.eventMode = 'none';
+      basicText.name = 'Text_' + columnOrder[j];
+      columnAxisContainer.addChild(basicText);
+    }
+    viewport.current?.addChild(columnAxisContainer);
+
+    const columnAxis = new Sprite(Texture.WHITE);
+    columnAxis.width = columnOrder.length * config.cellWidth;
+    columnAxis.height = config.textOffsetY;
+    columnAxis.tint = 0xaaaaaa;
+    columnAxis.position.set(config.textOffsetX, 0);
+    columnAxis.alpha = 0.0;
+    columnAxis.interactive = true;
+    columnAxis.on('pointerdown', (event) => {
+      // const reordering = new ReorderColumnsAction(columnOrder);
+
+      if (!props.graph) throw new Error('Graph is undefined; cannot reorder matrix');
+
+      const reorderingManager = new ReorderingManager(props.graph);
+      const newOrdering = reorderingManager.reorderMatrix('count', columnOrder, rowOrder);
+      columnOrder = newOrdering.columnOrder.reverse();
+
+      reorderColumns(columnOrder);
+    });
+    // columnAxis.on('pointerdown', reorderColumns);
+    columnAxis.on('pointerover', (event) => {
+      Actions.fadeTo(columnAxis, 0.5, 0.5, Interpolations.pow2out).play();
+    });
+    columnAxis.on('pointerout', (event) => {
+      Actions.fadeTo(columnAxis, 0.0, 0.5, Interpolations.pow2out).play();
+    });
+    viewport.current?.addChild(columnAxis);
+  };
+
+  function shuffle(array: any[]) {
+    var m = array.length,
+      t,
+      i;
+
+    // While there remain elements to shuffle…
+    while (m) {
+      // Pick a remaining element…
+      i = Math.floor(Math.random() * m--);
+
+      // And swap it with the current element.
+      t = array[m];
+      array[m] = array[i];
+      array[i] = t;
+    }
+
+    return array;
+  }
+
+  function setupRowLegend(rows: Node[], rowOrder: string[]) {
+    for (let i = 0; i < rowOrder.length; i++) {
+      const rowID = rowOrder[i];
+      const finalTextSize = 10;
+      const bigRandomScaleFactor = 10;
+      const basicText = new Text(rowID, { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'right' });
+      basicText.anchor.x = 1;
+      basicText.anchor.y = 0;
+      basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor));
+      basicText.position.set(config.textOffsetX - 5, i * config.cellHeight + config.textOffsetY + config.cellHeight / 4);
+      basicText.name = rowID;
+      rowsContainer.addChild(basicText);
+
+      Actions.sequence(Actions.delay(i * 0.005 * Math.random()), Actions.fadeIn(basicText, 0.5, Interpolations.pow2out)).play();
+    }
+  }
+
+  const setupRowInteractivity = (cols: Node[], rows: Node[], rowOrder: string[], colorScale: any) => {
+    const rowAxis = new Sprite(Texture.WHITE);
+    rowAxis.width = config.textOffsetX;
+    rowAxis.height = rows.length * config.cellHeight;
+    rowAxis.tint = 0xaaaaaa;
+    rowAxis.position.set(0, config.textOffsetY);
+    rowAxis.alpha = 0.0;
+    rowAxis.interactive = true;
+    rowAxis.on('pointerdown', (event) => {
+      if (props.graph) {
+        rowOrder = shuffle(rowOrder);
+        reorderRows(cols, rows, props?.graph.edges, rowOrder, colorScale, config.cellWidth, config.cellHeight);
+      }
+    });
+    rowAxis.on('pointerover', (event) => {
+      Actions.fadeTo(rowAxis, 0.5, 0.5, Interpolations.pow2out).play();
+    });
+    rowAxis.on('pointerout', (event) => {
+      Actions.fadeTo(rowAxis, 0.0, 0.5, Interpolations.pow2out).play();
+    });
+    viewport.current?.addChild(rowAxis);
+  };
+
+  const tick = (delta: number) => {
+    if (props.graph) {
+      Actions.tick(delta / 60);
+    }
+  };
+
+  return (
+    <>
+      {popups.map((popup) => (
+        <NLPopup onClose={() => {}} data={popup} key={popup.node.id} />
+      ))}
+      {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />}
+      <div className="h-full w-full overflow-hidden" ref={ref}></div>
+    </>
+  );
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/MatrixPopup.tsx b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPopup.tsx
new file mode 100644
index 000000000..cab6d5b29
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPopup.tsx
@@ -0,0 +1,46 @@
+import { IPointData } from 'pixi.js';
+import { NodeType } from '../Types';
+
+export type NodelinkPopupProps = {
+  data: { node: NodeType; pos: IPointData };
+  onClose: () => void;
+};
+
+export const NLPopup = (props: NodelinkPopupProps) => {
+  const node = props.data.node;
+
+  return (
+    <div
+      className="absolute card card-bordered z-50 bg-white rounded-none text-[0.9rem] min-w-[10rem]"
+      // style={{ top: props.data.pos.y + 10, left: props.data.pos.x + 10 }}
+      style={{ transform: 'translate(' + (props.data.pos.x + 20) + 'px, ' + (props.data.pos.y + 10) + 'px)' }}
+    >
+      <div className="card-body p-0">
+        <span className="px-2.5 pt-2">
+          <span>Node</span>
+          <span className="float-right">{node.id}</span>
+        </span>
+        <div className="h-[1px] w-full bg-offwhite-300"></div>
+        <div className="px-2.5 text-[0.8rem]">
+          {node.attributes &&
+            Object.entries(node.attributes).map(([k, v], i) => {
+              return (
+                <div key={k} className="flex flex-row gap-3">
+                  <span className="">{k}: </span>
+                  <span className="ml-auto max-w-[10rem] text-right truncate">
+                    <span title={JSON.stringify(v)}>{JSON.stringify(v)}</span>
+                  </span>
+                </div>
+              );
+            })}
+          {node.cluster && (
+            <p>
+              Cluster: <span className="float-right">{node.cluster}</span>
+            </p>
+          )}
+        </div>
+        <div className="h-[1px] w-full"></div>
+      </div>
+    </div>
+  );
+};
diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx
new file mode 100644
index 000000000..ae69d44e2
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx
@@ -0,0 +1,227 @@
+import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access';
+import { toPlainObject } from 'lodash-es';
+import * as reorder from 'reorder.js';
+
+export class ReorderingManager {
+  private leafOrder = reorder.optimal_leaf_order().distance(reorder.distance.manhattan);
+  graph: GraphQueryResult;
+
+  constructor(graph: GraphQueryResult) {
+    this.graph = graph;
+  }
+
+  private computeNameOrder = (columnOrder: string[], rowOrder: string[]) => {
+    const columnOrderSorted = columnOrder.sort((a, b) => a.localeCompare(b));
+    const rowOrderSorted = rowOrder.sort((a, b) => a.localeCompare(b));
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrderSorted };
+  };
+
+  private computeCountOrder = (columnOrder: string[], rowOrder: string[]) => {
+    let columnOrderSorted: { id: string; count: number }[] = [];
+    let rowOrderSorted: { id: string; count: number }[] = [];
+
+    for (let i = 0; i < columnOrder.length; i++) {
+      const oneColumn = columnOrder[i];
+      const edgesForThisColumn = this.graph.edges.filter((edge) => {
+        return edge.from === oneColumn || edge.to === oneColumn;
+      });
+
+      columnOrderSorted.push({ id: oneColumn, count: edgesForThisColumn.length });
+    }
+
+    for (let j = 0; j < rowOrder.length; j++) {
+      const oneRow = rowOrder[j];
+      const edgesForThisRow = this.graph.edges.filter((edge) => {
+        return edge.from === oneRow || edge.to === oneRow;
+      });
+
+      rowOrderSorted.push({
+        id: oneRow,
+        count: edgesForThisRow.length,
+      });
+    }
+
+    columnOrderSorted = columnOrderSorted.sort((a, b) => b.count - a.count);
+    rowOrderSorted = rowOrderSorted.sort((a, b) => b.count - a.count);
+
+    return { columnOrder: columnOrderSorted.map((a) => a.id), rowOrder: rowOrderSorted.map((a) => a.id) };
+  };
+
+  private computeLeaforder = (columnOrder: string[], rowOrder: string[]) => {
+    const adjacency: number[][] = this.constructAdjacencyMatrix(this.graph, columnOrder, rowOrder);
+
+    const order = this.leafOrder.reorder(adjacency).map((i) => columnOrder[i]);
+
+    console.debug('computed leaforder', order);
+
+    return { columnOrder: order, rowOrder: rowOrder };
+  };
+
+  private constructAdjacencyMatrix(graph: GraphQueryResult, columnOrder: string[], rowOrder: string[]): number[][] {
+    const adjacency: number[][] = [];
+    for (let i = 0; i < columnOrder.length; i++) {
+      const oneColumn = columnOrder[i];
+      const edgesForThisColumn = graph.edges.filter((edge) => {
+        return edge.from === oneColumn || edge.to === oneColumn;
+      });
+
+      const rowOrderIDs = [];
+      for (let j = 0; j < rowOrder.length; j++) {
+        const oneRow = rowOrder[j];
+        if (
+          edgesForThisColumn.filter((edge) => {
+            return edge.from === oneRow || edge.to === oneRow;
+          }).length > 0
+        ) {
+          rowOrderIDs.push(j);
+        }
+      }
+      adjacency.push(rowOrderIDs);
+    }
+
+    return adjacency;
+  }
+
+  private computeBarycenter = (columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+
+    const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder);
+    const barycenter = reorderAny.barycenter_order(reorderGraph);
+
+    let improved = reorderAny.adjacent_exchange(reorderGraph, barycenter[0], barycenter[1]);
+
+    const columnOrderSorted = improved[0].map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a));
+    const rowOrderSorted = improved[0].map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a));
+
+    console.debug('improved barycenter', improved, columnOrderSorted, rowOrderSorted);
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrderSorted };
+  };
+
+  private getReorderJSGraph = (graph: GraphQueryResult, columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+
+    // console.log('getReorderJSGraph', graph, columnOrder, rowOrder);
+
+    const nodesTemp = graph.nodes.map(
+      (node) =>
+        ({
+          ...node,
+          index: 0,
+          weight: 0,
+        } as any)
+    );
+
+    const nodes = columnOrder.map((id) => nodesTemp.find((node) => node.id === id));
+    const graphReorderJs = reorderAny.graph().nodes(nodes);
+
+    const edges = graph.edges.map((edge) => {
+      const source = nodes.find((node) => node.id === edge.from);
+      const target = nodes.find((node) => node.id === edge.to);
+
+      if (!source || !target) {
+        return new Error('Source or target not found from edge list');
+      }
+      return {
+        ...edge,
+        source,
+        target,
+      } as any;
+    });
+
+    graphReorderJs.links(edges);
+    graphReorderJs.init();
+
+    return graphReorderJs;
+  };
+
+  private computeRCM = (columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+    const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder);
+
+    const order = reorderAny.reverse_cuthill_mckee_order(reorderGraph);
+    const columnOrderSorted = order.map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a));
+    const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a));
+
+    console.debug('improved rcm', order, columnOrderSorted, rowOrderSorted);
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrderSorted };
+  };
+
+  private computeSpectal = (columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+    const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder);
+
+    var order = reorderAny.spectral_order(reorderGraph);
+    const columnOrderSorted = order.map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a));
+    const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a));
+
+    console.debug('improved spectral', order, columnOrderSorted, rowOrderSorted);
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrderSorted };
+  };
+
+  private computeBFS = (columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+    const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder);
+
+    var order = reorderAny.bfs_order(reorderGraph);
+
+    const columnOrderSorted = order.map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a));
+    const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a));
+
+    console.debug('improved bfs', order, columnOrderSorted, rowOrderSorted);
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrderSorted };
+  };
+
+  private computePCA = (columnOrder: string[], rowOrder: string[]) => {
+    const reorderAny = reorder as any;
+
+    const columnOrderUnSorted = columnOrder.map((id: string) => this.graph.nodes.find((node) => node.id === id));
+
+    const order = reorderAny.pca_order(columnOrderUnSorted);
+    const columnOrderSorted = order.map((i: number) => columnOrder[i]);
+
+    console.debug('improved pca', order, columnOrderSorted, rowOrder);
+
+    return { columnOrder: columnOrderSorted, rowOrder: rowOrder };
+  };
+
+  public reorderMatrix = (orderingname = 'leafordering', columnOrder: string[], rowOrder: string[]) => {
+    console.log('reorderMatrix', orderingname);
+    switch (orderingname.toLowerCase()) {
+      case 'leafordering': {
+        return this.computeLeaforder(columnOrder, rowOrder);
+      }
+      case 'name': {
+        return this.computeNameOrder(columnOrder, rowOrder);
+      }
+      case 'count': {
+        return this.computeCountOrder(columnOrder, rowOrder);
+      }
+      case 'barycenter': {
+        return this.computeBarycenter(columnOrder, rowOrder);
+      }
+      case 'rcm': {
+        return this.computeRCM(columnOrder, rowOrder);
+      }
+      case 'spectral': {
+        return this.computeSpectal(columnOrder, rowOrder);
+      }
+      // case 'pca': {
+      //   return this.computePCA(columnOrder, rowOrder);
+      // }
+      // case 'bfs': {
+      //   return this.computeBFS(columnOrder, rowOrder);
+      // }
+      case 'none' || 'identity': {
+        return { columnOrder, rowOrder };
+      }
+      default: {
+        return this.computeLeaforder(columnOrder, rowOrder);
+      }
+    }
+  };
+}
diff --git a/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx b/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx
new file mode 100644
index 000000000..ea5977146
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx
@@ -0,0 +1,130 @@
+import { Meta } from '@storybook/react';
+
+import { configureStore } from '@reduxjs/toolkit';
+import { Provider } from 'react-redux';
+import {
+  big2ndChamberQueryResult,
+  bigMockQueryResults,
+  big2ndChamberSchemaRaw,
+  smallFlightsQueryResults,
+  mockLargeQueryResults,
+} from '../../../mock-data';
+
+import { VisualizationPanel } from '../../panel/visualization';
+import {
+  assignNewGraphQueryResult,
+  graphQueryResultSlice,
+  querybuilderSlice,
+  schemaSlice,
+  setSchema,
+  visualizationSlice,
+} from '../../../data-access/store';
+
+import { SchemaUtils } from '../../../schema/schema-utils';
+import { simpleSchemaAirportRaw } from '../../../mock-data/schema/simpleAirportRaw';
+import { Visualizations, setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice';
+
+const Component: Meta<typeof VisualizationPanel> = {
+  title: 'Visualizations/MatrixVis',
+  component: VisualizationPanel,
+  decorators: [
+    (story) => (
+      <Provider store={Mockstore}>
+        <div
+          style={{
+            width: '100%',
+            height: '100vh',
+          }}
+        >
+          {story()}
+        </div>
+      </Provider>
+    ),
+  ],
+};
+
+const Mockstore: any = configureStore({
+  reducer: {
+    schema: schemaSlice.reducer,
+    graphQueryResult: graphQueryResultSlice.reducer,
+    visualize: visualizationSlice.reducer,
+    querybuilder: querybuilderSlice.reducer,
+  },
+});
+
+export const TestWithData = {
+  layout: 'fullscreen',
+  args: {
+    loading: false,
+  },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw);
+    dispatch(setSchema(schema.export()));
+    dispatch(
+      assignNewGraphQueryResult({
+        queryID: '1',
+        result: {
+          type: 'nodelink',
+          payload: {
+            nodes: [
+              { id: 'agent/007', attributes: { name: 'Daniel Craig' } },
+              { id: 'villain', attributes: { name: 'Le Chiffre' } },
+            ],
+            edges: [{ id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }],
+          },
+        },
+      })
+    );
+    dispatch(setActiveVisualization(Visualizations.Matrix));
+  },
+};
+
+export const TestWithNoData = {
+  args: { loading: false },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    dispatch(
+      assignNewGraphQueryResult({
+        queryID: '1',
+        result: {
+          type: 'nodelink',
+          payload: {
+            nodes: [],
+            edges: [],
+          },
+        },
+      })
+    );
+    dispatch(setActiveVisualization(Visualizations.Matrix));
+  },
+};
+
+export const TestWithBig2ndChamber = {
+  args: { loading: false },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } }));
+    dispatch(setActiveVisualization(Visualizations.Matrix));
+  },
+};
+
+export const TestWithSmallFlights = {
+  args: { loading: false },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: smallFlightsQueryResults } }));
+    dispatch(setActiveVisualization(Visualizations.Matrix));
+  },
+};
+
+export const TestWithLargeQueryResult = {
+  args: { loading: false },
+  play: async () => {
+    const dispatch = Mockstore.dispatch;
+    dispatch(assignNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockLargeQueryResults } }));
+    dispatch(setActiveVisualization(Visualizations.Matrix));
+  },
+};
+
+export default Component;
diff --git a/libs/shared/lib/vis/visualizations/matrix/matrixvis.module.scss b/libs/shared/lib/vis/visualizations/matrix/matrixvis.module.scss
new file mode 100644
index 000000000..915342c14
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/matrixvis.module.scss
@@ -0,0 +1,71 @@
+/**
+ * This program has been developed by students from the bachelor Computer Science at
+ * Utrecht University within the Software Project course.
+ * © Copyright Utrecht University (Department of Information and Computing Sciences)
+ */
+
+/* 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 NodeLinkConfigPanel
+.container {
+  font-family: 'Open Sans', sans-serif;
+  display: flex;
+  flex-direction: column;
+  p {
+    margin: 0.5rem 0;
+    font-size: 13px;
+    font-weight: 600;
+    color: #2d2d2d;
+  }
+  .title {
+    color: #212020;
+    font-weight: 800;
+    line-height: 1.6em;
+    font-size: 16px;
+    text-align: center;
+    margin-bottom: 1rem;
+    margin-top: 10px;
+  }
+  .subtitle {
+    color: #212020;
+    font-weight: 700;
+    font-size: 14px;
+    margin-top: 1.5rem;
+  }
+  .subsubtitle {
+    font-weight: 700;
+    margin-top: 0.9rem;
+  }
+  .subContainer {
+    .rulesContainer {
+      margin-top: 0.5rem;
+      .subsubtitle {
+        text-align: center;
+      }
+    }
+  }
+}
+
+.selectContainer {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  select {
+    width: 6rem;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    option {
+      width: 35px;
+      text-overflow: ellipsis;
+    }
+  }
+}
+
+#Slider {
+  width: 80%;
+  margin-left: 20px;
+}
diff --git a/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx b/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx
new file mode 100644
index 000000000..689a996ff
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx
@@ -0,0 +1,51 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { useImmer } from 'use-immer';
+import { GraphQueryResult, useGraphQueryResult } from '../../../data-access/store';
+import { ML } from '../../../data-access/store/mlSlice';
+import { LinkType, NodeType } from './Types';
+import { MatrixPixi } from './components/MatrixPixi';
+import { VisualizationPropTypes, VISComponentType, localConfigSchemaType } from '../../Types';
+
+interface Props {
+  loading?: boolean;
+  // currentColours: any;
+}
+
+// const displayName = 'Matrix Visualization';
+
+export const MatrixVis = React.memo(({ data, ml, dispatch, localConfig }: VisualizationPropTypes<typeof displayName>) => {
+  const ref = useRef<HTMLDivElement>(null);
+  const [graph, setGraph] = useImmer<GraphQueryResult | undefined>(undefined);
+  const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]);
+  const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]);
+
+  useEffect(() => {
+    if (data) {
+      setGraph(data);
+    }
+  }, [data, ml]);
+
+  return (
+    <>
+      <div className="h-full w-full overflow-hidden" ref={ref}>
+        <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} localConfig={localConfig} />
+      </div>
+    </>
+  );
+});
+
+const displayName = 'MatrixVis';
+const localConfigSchema: localConfigSchemaType = {
+  marks: {
+    type: 'dropdown',
+    options: ['rect', 'circle'],
+    value: 'rect',
+    label: 'Configure Marks',
+  },
+};
+
+export const MatrixVisComponent: VISComponentType = {
+  displayName: displayName,
+  VIS: MatrixVis,
+  localConfigSchema: localConfigSchema,
+};
diff --git a/libs/shared/package.json b/libs/shared/package.json
index f654e515f..13eebf5a6 100644
--- a/libs/shared/package.json
+++ b/libs/shared/package.json
@@ -51,6 +51,7 @@
     "keycloak-js": "^21.1.1",
     "lodash-es": "^4.17.21",
     "pixi-viewport": "^5.0.2",
+    "pixi-actions": "^1.1.10",
     "pixi.js": "^7.1.4",
     "react-color": "^2.19.3",
     "react-cookie": "^4.1.1",
@@ -69,7 +70,9 @@
     "tslib": "^2.5.0",
     "typed": "link:@deck.gl/core/typed",
     "use-immer": "^0.9.0",
-    "web-worker": "^1.2.0"
+    "web-worker": "^1.2.0",
+    "reorder.js": "^2.2.6",
+    "color-string": "^1.9.1"
   },
   "devDependencies": {
     "@iconify/json": "^2.2.95",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c584b0f6f..b5d231354 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -259,6 +259,9 @@ importers:
       color:
         specifier: ^4.2.3
         version: 4.2.3
+      color-string:
+        specifier: ^1.9.1
+        version: 1.9.1
       config:
         specifier: workspace:*
         version: link:../config
@@ -310,6 +313,9 @@ importers:
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
+      pixi-actions:
+        specifier: ^1.1.10
+        version: 1.1.10(pixi.js@7.2.1)
       pixi-viewport:
         specifier: ^5.0.2
         version: 5.0.2
@@ -346,6 +352,9 @@ importers:
       regenerator-runtime:
         specifier: 0.13.11
         version: 0.13.11
+      reorder.js:
+        specifier: ^2.2.6
+        version: 2.2.6
       sass:
         specifier: ^1.59.3
         version: 1.59.3
@@ -5962,6 +5971,10 @@ packages:
       string-argv: 0.3.1
     dev: true
 
+  /@sgratzl/science@2.0.0:
+    resolution: {integrity: sha512-LO3gArm8rVczcksg35xnZbOLEOE091VjyvvzR8mnPjP5K5+xqZEWhBi+lDcEk50dAlfPBoBHfwmu+MmiiSpXHw==}
+    dev: false
+
   /@sinclair/typebox@0.25.24:
     resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==}
     dev: true
@@ -16172,6 +16185,14 @@ packages:
     engines: {node: '>= 6'}
     dev: true
 
+  /pixi-actions@1.1.10(pixi.js@7.2.1):
+    resolution: {integrity: sha512-GLAcgMY8qWh9V/cVXbHEbnpxSb9hJRFYGOfjQyiEdJRUBVm4ZQ6j5mGnKTOV0DxFozpkdqw+WjNXcAKKdWeu8w==}
+    peerDependencies:
+      pixi.js: ^7.0.0
+    dependencies:
+      pixi.js: 7.2.1(@pixi/utils@7.2.1)
+    dev: false
+
   /pixi-viewport@5.0.2:
     resolution: {integrity: sha512-U77KnCTl81xEgxEQRFEuI7MYVySWwCVkA41EnM8KiOYwgVOwdBUa7318O+u61IOnTwnoYLzaihy/kpoONKU13Q==}
     dev: false
@@ -16987,6 +17008,7 @@ packages:
       react: 18.2.0
       scheduler: 0.19.1
     dev: false
+    bundledDependencies: false
 
   /react-dom@18.2.0(react@18.2.0):
     resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
@@ -17875,6 +17897,12 @@ packages:
       strip-ansi: 3.0.1
     dev: false
 
+  /reorder.js@2.2.6:
+    resolution: {integrity: sha512-mE0Vffgm6Bf2REDUCF5eNbYl0FAzuTzhAneokLEcnhWcamdwz9TwGLySrgYQDubqgr4bM85hRAcGG0q3kqCwCw==}
+    dependencies:
+      '@sgratzl/science': 2.0.0
+    dev: false
+
   /request@2.88.2:
     resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
     engines: {node: '>= 6'}
-- 
GitLab