From 4df552e3ae8f799795035fe4f258ff8f7eff2cc9 Mon Sep 17 00:00:00 2001 From: Milho001 <l.milhomemfrancochristino@uu.nl> Date: Tue, 19 Sep 2023 09:59:02 +0000 Subject: [PATCH] feat(nodelink): popup on mouse hover with node attributes --- .../lib/vis/nodelink/components/NLPixi.tsx | 42 ++-- .../lib/vis/nodelink/components/NLPopup.tsx | 207 ++++-------------- libs/shared/lib/vis/nodelink/nodelinkvis.tsx | 7 +- 3 files changed, 69 insertions(+), 187 deletions(-) diff --git a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx index a2f058832..79202fad0 100644 --- a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx +++ b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx @@ -13,6 +13,7 @@ import { parseQueryResult } from './query2NL'; type Props = { onClick: (node: NodeType) => void; + onHover: (data: { node: NodeType; pos: IPointData } | undefined) => void; highlightNodes: NodeType[]; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; @@ -77,11 +78,26 @@ export const NLPixi = (props: Props) => { // the reason for this is because of multitouch // we want to track the movement of this particular touch event.stopPropagation(); + console.log('dragstart', node); + onHover(event, undefined); + if (viewport.current) viewport.current.pause = true; dragging.current = { node, gfx }; onlyClicked.current = true; } + function onHover(event: FederatedPointerEvent, node: NodeType | undefined) { + event.stopPropagation(); + if (viewport?.current && !viewport?.current?.pause && node) { + const rect = ref.current?.getBoundingClientRect(); + const x = (rect?.x || 0) + (node.x || 0); + const y = (rect?.y || 0) + (node.y || 0); + props.onHover({ node, pos: viewport.current.toScreen(x, y) }); + } else { + props.onHover(undefined); + } + } + function onDragEnd(event: FederatedPointerEvent) { if (dragging.current) { event.stopPropagation(); @@ -92,6 +108,7 @@ export const NLPixi = (props: Props) => { onlyClicked.current = false; props.onClick(dragging.current.node); } + onHover(event, dragging.current.node); dragging.current = null; } } @@ -132,12 +149,11 @@ export const NLPixi = (props: Props) => { gfx.position.set(node.x, node.y); + gfx.off('mouseover'); gfx.off('mousedown'); - gfx.off('mousemove'); - gfx.off('mouseup'); + gfx.on('mouseover', (e) => onHover(e, node)); + gfx.on('mouseout', (e) => onHover(e, undefined)); gfx.on('mousedown', (e) => onDragStart(e, node, gfx)); - gfx.on('mousemove', onDragMove); - gfx.on('mouseup', onDragEnd); // if (!item.position) { // item.position = new Point(node.x, node.y); @@ -168,7 +184,6 @@ export const NLPixi = (props: Props) => { node.selected = selected; updateNode(node); - gfx.hitArea = new Circle(0, 0, 4); gfx.name = 'node_' + node.id; gfx.eventMode = 'dynamic'; @@ -280,21 +295,6 @@ export const NLPixi = (props: Props) => { const gfx = nodeMap.current.get(node.id); if (!gfx || node.x === undefined || node.y === undefined) return; gfx.position.copyFrom(node as IPointData); - // const pos = gfx.position; - // let normal = { x: node.x - pos.x, y: node.y - pos.y }; - // const size = Math.sqrt(normal.x * normal.x + normal.y * normal.y); - // normal = { x: normal.x / size, y: normal.y / size }; - // // const mid: IPointData = { x: (pos.x + (node.x || 0)) / 2, y: (pos.y + (node.y || 0)) / 2 }; - // const vel = { x: pos.x + normal.x * delta * 0.1, y: pos.y + normal.y * delta * 0.1 }; - // gfx.position.copyFrom(vel as IPointData); - // console.log(normal); - - // gfx.position.set(node.x, node.y); - // if (node.x - pos.x + (node.y - pos.y) > 10000) gfx.position.set(node.x, node.y); - // else { - // const normal = { x: (node.x - pos.x) / (node.x + pos.x), y: (node.y - pos.y) / (node.y + pos.y) }; - // gfx.position.set(pos.x + (normal.x / delta) * 50, pos.y + (normal.y / delta) * 50); - // } }); // Update forces of the links @@ -399,6 +399,8 @@ export const NLPixi = (props: Props) => { app.stage.eventMode = 'dynamic'; app.stage.on('mouseup', onDragEnd); app.stage.on('pointerup', onDragEnd); + app.stage.on('mousemove', onDragMove); + app.stage.on('mouseup', onDragEnd); app.ticker.add(tick); diff --git a/libs/shared/lib/vis/nodelink/components/NLPopup.tsx b/libs/shared/lib/vis/nodelink/components/NLPopup.tsx index d6c016daf..850099430 100644 --- a/libs/shared/lib/vis/nodelink/components/NLPopup.tsx +++ b/libs/shared/lib/vis/nodelink/components/NLPopup.tsx @@ -1,168 +1,43 @@ -/** - * Creates a list of attributes from a given node, that can be visualized when the node is clicked - * @param node The node from the graph which attributes need to be visualized - */ - +import { IPointData } from 'pixi.js'; import { NodeType } from '../Types'; -export const useNLPopup = (node: NodeType) => {}; -// private createAttributes = (node: NodeType) => { -// node.gfxAttributes = new PIXI.Graphics(); -// node.gfxAttributes.beginFill(0xeeeeee); -// node.gfxAttributes.name = 'attributes_' + node.id; -// node.gfxAttributes.alpha = 0; -// node.gfxAttributes.eventMode = 'auto'; -// // add title -// const gfxName = new PIXI.Text(); -// if (node.displayInfo != undefined) { -// gfxName.text = node.displayInfo; -// } else { -// gfxName.text = node.id; -// } -// gfxName.style.fontSize = 16; -// gfxName.style.fontFamily = 'Poppins, arial, sans-serif'; -// gfxName.style.fontWeight = 'bold'; -// gfxName.x = 10; -// gfxName.y = 10; -// gfxName.name = 'header'; -// node.gfxAttributes.addChild(gfxName); - -// // add attributes container -// const container = new PIXI.Container(); -// node.gfxAttributes.addChild(container); -// // add container for keys -// const keys = new PIXI.Container(); -// container.addChild(keys); -// // add container for values -// const values = new PIXI.Container(); -// container.addChild(values); - -// // add attributes -// if (node.attributes) { -// let index = 0; -// for (const key in node.attributes) { -// const attributes = this.visibleAttributes[node?.label || node.id]; -// const attributeHidden = attributes ? attributes[key] === false : false; -// if (attributeHidden) continue; // if attribute should be hidden, then skip -// this.addAttribute(keys, values, key, String(node.attributes[key]), index); -// index++; -// } -// } - -// // position attribute containers -// container.x = 10; -// container.y = 45; -// values.x = keys.width + 10; - -// // calculate width -// const width = this.calcAttrWidth(node.gfxAttributes) + 40; -// const height = container.height + 55; -// node.gfxAttributes.drawRoundedRect(0, 0, width, height, 10); - -// // add stripe -// const gfxLine = new PIXI.Graphics(); -// gfxLine.beginFill(0xbbbbbb); -// gfxLine.drawRect(10, 35, width - 20, 1); -// node.gfxAttributes.addChild(gfxLine); - -// this.stage.addChild(node.gfxAttributes); -// }; - -// /** -// * Helper function of createAttributes: adds an attribute to the new attributes graphic -// */ -// private addAttribute(keys: PIXI.Container, values: PIXI.Container, key: string, value: string, index: number): void { -// // add text for key -// const keyText = new PIXI.Text(); -// keyText.style.fontSize = 14; -// keyText.style.fontFamily = 'Poppins, arial, sans-serif'; -// keyText.style.fontWeight = 'bold'; -// keyText.name = 'key'; -// keyText.text = key + ':'; -// keyText.y = index * 20; -// keys.addChild(keyText); -// // add text for value -// const valueText = new PIXI.Text(); -// valueText.style.fontSize = 14; -// valueText.style.fontFamily = 'Poppins, arial, sans-serif'; -// valueText.name = 'value'; -// valueText.text = value; -// valueText.y = index * 20; -// values.addChild(valueText); -// } - -// /** -// * Helper function of createAttributes: calculates the width of the attributes graphic -// */ -// private calcAttrWidth(gfxAttributes: PIXI.Graphics): number { -// let width = 0; -// gfxAttributes.children.forEach((child) => { -// width = this.calcObjectWidth(child as PIXI.Text | PIXI.Graphics, width); -// }); - -// return width; -// } - -// /** -// * Helper function of calcAttrWidth: calculates the width of a child element of the attributes graphic -// */ -// private calcObjectWidth(object: PIXI.Text | PIXI.Graphics, minWidth?: number): number { -// const newWidth = object.width; -// return minWidth ? Math.max(newWidth, minWidth) : newWidth; -// } - -// /** -// * Calls for the Display that pops up and highlights the node and the links -// * if ShortestPath turns on, it turns off the highlightedlinks! -// * @param node The node clicked in the event -// */ -// public ToggleInformationOnNode(node: NodeType) { -// this.simulation.alphaTarget(0).restart(); // renderer will not always update without this line -// this.showAttributes(node); -// // this.highlightNode(node); -// // this.highlightLinks(node); -// // this.showShortestPath(); -// } - -// /** -// * When clicking on a node, toggle the select, making the text visible/invisible. -// * @param node The node which text should be toggled. -// */ -// public showAttributes = (node: NodeType) => { -// if (node.selected) { -// if (node.gfxAttributes) { -// node.gfxAttributes.alpha = 0; -// node.selected = false; -// } -// } else { -// if (node.gfxAttributes) node.gfxAttributes.destroy(); -// this.createAttributes(node); -// if (node.gfxAttributes) { -// node.gfxAttributes.scale.x = 1 / this.scalexy + 0.001 * this.scalexy; -// node.gfxAttributes.scale.y = 1 / this.scalexy + 0.001 * this.scalexy; -// node.gfxAttributes.alpha = 1; -// node.selected = true; -// } -// } -// }; - -// /** -// * Refreshes the attributes. This function is called when the attribute filter has been changed -// * @param graph the graph of which its nodes should be checked for attributes -// */ -// const UpdateAttributes = (graph: GraphType) => { -// this.simulation.alphaTarget(0).restart(); // required so that the visualization updates -// graph.nodes.forEach((node: NodeType) => { -// if (node.gfxAttributes !== undefined) { -// // destroy and add the attributes pop-up again -// node.gfxAttributes.destroy(); -// this.createAttributes(node); -// node.gfxAttributes.scale.x = 1 / this.scalexy + 0.001 * this.scalexy; -// node.gfxAttributes.scale.y = 1 / this.scalexy + 0.001 * this.scalexy; -// // make the pop-up visible if it was visible before -// if (node.selected) { -// node.gfxAttributes.alpha = 1; -// } -// } -// }); -// }; +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 bg-white rounded-none text-[0.9rem] min-w-[10rem]" + style={{ top: props.data.pos.y + 10, left: props.data.pos.x + 10 }} + > + <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 flex-wrap max-w-[10rem] text-right">{v}</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/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 065f5d26d..74baf7208 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -9,6 +9,7 @@ import { parseQueryResult } from './components/query2NL'; import { processML } from './components/NLMachineLearning'; import { useImmer } from 'use-immer'; import { ML, setShortestPathSource, setShortestPathTarget } from '../../data-access/store/mlSlice'; +import { NLPopup } from './components/NLPopup'; interface Props { loading?: boolean; @@ -45,6 +46,7 @@ export const NodeLinkVis = React.memo((props: Props) => { const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]); + const [popup, setPopup] = useState<undefined | { node: NodeType; pos: PIXI.IPointData }>(undefined); const graphQueryResult = useGraphQueryResult(); const ml = useML(); @@ -101,15 +103,18 @@ export const NodeLinkVis = React.memo((props: Props) => { return ( <> <div className="h-full w-full overflow-hidden" ref={ref}> + {!!popup && <NLPopup onClose={() => setPopup(undefined)} data={popup} />} <NLPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} onClick={(node) => { console.log(ml.shortestPath); - onClickedNode(node, ml); }} + onHover={(data) => { + setPopup(data); + }} /> {/* <VisConfigPanelComponent> */} -- GitLab