From 8584ffa4df7a411df2d03c151505cf9ceaa48944 Mon Sep 17 00:00:00 2001 From: Milho001 <l.milhomemfrancochristino@uu.nl> Date: Thu, 4 Jul 2024 12:39:17 +0000 Subject: [PATCH] fix(vis): remove read-only bug from nl required refactor of how the graph data structure is handled by nl --- .../vis/components/config/SelectionConfig.tsx | 7 +- .../components/NLMachineLearning.tsx | 40 ++--- .../nodelinkvis/components/NLPixi.tsx | 139 +++++++++--------- .../nodelinkvis/components/query2NL.tsx | 34 +++-- .../nodelinkvis/components/utils.tsx | 5 +- .../nodelinkvis/nodelinkvis.tsx | 12 +- .../vis/visualizations/nodelinkvis/types.ts | 41 +++--- 7 files changed, 142 insertions(+), 136 deletions(-) diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx index 7d41cf4a1..1d5e96f04 100644 --- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx +++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/store/interactionSlice'; import { Delete } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; @@ -25,8 +26,8 @@ export const SelectionConfig = () => { /> </div> {selection.content.map((item, index) => ( - <> - <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1"> + <React.Fragment key={index + 'id'}> + <div className="flex justify-between items-center px-4 py-1 gap-1"> <span className="text-xs font-normal">ID</span> <span className="text-xs">{item._id}</span> </div> @@ -43,7 +44,7 @@ export const SelectionConfig = () => { </div> ); })} - </> + </React.Fragment> ))} </div> ); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx index 78d36400c..19c50b629 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx @@ -6,18 +6,18 @@ export function processLinkPrediction(ml: ML, graph: GraphType): GraphType { if (ml === undefined || ml.linkPrediction === undefined) return graph; if (ml.linkPrediction.enabled) { - let allNodeIds = new Set(graph.nodes.map((n) => n._id)); + let allNodeIds = new Set(Object.keys(graph.nodes)); ml.linkPrediction.result.forEach((link) => { if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) { const toAdd: LinkType = { - id: link.from + link.to, + id: link.from + ':LP:' + link.to, // TODO: this only supports one link between two nodes source: link.from, target: link.to, value: link.attributes.jaccard_coefficient as number, mlEdge: true, color: 0x000000, }; - graph.links.push(toAdd); + graph.links[toAdd.id] = toAdd; } }); } @@ -35,18 +35,16 @@ export function processCommunityDetection(ml: ML, graph: GraphType): GraphType { }); }); - graph.nodes = graph.nodes.map((node, i) => { - if (allNodeIdMap.has(node._id)) { - node.cluster = allNodeIdMap.get(node._id); + Object.keys(graph.nodes).forEach((nodeId) => { + if (allNodeIdMap.has(nodeId)) { + graph.nodes[nodeId].cluster = allNodeIdMap.get(nodeId); } else { - node.cluster = -1; + graph.nodes[nodeId].cluster = -1; } - return node; }); } else { - graph.nodes = graph.nodes.map((node, i) => { - node.cluster = undefined; - return node; + Object.keys(graph.nodes).forEach((nodeId) => { + graph.nodes[nodeId].cluster = undefined; }); } return graph; @@ -101,6 +99,7 @@ export const useNLMachineLearning = (props: { * Gets the edges corresponding to the shortestPath. * @param pathString The path as a string. * @returns The path as a LinkType[] + * @deprecated This function is not working anymore */ function getShortestPathEdges(pathString: string[]): LinkType[] { try { @@ -112,13 +111,14 @@ export const useNLMachineLearning = (props: { continue; } let edgeFound = false; - props.graph.links.forEach((link: any) => { - const { source, target } = link; + Object.keys(props.graph.links).forEach((key) => { + const link = props.graph.links[key]; if ( - (pathString[index] == source.id && pathString[index + 1] == target.id) || - (pathString[index] == source && pathString[index + 1] == target) || - (pathString[index + 1] == source.id && pathString[index] == target.id) || - (pathString[index + 1] == source && pathString[index] == target) + false // FIXME: This is not working anymore + // (pathString[index] == source.id && pathString[index + 1] == target.id) || + // (pathString[index] == source && pathString[index + 1] == target) || + // (pathString[index + 1] == source.id && pathString[index] == target.id) || + // (pathString[index + 1] == source && pathString[index] == target) ) { newPath.push(link); edgeFound = true; @@ -172,10 +172,10 @@ export const useNLMachineLearning = (props: { * after a community detection algorithm, where the cluster of these nodes could have been changed. */ const resetClusterOfNodes = (type: number): void => { - props.graph.nodes.forEach((node: NodeType) => { - const numberOfClusters = props.numberOfMlClusters; + Object.keys(props.graph.nodes).forEach((key) => { + const node = props.graph.nodes[key]; if (node.cluster == type) { - node.cluster = numberOfClusters; + node.cluster = props.numberOfMlClusters; } if (node.type == type) { node.cluster = node.type; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 36ea28f65..6357b96bd 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,4 +1,4 @@ -import { GraphType, LinkType, NodeType } from '../types'; +import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types'; import { dataColors, visualizationColors } from 'config'; import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { @@ -19,19 +19,18 @@ import { NLPopup } from './NLPopup'; import { hslStringToHex, nodeColor } from './utils'; import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout'; import { MultiGraph } from 'graphology'; -import { VisualizationSettingsType } from '../../../common'; import { Viewport } from 'pixi-viewport'; import { NodelinkVisProps } from '../nodelinkvis'; type Props = { - onClick: (event?: { node: NodeType; pos: IPointData }) => void; + onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void; // onHover: (data: { node: NodeType; pos: IPointData }) => void; // onUnHover: (data: { node: NodeType; pos: IPointData }) => void; highlightNodes: NodeType[]; configuration: NodelinkVisProps; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; - graph?: GraphType; + graph: GraphType; layoutAlgorithm: string; showPopupsOnHover: boolean; }; @@ -69,9 +68,10 @@ export const NLPixi = (props: Props) => { const mouseInCanvas = useRef<boolean>(false); const isSetup = useRef(false); const ml = useML(); - const dragging = useRef<{ node: NodeType; gfx: Sprite } | null>(null); + const dragging = useRef<{ node: NodeTypeD3; gfx: Sprite } | null>(null); const onlyClicked = useRef(false); const searchResults = useSearchResultData(); + const graph = useRef<GraphTypeD3>({ nodes: [], links: [] }); const layoutAlgorithm = useRef<CytoscapeLayout | GraphologyLayout>(new LayoutFactory().createLayout(Layouts.DAGRE)); @@ -123,7 +123,7 @@ export const NLPixi = (props: Props) => { const imperative = useRef<any>(null); useImperativeHandle(imperative, () => ({ - onDragStart(node: NodeType, gfx: Sprite) { + onDragStart(node: NodeTypeD3, gfx: Sprite) { dragging.current = { node, gfx }; onlyClicked.current = true; @@ -167,7 +167,7 @@ export const NLPixi = (props: Props) => { setPopups(popups.filter((p) => p.node._id !== dragging.current?.node._id)); props.onClick(); } else { - setPopups([...popups, { node: dragging.current.node, pos: toGlobal(dragging.current.node) }]); + setPopups([...popups, { node: props.graph.nodes[dragging.current.node._id], pos: toGlobal(dragging.current.node) }]); props.onClick({ node: dragging.current.node, pos: toGlobal(dragging.current.node) }); } } @@ -176,7 +176,7 @@ export const NLPixi = (props: Props) => { } else { } }, - onHover(node: NodeType) { + onHover(node: NodeTypeD3) { if ( mouseInCanvas.current && viewport?.current && @@ -184,7 +184,7 @@ export const NLPixi = (props: Props) => { node && popups.filter((p) => p.node._id === node._id).length === 0 ) { - setQuickPopup({ node, pos: toGlobal(node) }); + setQuickPopup({ node: props.graph.nodes[node._id], pos: toGlobal(node) }); } }, onUnHover() { @@ -224,7 +224,7 @@ export const NLPixi = (props: Props) => { } }, [ref]); - function toGlobal(node: NodeType): IPointData { + function toGlobal(node: NodeTypeD3): IPointData { if (viewport?.current) { // const rect = ref.current?.getBoundingClientRect(); const rect = { x: 0, y: 0 }; @@ -234,7 +234,7 @@ export const NLPixi = (props: Props) => { } else return { x: 0, y: 0 }; } - function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Sprite) { + function onDragStart(event: FederatedPointerEvent, node: NodeTypeD3, gfx: Sprite) { event.stopPropagation(); if (imperative.current) imperative.current.onDragStart(node, gfx); } @@ -249,19 +249,20 @@ export const NLPixi = (props: Props) => { if (imperative.current) imperative.current.onDragEnd(); } - const updateNode = (node: NodeType) => { + const updateNode = (node: NodeTypeD3) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; // Update texture when selected - const texture = Assets.get(textureId(node.selected)); + const nodeMeta = props.graph.nodes[node._id]; + const texture = Assets.get(textureId(nodeMeta.selected)); gfx.texture = texture; // Cluster colors - if (node?.cluster) { - gfx.tint = node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000; + if (nodeMeta?.cluster) { + gfx.tint = nodeMeta.cluster >= 0 ? nodeColor(nodeMeta.cluster) : 0x000000; } else { - gfx.tint = nodeColor(node.type); + gfx.tint = nodeColor(nodeMeta.type); } gfx.position.set(node.x, node.y); @@ -296,7 +297,9 @@ export const NLPixi = (props: Props) => { // } }; - const createNode = (node: NodeType, selected?: boolean) => { + const createNode = (node: NodeTypeD3, selected?: boolean) => { + const nodeMeta = props.graph.nodes[node._id]; + // check if node is already drawn, and if so, delete it if (node && node?._id && nodeMap.current.has(node._id)) { nodeMap.current.delete(node._id); @@ -308,14 +311,13 @@ export const NLPixi = (props: Props) => { const texture = Assets.get(textureId()); gfx = new Sprite(texture); - gfx.tint = nodeColor(node.type); - const scale = (Math.max(node.radius || 5, 5) / 70) * 2; + gfx.tint = nodeColor(nodeMeta.type); + const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2; gfx.scale.set(scale, scale); gfx.anchor.set(0.5, 0.5); nodeMap.current.set(node._id, gfx); nodeLayer.addChild(gfx); - node.selected = selected; updateNode(node); gfx.name = 'node_' + node._id; @@ -332,30 +334,30 @@ export const NLPixi = (props: Props) => { // }); // }; - const updateLink = (link: LinkType) => { + const updateLink = (link: LinkTypeD3) => { if (!props.graph || nodeMap.current.size === 0) return; + const linkMeta = props.graph.links[link._id]; const _source = link.source; const _target = link.target; if (!_source || !_target) { - console.log('source or target not found', _source, _target); return; } let sourceId = ''; let targetId = ''; - let source: NodeType | undefined; - let target: NodeType | undefined; + let source: NodeTypeD3 | undefined; + let target: NodeTypeD3 | undefined; if (typeof _source === 'string') { sourceId = link.source as string; targetId = link.target as string; - source = nodeMap.current.get(sourceId) as NodeType | undefined; - target = nodeMap.current.get(targetId) as NodeType | undefined; + source = nodeMap.current.get(sourceId) as NodeTypeD3 | undefined; + target = nodeMap.current.get(targetId) as NodeTypeD3 | undefined; } else { - source = link.source as NodeType; - target = link.target as NodeType; + source = link.source as NodeTypeD3; + target = link.target as NodeTypeD3; sourceId = source._id; targetId = target._id; } @@ -368,28 +370,28 @@ export const NLPixi = (props: Props) => { // let color = link.color || 0x000000; let color = config.LINE_COLOR_DEFAULT; let style = config.LINE_WIDTH_DEFAULT; - let alpha = link.alpha || 1; - if (link.mlEdge) { + let alpha = linkMeta.alpha || 1; + if (linkMeta.mlEdge) { color = config.LINE_COLOR_ML; - if (link.value > ml.communityDetection.jaccard_threshold) { - style = link.value * 1.8; + if (linkMeta.value > ml.communityDetection.jaccard_threshold) { + style = linkMeta.value * 1.8; } else { style = 0; alpha = 0.2; } - } else if (props.highlightedLinks && props.highlightedLinks.includes(link)) { - if (link.mlEdge && ml.communityDetection.jaccard_threshold) { - if (link.value > ml.communityDetection.jaccard_threshold) { + } else if (props.highlightedLinks && props.highlightedLinks.includes(linkMeta)) { + if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) { + if (linkMeta.value > ml.communityDetection.jaccard_threshold) { color = dataColors.magenta[50]; // 0xaa00ff; - style = link.value * 1.8; + style = linkMeta.value * 1.8; } } else { color = dataColors.red[70]; // color = 0xff0000; style = 1.0; } - } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(link)) { + } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) { color = dataColors.green[50]; // color = 0x00ff00; style = 3.0; @@ -398,7 +400,7 @@ export const NLPixi = (props: Props) => { // Conditional alpha for search results if (searchResults.nodes.length > 0 || searchResults.edges.length > 0) { // FIXME: searchResults.edges should be a hashmap to improve performance. - const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id); + const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link._id); alpha = isLinkInSearchResults ? 1 : 0.05; } @@ -438,11 +440,11 @@ export const NLPixi = (props: Props) => { if (isSetup.current === false) setup(); else update(false); } - }, [props.graph, config, assetsLoaded]); + }, [config, assetsLoaded]); useEffect(() => { if (props.graph) { - props.graph.nodes.forEach((node: NodeType) => { + graph.current.nodes.forEach((node) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id); @@ -463,7 +465,7 @@ export const NLPixi = (props: Props) => { const widthHalf = app.renderer.width / 2; const heightHalf = app.renderer.height / 2; - props.graph.nodes.forEach((node: NodeType, i) => { + graph.current.nodes.forEach((node, i) => { if (!layoutAlgorithm.current) return; const gfx = nodeMap.current.get(node._id); if (!gfx || node.x === undefined || node.y === undefined) return; @@ -474,24 +476,19 @@ export const NLPixi = (props: Props) => { stopped += 1; return; } - try { - if (layoutAlgorithm.current.provider === 'Graphology') { - // this is a dirty hack to fix the graphology layout being out of bounds - node.x = position.x + widthHalf; - node.y = position.y + heightHalf; - } else { - node.x = position.x; - node.y = position.y; - } - } catch (e) { - // node.x and .y become read-only when some layout algorithms are finished - layoutState.current = 'paused'; + if (layoutAlgorithm.current.provider === 'Graphology') { + // this is a dirty hack to fix the graphology layout being out of bounds + node.x = position.x + widthHalf; + node.y = position.y + heightHalf; + } else { + node.x = position.x; + node.y = position.y; } gfx.position.copyFrom(node as IPointData); }); - if (stopped === props.graph.nodes.length) { + if (stopped === graph.current.nodes.length) { layoutStoppedCount.current = layoutStoppedCount.current + 1; if (layoutStoppedCount.current > 1000) { layoutState.current = 'paused'; @@ -507,7 +504,7 @@ export const NLPixi = (props: Props) => { // Draw the links linkGfx.clear(); linkGfx.beginFill(); - props.graph.links.forEach((link: any) => { + graph.current.links.forEach((link: any) => { updateLink(link); }); linkGfx.endFill(); @@ -526,7 +523,7 @@ export const NLPixi = (props: Props) => { } nodeMap.current.forEach((gfx, id) => { - if (!props.graph?.nodes?.find((node) => node._id === id)) { + if (!graph.current.nodes.find((node) => node._id === id)) { nodeLayer.removeChild(gfx); gfx.destroy(); nodeMap.current.delete(id); @@ -535,17 +532,13 @@ export const NLPixi = (props: Props) => { linkGfx.clear(); - props.graph.nodes.forEach((node: NodeType) => { + graph.current.nodes.forEach((node) => { if (!forceClear && nodeMap.current.has(node._id)) { const old = nodeMap.current.get(node._id); - try { - node.x = old?.x || node.x; - node.y = old?.y || node.y; - updateNode(node); - } catch (e) { - // node.x and .y become read-only when some layout algorithms are finished - } + node.x = old?.x || node.x; + node.y = old?.y || node.y; + updateNode(node); } else { createNode(node); } @@ -583,6 +576,16 @@ export const NLPixi = (props: Props) => { if (!props.graph) throw Error('Graph is undefined'); + //Setup d3 graph structure + graph.current = { + nodes: Object.values(props.graph.nodes).map((n) => ({ _id: n._id, x: n.defaultX, y: n.defaultY })), + links: Object.values(props.graph.links).map((l) => ({ + _id: l.id, + source: l.source, + target: l.target, + })), + }; + const size = ref.current?.getBoundingClientRect(); viewport.current = new Viewport({ screenWidth: size?.width || 1000, @@ -620,12 +623,12 @@ export const NLPixi = (props: Props) => { if (!layoutAlgorithm) throw Error('LayoutAlgorithm is undefined'); const graphologyGraph = new MultiGraph(); - props.graph?.nodes.forEach((node) => { - if (forceClear) graphologyGraph.addNode(node._id, { size: node.radius || 5 }); - else graphologyGraph.addNode(node._id, { size: node.radius || 5, x: node.x || 0, y: node.y || 0 }); + graph.current.nodes.forEach((node) => { + if (forceClear) graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5 }); + else graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5, x: node.x || 0, y: node.y || 0 }); }); - props.graph?.links.forEach((link) => { + graph.current.links.forEach((link) => { graphologyGraph.addEdge(link.source, link.target); }); const boundingBox = { x1: 0, x2: app.renderer.screen.width, y1: 0, y2: app.renderer.screen.height }; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx index 990719dc6..952d6bc4e 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx @@ -142,7 +142,11 @@ type OptionsI = { * @returns {GraphType} A node-link graph containing the nodes and links for the diagram. */ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: OptionsI = {}): GraphType { - const nodes: NodeType[] = []; + let ret: GraphType = { + nodes: {}, + links: {}, + }; + const typeDict: { [key: string]: number } = {}; // Counter for the types let counter = 1; @@ -184,8 +188,8 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: type: typeNumber, displayInfo: preferredText, radius: radius, - x: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10, - y: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10, + defaultX: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10, + defaultY: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10, }; // let mlExtra = {}; @@ -209,13 +213,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: // Add mlExtra to the node if necessary // data = { ...data, ...mlExtra }; - nodes.push(data); + ret.nodes[data._id] = data; } // Filter unique edges and transform to LinkTypes // List for all links let links: LinkType[] = []; - let allNodeIds = new Set(nodes.map((n) => n._id)); + let allNodeIds = new Set(Object.keys(ret.nodes)); // Parse ml edges // if (ml != undefined) { @@ -239,14 +243,14 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: for (let i = 0; i < uniqueEdges.length; i++) { if (allNodeIds.has(uniqueEdges[i].from) && allNodeIds.has(uniqueEdges[i].to)) { const toAdd: LinkType = { - id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, + id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, // TODO: this only supports one link between two nodes source: uniqueEdges[i].from, target: uniqueEdges[i].to, value: uniqueEdges[i].count, mlEdge: false, color: 0x000000, }; - links.push(toAdd); + ret.links[toAdd.id] = toAdd; } } @@ -266,13 +270,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: } // Graph to be returned - let toBeReturned: GraphType = { - nodes: nodes, - links: links, - // linkPrediction: linkPredictionInResult, - // shortestPath: shortestPathInResult, - // communityDetection: communityDetectionInResult, - }; + // let toBeReturned: GraphType = { + // nodes: nodes, + // links: links, + // linkPrediction: linkPredictionInResult, + // shortestPath: shortestPathInResult, + // communityDetection: communityDetectionInResult, + // }; // If query with community detection; add number of clusters to the graph // const numberOfClusters = { @@ -283,5 +287,5 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: // } // return toBeReturned; - return processML(ml, toBeReturned); + return processML(ml, ret); } diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx index 36f17720a..173dbecc6 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx @@ -68,11 +68,12 @@ export function hslStringToHex(hsl: string) { */ export const getRelatedLinks = (graph: GraphType, nodes: NodeType[], jaccardThreshold: number): LinkType[] => { const relatedLinks: LinkType[] = []; - graph.links.forEach((link: LinkType) => { + Object.keys(graph.links).forEach((id) => { + const link = graph.links[id]; const { source, target } = link; if (isLinkVisible(link, jaccardThreshold)) { nodes.forEach((node: NodeType) => { - if (source == node || target == node || source == node._id || target == node._id) { + if (source == node._id || target == node._id || source == node._id || target == node._id) { relatedLinks.push(link); } }); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index 70c7867ba..4daa88491 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { GraphType, LinkType, NodeType } from './types'; +import { GraphType, LinkType, NodeType, NodeTypeD3 } from './types'; import { NLPixi } from './components/NLPixi'; import { parseQueryResult } from './components/query2NL'; import { useImmer } from 'use-immer'; @@ -66,7 +66,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } }, [data, ml]); - const onClickedNode = (event?: { node: NodeType; pos: IPointData }, ml?: ML) => { + const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => { if (graph) { if (!event?.node) { handleSelect(); @@ -74,11 +74,12 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } const node = event.node; - handleSelect({ nodes: [node as Node] }); + const nodeMeta = graph.nodes[node._id]; + handleSelect({ nodes: [nodeMeta as Node] }); if (ml && ml.shortestPath.enabled) { setGraph((draft) => { - let _node = draft?.nodes.find((n) => n._id === node._id); + let _node = draft?.nodes[node._id]; if (!_node) return draft; if (!ml.shortestPath.srcNode) { @@ -105,6 +106,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } }; + if (!graph) return null; return ( <NLPixi graph={graph} @@ -177,7 +179,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza type="dropdown" label="Shape" value={settings.shapes.shape} - options={[{circle: 'Circle'}, {rectangle: 'Square'}]} + options={[{ circle: 'Circle' }, { rectangle: 'Square' }]} onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })} /> ) : ( diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts index 7374b774b..321f20198 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts @@ -9,16 +9,21 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul /** Types for the nodes and links in the node-link diagram. */ export type GraphType = { - nodes: NodeType[]; - links: LinkType[]; + nodes: Record<string, NodeType>; // _id -> node + links: Record<string, LinkType>; // _id -> link // linkPrediction?: boolean; // shortestPath?: boolean; // communityDetection?: boolean; // numberOfMlClusters?: number; }; +export type GraphTypeD3 = { + nodes: NodeTypeD3[]; + links: LinkTypeD3[]; +}; + /** The interface for a node in the node-link diagram */ -export interface NodeType extends d3.SimulationNodeDatum, Node { +export interface NodeType extends Node { _id: string; // Number to determine the color of the node @@ -41,28 +46,14 @@ export interface NodeType extends d3.SimulationNodeDatum, Node { // 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; + defaultX?: number; + defaultY?: number; } +export type NodeTypeD3 = d3.SimulationNodeDatum & { _id: string }; + /** The interface for a link in the node-link diagram */ -export interface LinkType extends d3.SimulationLinkDatum<NodeType> { +export interface LinkType { // The thickness of a line id: string; value: number; @@ -70,14 +61,18 @@ export interface LinkType extends d3.SimulationLinkDatum<NodeType> { mlEdge: boolean; color: number; alpha?: number; + source: string; + target: string; } +export type LinkTypeD3 = d3.SimulationLinkDatum<NodeTypeD3> & { _id: string }; + /**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: Visualization[]; //The way to visualize attributes of this Node kind + visualizations: Visualization[]; //The way to visualize attributes of this Node kind }; export type CommunityDetectionNode = { -- GitLab