From 130f321357f3aaee281a97325e4e62bfd7fddcd8 Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Wed, 4 Sep 2024 17:14:32 +0000
Subject: [PATCH] Feat/export matrix vis

---
 .../matrixvis/components/MatrixPixi.tsx       | 81 +++++++++++++++----
 .../visualizations/matrixvis/matrixvis.tsx    | 40 +++++++--
 2 files changed, 100 insertions(+), 21 deletions(-)

diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
index e1e82846d..489596fd8 100644
--- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
@@ -2,7 +2,7 @@ import { Edge, GraphQueryResult, Node, useML, useSearchResultData } from '@graph
 import { dataColors, visualizationColors } from 'config';
 import { Viewport } from 'pixi-viewport';
 import { Application, ColorSource, Container, FederatedPointerEvent, Graphics, IPointData, Point, Text } from 'pixi.js';
-import { useEffect, useRef, useState, useImperativeHandle } from 'react';
+import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
 import { LinkType, NodeType } from '../types';
 import { NLPopup } from './MatrixPopup';
 
@@ -28,6 +28,7 @@ import {
 import { MatrixVisProps } from '../matrixvis';
 import { Theme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { useConfig } from '@graphpolaris/shared/lib/data-access/store';
+import html2canvas from 'html2canvas';
 
 const styleMatrixSize = 50;
 
@@ -48,7 +49,7 @@ const columnsContainer = new Container();
 // MAIN COMPONENT
 //////////////////
 
-export const MatrixPixi = (props: Props) => {
+export const MatrixPixi = forwardRef((props: Props, refExternal) => {
   let config = {
     textOffsetX: 50,
     textOffsetY: 50,
@@ -74,12 +75,20 @@ export const MatrixPixi = (props: Props) => {
   // const [columnOrder, setColumnOrder] = useState<string[]>([]);
 
   const viewport = useRef<Viewport>();
-  const ref = useRef<HTMLDivElement>(null);
+  const internalRef = useRef<HTMLDivElement>(null);
   const canvas = useRef<HTMLCanvasElement>(null);
   const svg = useRef<SVGSVGElement>(null);
   const isSetup = useRef(false);
   const ml = useML();
 
+  useEffect(() => {
+    if (typeof refExternal === 'function') {
+      refExternal(internalRef.current);
+    } else if (refExternal) {
+      (refExternal as React.MutableRefObject<HTMLDivElement | null>).current = internalRef.current;
+    }
+  }, [refExternal]);
+
   const imperative = useRef<any>(null);
   useImperativeHandle(imperative, () => ({
     getBackgroundColor() {
@@ -87,12 +96,49 @@ export const MatrixPixi = (props: Props) => {
       return globalConfig.theme === Theme.dark ? 0x121621 : 0xffffff;
     },
   }));
+  useImperativeHandle(refExternal, () => ({
+    exportImage() {
+      const captureImage = () => {
+        const element = internalRef.current; // The container that holds both canvas and SVG
+
+        if (element) {
+          html2canvas(element, {
+            backgroundColor: '#FFFFFF', // Set background color to white
+          })
+            .then((canvas) => {
+              const finalImage = canvas.toDataURL('image/png');
+
+              // Download the final image
+              const link = document.createElement('a');
+              link.href = finalImage;
+              link.download = 'matrixvis.png';
+              document.body.appendChild(link);
+              link.click();
+              document.body.removeChild(link);
+            })
+            .catch((error) => {
+              console.error('Error capturing image:', error);
+            });
+        } else {
+          console.error('Container element not found');
+        }
+      };
+
+      const renderCanvas = () => {
+        requestAnimationFrame(() => {
+          captureImage();
+        });
+      };
+
+      renderCanvas();
+    },
+  }));
 
   let app: Application;
 
   function resize() {
-    const width = ref?.current?.clientWidth || 1000;
-    const height = ref?.current?.clientHeight || 1000;
+    const width = internalRef?.current?.clientWidth || 1000;
+    const height = internalRef?.current?.clientHeight || 1000;
 
     app.renderer.resize(width, height);
     if (viewport.current) {
@@ -111,7 +157,7 @@ export const MatrixPixi = (props: Props) => {
 
   useEffect(() => {
     // 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 (props.graph && internalRef.current && internalRef.current.children.length > 0) {
       if (!isSetup.current) setup();
       else update();
     }
@@ -119,7 +165,7 @@ export const MatrixPixi = (props: Props) => {
 
   useEffect(() => {
     // 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 (props.graph && internalRef.current && internalRef.current.children.length > 0) {
       setup();
     }
   }, [props.settings]);
@@ -152,7 +198,7 @@ export const MatrixPixi = (props: Props) => {
 
   const update = (forceClear = false) => {
     setPopups([]);
-    if (!props.graph || !ref.current) return;
+    if (!props.graph || !internalRef.current) return;
 
     if (props.graph) {
       if (forceClear) {
@@ -274,7 +320,7 @@ export const MatrixPixi = (props: Props) => {
       const resizeObserver = new ResizeObserver(() => {
         resize();
       });
-      resizeObserver.observe(ref.current as HTMLDivElement);
+      resizeObserver.observe(internalRef.current as HTMLDivElement);
     }
 
     if (svg.current != null) {
@@ -284,7 +330,7 @@ export const MatrixPixi = (props: Props) => {
     columnsContainer.removeChildren();
     app.stage.removeChildren();
 
-    const size = ref.current?.getBoundingClientRect();
+    const size = internalRef.current?.getBoundingClientRect();
     if (viewport.current == null) {
       viewport.current = new Viewport({
         screenWidth: size?.width || 1000,
@@ -492,7 +538,7 @@ export const MatrixPixi = (props: Props) => {
       .text(label);
 
     // Click handler for reordering columns
-    const axisTopHandle = ref.current?.querySelector(`.axisTop`) as HTMLDivElement;
+    const axisTopHandle = internalRef.current?.querySelector(`.axisTop`) as HTMLDivElement;
     axisTopHandle.addEventListener('click', () => {
       if (!props.graph) throw new Error('Graph is undefined; cannot reorder matrix');
 
@@ -590,7 +636,7 @@ export const MatrixPixi = (props: Props) => {
       .text(label);
 
     // Click handler for reordering columns
-    const axisLeftHandle = ref.current?.querySelector(`.axisLeft`) as HTMLDivElement;
+    const axisLeftHandle = internalRef.current?.querySelector(`.axisLeft`) as HTMLDivElement;
     axisLeftHandle.addEventListener('click', () => {
       if (!props.graph) throw new Error('Graph is undefined; cannot reorder matrix');
 
@@ -635,7 +681,7 @@ export const MatrixPixi = (props: Props) => {
         const halfHeight = (this as SVGTextElement).getBBox().width / 2;
         const y = Math.max(
           config.textOffsetY + halfHeight,
-          Math.min((ref.current?.clientHeight ?? 0) - halfHeight, (scaleRows.range()[1] + scaleRows.range()[0]) / 2),
+          Math.min((internalRef.current?.clientHeight ?? 0) - halfHeight, (scaleRows.range()[1] + scaleRows.range()[0]) / 2),
         );
 
         return `translate(
@@ -661,7 +707,7 @@ export const MatrixPixi = (props: Props) => {
         <NLPopup onClose={() => {}} data={popup} key={popup.node.id} />
       ))}
       {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />}
-      <div ref={ref} className={`h-full w-full overflow-hidden relative matrix`}>
+      <div ref={internalRef} className={`h-full w-full overflow-hidden relative matrix`}>
         <canvas ref={canvas}></canvas>
         <div
           className={`axisLeft`}
@@ -686,7 +732,10 @@ export const MatrixPixi = (props: Props) => {
             height: styleMatrixSize,
             backdropFilter: 'blur(10px)',
             background: globalConfig.theme === Theme.dark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255, 0.5)',
-            boxShadow: globalConfig.theme === Theme.dark ? `${styleMatrixSize}px 1px 0px 0px rgba(255,255,255,0.2)` : `${styleMatrixSize}px 1px 0px 0px rgba(0,0,0,0.2)`,
+            boxShadow:
+              globalConfig.theme === Theme.dark
+                ? `${styleMatrixSize}px 1px 0px 0px rgba(255,255,255,0.2)`
+                : `${styleMatrixSize}px 1px 0px 0px rgba(0,0,0,0.2)`,
           }}
         ></div>
         <svg
@@ -698,4 +747,4 @@ export const MatrixPixi = (props: Props) => {
       </div>
     </>
   );
-};
+});
diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx
index 511e9cd6a..4dc4b26a9 100644
--- a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx
+++ b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
 import { useImmer } from 'use-immer';
 import { GraphQueryResult } from '../../../data-access/store';
 import { LinkType, NodeType } from './types';
@@ -18,22 +18,46 @@ const settings: MatrixVisProps = {
   color: 'blue',
 };
 
-export const MatrixVis = React.memo(({ data, ml, settings }: VisualizationPropTypes<MatrixVisProps>) => {
+export interface MatrixVisHandle {
+  exportImageInternal: () => void;
+}
+
+const MatrixVis = forwardRef<MatrixVisHandle, VisualizationPropTypes<MatrixVisProps>>(({ data, ml, settings }, refExternal) => {
   const ref = useRef<HTMLDivElement>(null);
   const [graph, setGraph] = useImmer<GraphQueryResult | undefined>(undefined);
   const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]);
   const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]);
 
+  const matrixPixiRef = useRef<any>(null);
+
   useEffect(() => {
     if (data) {
       setGraph(data);
     }
   }, [data, ml]);
 
+  const exportImageInternal = () => {
+    matrixPixiRef.current.exportImage();
+  };
+
+  useImperativeHandle(
+    refExternal,
+    () => ({
+      exportImageInternal,
+    }),
+    [],
+  );
+
   return (
     <>
       <div className="h-full w-full overflow-hidden" ref={ref}>
-        <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} settings={settings} />
+        <MatrixPixi
+          ref={matrixPixiRef}
+          graph={graph}
+          highlightNodes={highlightNodes}
+          highlightedLinks={highlightedLinks}
+          settings={settings}
+        />
       </div>
     </>
   );
@@ -61,14 +85,20 @@ const MatrixSettings = ({ settings, updateSettings }: VisualizationSettingsPropT
   );
 };
 
+const matrixVisRef = React.createRef<{ exportImageInternal: () => void }>();
+
 export const MatrixVisComponent: VISComponentType<MatrixVisProps> = {
   displayName: 'MatrixVis',
   description: 'Overview & Details',
-  component: MatrixVis,
+  component: React.forwardRef((props: VisualizationPropTypes<MatrixVisProps>, ref) => <MatrixVis {...props} ref={matrixVisRef} />),
   settingsComponent: MatrixSettings,
   settings: settings,
   exportImage: () => {
-    alert('Not yet supported');
+    if (matrixVisRef.current) {
+      matrixVisRef.current.exportImageInternal();
+    } else {
+      console.error('MatrixVis reference is not set.');
+    }
   },
 };
 
-- 
GitLab