Skip to content
Snippets Groups Projects
Commit 07c7af58 authored by Leonardo Christino's avatar Leonardo Christino
Browse files

Merge branch 'DEV-134' into 'main'

feat(nodelink): popup on mouse hover with node attributes

See merge request !62
parents f3bb6796 4df552e3
No related branches found
No related tags found
1 merge request!62feat(nodelink): popup on mouse hover with node attributes
Pipeline #127223 passed
......@@ -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);
......
/**
* 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>
);
};
......@@ -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> */}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment