From 1887fb94eb07eac708f38687a5d1b72d11ca6324 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Wed, 4 Sep 2024 12:30:31 +0200 Subject: [PATCH] feat(nl): export png from nodelink --- .../nodelinkvis/components/NLPixi.tsx | 69 +++++++- .../nodelinkvis/nodelinkvis.tsx | 166 ++++++++++-------- 2 files changed, 160 insertions(+), 75 deletions(-) diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 52b711cb7..c0434d43d 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,6 +1,6 @@ import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types'; import { dataColors, visualizationColors } from 'config'; -import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react'; import { Application, Color, @@ -47,7 +47,7 @@ type LayoutState = 'reset' | 'running' | 'paused'; // MAIN COMPONENT ////////////////// -export const NLPixi = (props: Props) => { +export const NLPixi = forwardRef((props: Props, refExternal) => { const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]); @@ -334,6 +334,67 @@ export const NLPixi = (props: Props) => { }, })); + useImperativeHandle(refExternal, () => ({ + exportImage() { + const captureImage = () => { + const canvas = ref.current?.querySelector('canvas') as HTMLCanvasElement; + if (canvas) { + canvas.toBlob((blob) => { + if (blob) { + const imageUrl = URL.createObjectURL(blob); + const whiteCanvas = document.createElement('canvas'); + whiteCanvas.width = canvas.width; + whiteCanvas.height = canvas.height; + const ctx = whiteCanvas.getContext('2d'); + if (ctx) { + // Draw a white background + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, whiteCanvas.width, whiteCanvas.height); + + // Draw the original canvas image on top + const img = new Image(); + img.src = imageUrl; + img.onload = () => { + ctx.drawImage(img, 0, 0); + + // Now export the combined image + const finalImage = whiteCanvas.toDataURL('image/png'); + + const link = document.createElement('a'); + link.href = finalImage; + link.download = 'nodelinkvis.png'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Revoke the object URL to free up memory + URL.revokeObjectURL(imageUrl); + }; + img.onerror = (err) => { + console.error('Failed to load image', err); + }; + } else { + console.error('2D context not found on the new canvas'); + } + } else { + console.error('Failed to convert canvas to Blob'); + } + }, 'image/png'); + } else { + console.error('Canvas element not found'); + } + }; + + const renderCanvas = () => { + requestAnimationFrame(() => { + captureImage(); + }); + }; + + renderCanvas(); + }, + })); + function resize() { const width = ref?.current?.clientWidth || 1000; const height = ref?.current?.clientHeight || 1000; @@ -903,6 +964,8 @@ export const NLPixi = (props: Props) => { layoutAlgorithm.current.layout(graphologyGraph, boundingBox); }; + // export image + return ( <> {popups.map((popup) => ( @@ -937,4 +1000,4 @@ export const NLPixi = (props: Props) => { ></div> </> ); -}; +}); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index 4679606a3..5c4c23202 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'; import { GraphType, LinkType, NodeType, NodeTypeD3 } from './types'; import { NLPixi } from './components/NLPixi'; import { parseQueryResult } from './components/query2NL'; @@ -14,6 +14,10 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul import { IPointData } from 'pixi.js'; import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; +export interface NodeLinkVisHandle { + exportImageInternal: () => void; +} + export interface NodelinkVisProps { id: string; name: string; @@ -49,78 +53,91 @@ const settings: NodelinkVisProps = { nodeList: [], }; -export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSelect }: VisualizationPropTypes<NodelinkVisProps>) => { - const ref = useRef<HTMLDivElement>(null); - const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); - const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); - const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]); - - useEffect(() => { - if (data) { - setGraph( - parseQueryResult(data, ml, { - defaultX: (ref.current?.clientWidth || 1000) / 2, - defaultY: (ref.current?.clientHeight || 1000) / 2, - }), - ); - } - }, [data, ml]); - - const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => { - if (graph) { - if (!event?.node) { - handleSelect(); - return; +const NodeLinkVis = forwardRef<NodeLinkVisHandle, VisualizationPropTypes<NodelinkVisProps>>( + ({ data, ml, dispatch, settings, handleSelect }, refExternal) => { + const ref = useRef<HTMLDivElement>(null); + const nlPixiRef = useRef<any>(null); + const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); + const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); + const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]); + + useEffect(() => { + if (data) { + setGraph( + parseQueryResult(data, ml, { + defaultX: (ref.current?.clientWidth || 1000) / 2, + defaultY: (ref.current?.clientHeight || 1000) / 2, + }), + ); } - - const node = event.node; - const nodeMeta = graph.nodes[node._id]; - handleSelect({ nodes: [nodeMeta as Node] }); - - if (ml && ml.shortestPath.enabled) { - setGraph((draft) => { - let _node = draft?.nodes[node._id]; - if (!_node) return draft; - - if (!ml.shortestPath.srcNode) { - _node.isShortestPathSource = true; - dispatch(setShortestPathSource(node._id)); - } else if (ml.shortestPath.srcNode === node._id) { - _node.isShortestPathSource = false; - dispatch(setShortestPathSource(undefined)); - } else if (!ml.shortestPath.trtNode) { - _node.isShortestPathTarget = true; - dispatch(setShortestPathTarget(node._id)); - } else if (ml.shortestPath.trtNode === node._id) { - _node.isShortestPathTarget = false; - dispatch(setShortestPathTarget(undefined)); - } else { - _node.isShortestPathSource = true; - _node.isShortestPathTarget = false; - dispatch(setShortestPathSource(node._id)); - dispatch(setShortestPathTarget(undefined)); - } - return draft; - }); + }, [data, ml]); + + const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => { + if (graph) { + if (!event?.node) { + handleSelect(); + return; + } + + const node = event.node; + const nodeMeta = graph.nodes[node._id]; + handleSelect({ nodes: [nodeMeta as Node] }); + + if (ml && ml.shortestPath.enabled) { + setGraph((draft) => { + let _node = draft?.nodes[node._id]; + if (!_node) return draft; + + if (!ml.shortestPath.srcNode) { + _node.isShortestPathSource = true; + dispatch(setShortestPathSource(node._id)); + } else if (ml.shortestPath.srcNode === node._id) { + _node.isShortestPathSource = false; + dispatch(setShortestPathSource(undefined)); + } else if (!ml.shortestPath.trtNode) { + _node.isShortestPathTarget = true; + dispatch(setShortestPathTarget(node._id)); + } else if (ml.shortestPath.trtNode === node._id) { + _node.isShortestPathTarget = false; + dispatch(setShortestPathTarget(undefined)); + } else { + _node.isShortestPathSource = true; + _node.isShortestPathTarget = false; + dispatch(setShortestPathSource(node._id)); + dispatch(setShortestPathTarget(undefined)); + } + return draft; + }); + } } - } - }; + }; - if (!graph) return null; - return ( - <NLPixi - graph={graph} - configuration={settings} - highlightNodes={highlightNodes} - highlightedLinks={highlightedLinks} - onClick={(event) => { - onClickedNode(event, ml); - }} - layoutAlgorithm={settings.layout} - showPopupsOnHover={settings.showPopUpOnHover} - /> - ); -}); + const exportImageInternal = () => { + nlPixiRef.current.exportImage(); + }; + + useImperativeHandle(refExternal, () => ({ + exportImageInternal, + })); + + if (!graph) return null; + + return ( + <NLPixi + ref={nlPixiRef} + graph={graph} + configuration={settings} + highlightNodes={highlightNodes} + highlightedLinks={highlightedLinks} + onClick={(event) => { + onClickedNode(event, ml); + }} + layoutAlgorithm={settings.layout} + showPopupsOnHover={settings.showPopUpOnHover} + /> + ); + }, +); const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<NodelinkVisProps>) => { useEffect(() => { @@ -216,15 +233,20 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza </SettingsContainer> ); }; +const nodeLinkVisRef = React.createRef<{ exportImageInternal: () => void }>(); export const NodeLinkComponent: VISComponentType<NodelinkVisProps> = { displayName: 'NodeLinkVis', description: 'General Patterns and Connections', - component: NodeLinkVis, + component: React.forwardRef((props: VisualizationPropTypes<NodelinkVisProps>, ref) => <NodeLinkVis {...props} ref={nodeLinkVisRef} />), settingsComponent: NodelinkSettings, settings: settings, exportImage: () => { - alert('Not yet supported'); + if (nodeLinkVisRef.current) { + nodeLinkVisRef.current.exportImageInternal(); + } else { + console.error('NodeLink reference is not set.'); + } }, }; -- GitLab