diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx index e1e82846d02bff61288d987b240caf44c2d60ec1..489596fd810f764db03081f2249ae02f0247a68c 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 511e9cd6ac98b84302f262b5d6ad689ad1b5ee50..4dc4b26a923485c72144bf947d29098f28e8f58f 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.'); + } }, };