Skip to content
Snippets Groups Projects
Commit 09b1970b authored by Dennis Collaris's avatar Dennis Collaris
Browse files

feat: use floating ui for node link tooltips

Additional features:
- Shift-click for multi select
- Click anywhere in the stage to deselect
- Panning and zooming does not deselect
parent c0de439a
No related branches found
No related tags found
No related merge requests found
This commit is part of merge request !158. Comments created here will be created in the context of that merge request.
import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types';
import { dataColors, visualizationColors } from 'config';
import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import {
Application,
AssetsBundle,
......@@ -22,6 +22,7 @@ import { MultiGraph } from 'graphology';
import { Viewport } from 'pixi-viewport';
import { NodelinkVisProps } from '../nodelinkvis';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { MovedEvent } from 'pixi-viewport/dist/types';
type Props = {
......@@ -70,8 +71,6 @@ export const NLPixi = (props: Props) => {
const mouseInCanvas = useRef<boolean>(false);
const isSetup = useRef(false);
const ml = useML();
const dragging = useRef<{ node: NodeTypeD3; gfx: Sprite } | null>(null);
const onlyClicked = useRef(false);
const searchResults = useSearchResultData();
const graph = useRef<GraphTypeD3>({ nodes: [], links: [] });
......@@ -124,71 +123,58 @@ export const NLPixi = (props: Props) => {
const imperative = useRef<any>(null);
const mouseClickThreshold = 200; // Time between mouse up and down events that is considered a click, and not a drag.
useImperativeHandle(imperative, () => ({
onDragStart(node: NodeTypeD3, gfx: Sprite) {
dragging.current = { node, gfx };
onlyClicked.current = true;
// todo: graphology does not support fixed nodes
// todo: after vis-settings panel is there, we should to also support the original d3 force to allow interactivity if needed
if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return;
if (viewport.current) viewport.current.pause = true;
onMouseDown(event: FederatedPointerEvent) {
(event as any).mouseDownTimeStamp = event.timeStamp;
},
onMouseUpNode(event: FederatedPointerEvent) {
// If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups.
const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp;
if(holdDownTime > mouseClickThreshold) {
return;
}
onDragMove(movementX: number, movementY: number) {
if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return;
const sprite = event.target as Sprite;
const node = (sprite as any).node as NodeType;
for (const idx in popups) {
const p = popups[idx];
p.pos.x += movementX / (viewport.current?.scaled || 1);
p.pos.y += movementY / (viewport.current?.scaled || 1);
popups[idx] = p;
setPopups([...popups]);
}
if (dragging.current) {
onlyClicked.current = false;
if (quickPopup) setQuickPopup(undefined);
const idx = popups.findIndex((p) => p.node._id === dragging.current?.node._id);
if (idx >= 0) {
const p = popups[idx];
p.pos.x += movementX / (viewport.current?.scaled || 1);
p.pos.y += movementY / (viewport.current?.scaled || 1);
popups[idx] = p;
setPopups([...popups]);
if (event.shiftKey) {
setPopups([...popups, { node: node, pos: toGlobal(node) }]);
} else {
setPopups([{ node: node, pos: toGlobal(node) }]);
for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite;
sprite.texture = Assets.get(textureId(false));
}
if (!dragging.current.node.fx) dragging.current.node.fx = dragging.current.node.x || 0;
if (!dragging.current.node.fy) dragging.current.node.fy = dragging.current.node.y || 0;
dragging.current.node.fx += movementX / (viewport.current?.scaled || 1);
dragging.current.node.fy += movementY / (viewport.current?.scaled || 1);
// force.simulation.alpha(0.1).restart();
}
sprite.texture = Assets.get(textureId(true));
props.onClick({ node: node, pos: toGlobal(node) });
event.stopPropagation();
},
onDragEnd() {
if (dragging.current) {
// dragging.current.node.fx = null;
// dragging.current.node.fy = null;
if (viewport.current) viewport.current.pause = false;
if (onlyClicked.current) {
onlyClicked.current = false;
if (popups.filter((d) => d.node._id === dragging.current?.node._id).length > 0) {
setPopups(popups.filter((p) => p.node._id !== dragging.current?.node._id));
props.onClick();
} else {
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) });
}
onMouseUpStage(event: FederatedPointerEvent) {
// If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups.
const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp;
if(holdDownTime < mouseClickThreshold) {
for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite;
sprite.texture = Assets.get(textureId(false));
}
this.onHover(dragging.current.node);
dragging.current = null;
} else {
setPopups([]);
props.onClick();
}
},
onHover(node: NodeTypeD3) {
onHover(event: FederatedPointerEvent) {
const sprite = event.target as Sprite;
const node = (sprite as any).node as NodeType;
if (
mouseInCanvas.current &&
viewport?.current &&
......@@ -202,10 +188,14 @@ export const NLPixi = (props: Props) => {
onUnHover() {
setQuickPopup(undefined);
},
onPan() {
setPopups([]);
props.onClick();
},
onMoved(event: MovedEvent) {
for (const popup of popups) {
if (popup.node.x == null || popup.node.y == null) continue;
popup.pos.x = event.viewport.transform.position.x + (popup.node.x * event.viewport.scale.x);
popup.pos.y = event.viewport.transform.position.y + (popup.node.y * event.viewport.scale.y);
}
setPopups([...popups]);
}
}));
function resize() {
......@@ -246,22 +236,7 @@ export const NLPixi = (props: Props) => {
} else return { x: 0, y: 0 };
}
function onDragStart(event: FederatedPointerEvent, node: NodeTypeD3, gfx: Sprite) {
event.stopPropagation();
if (imperative.current) imperative.current.onDragStart(node, gfx);
}
function onDragMove(event: FederatedPointerEvent) {
event.stopPropagation();
if (imperative.current) imperative.current.onDragMove(event.movementX, event.movementY);
}
function onDragEnd(event: FederatedPointerEvent) {
event.stopPropagation();
if (imperative.current) imperative.current.onDragEnd();
}
const updateNode = (node: NodeTypeD3) => {
const updateNode = (node: NodeType) => {
const gfx = nodeMap.current.get(node._id);
if (!gfx) return;
......@@ -279,20 +254,6 @@ export const NLPixi = (props: Props) => {
gfx.position.set(node.x, node.y);
gfx.off('mouseover');
gfx.off('mousedown');
gfx.on('mouseover', (e) => {
e.stopPropagation();
e.preventDefault();
if (imperative.current) imperative.current.onHover(node);
});
gfx.on('mouseout', (e) => {
e.stopPropagation();
e.preventDefault();
if (imperative.current) imperative.current.onUnHover();
});
gfx.on('mousedown', (e) => onDragStart(e, node, gfx));
// if (!item.position) {
// item.position = new Point(node.x, node.y);
// } else {
......@@ -319,23 +280,28 @@ export const NLPixi = (props: Props) => {
// Do not draw node if it has no position
if (node.x === undefined || node.y === undefined) return;
let gfx: Sprite;
let sprite: Sprite;
const texture = Assets.get(textureId());
gfx = new Sprite(texture);
sprite = new Sprite(texture);
gfx.tint = nodeColor(nodeMeta.type);
sprite.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);
sprite.scale.set(scale, scale);
sprite.anchor.set(0.5, 0.5);
sprite.eventMode = 'static';
sprite.on('mousedown', (e) => imperative.current.onMouseDown(e));
sprite.on('mouseup', (e) => imperative.current.onMouseUpNode(e));
sprite.on('mouseover', (e) => imperative.current.onHover(e));
sprite.on('mouseout', (e) => imperative.current.onUnHover(e));
nodeMap.current.set(node._id, sprite);
nodeLayer.addChild(sprite);
updateNode(node);
gfx.name = 'node_' + node._id;
gfx.eventMode = 'dynamic';
(sprite as any).node = node;
return gfx;
return sprite;
};
// /** UpdateRadius works just like UpdateColors, but also applies radius*/
......@@ -524,7 +490,6 @@ export const NLPixi = (props: Props) => {
};
const update = (forceClear = false) => {
setPopups([]);
if (!props.graph || !ref.current) return;
if (props.graph) {
......@@ -613,21 +578,13 @@ export const NLPixi = (props: Props) => {
viewport.current.addChild(linkGfx);
viewport.current.addChild(nodeLayer);
viewport.current.on('drag-start', (event) => {
imperative.current.onPan();
});
viewport.current.on('moved', () => {
for (const popup in popups) {
popup.pos.x += 10;
popup.pos.y += 10;
}
setPopups([...popups]);
viewport.current.on('moved', (event) => {
imperative.current.onMoved(event)
})
app.stage.eventMode = 'dynamic';
app.stage.on('pointerup', onDragEnd);
app.stage.on('mousemove', onDragMove);
app.stage.on('mouseup', onDragEnd);
app.stage.on('mousedown', (e) => imperative.current.onMouseDown(e));
app.stage.on('mouseup', (e) => imperative.current.onMouseUpStage(e));
nodeMap.current.clear();
linkGfx.clear();
......@@ -660,12 +617,18 @@ export const NLPixi = (props: Props) => {
};
return (
<>
<Tooltip open={true}>
<Tooltip open={popups.length > 0}>
<TooltipTrigger x={popups[0]?.pos.x} y={popups[0]?.pos.y} />
<TooltipContent>
{popups[0] != null && <NLPopup onClose={() => {}} data={popups[0]} key={popups[0].node._id} />}
</TooltipContent>
</Tooltip>
<Tooltip open={popups.length > 1}>
<TooltipTrigger x={popups[1]?.pos.x} y={popups[1]?.pos.y} />
<TooltipContent>
{popups[1] != null && <NLPopup onClose={() => {}} data={popups[1]} key={popups[1].node._id} />}
</TooltipContent>
</Tooltip>
<div
className="h-full w-full overflow-hidden"
ref={ref}
......
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