Skip to content
Snippets Groups Projects
Commit 03093038 authored by Marcos Pieras's avatar Marcos Pieras
Browse files

feat: merged

parent 1d35d9d0
No related branches found
No related tags found
1 merge request!163feat: redesign tooltips for schema and nodelink
Pipeline #137646 passed
......@@ -11,12 +11,13 @@ import {
IPointData,
Sprite,
Assets,
Text,
Texture,
Resource,
} from 'pixi.js';
import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access';
import { NLPopup } from './NLPopup';
import { hslStringToHex, nodeColor, nodeColorHex } from './utils';
import { hslStringToHex, nodeColor } from './utils';
import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout';
import { MultiGraph } from 'graphology';
import { Viewport } from 'pixi-viewport';
......@@ -24,7 +25,6 @@ import { NodelinkVisProps } from '../nodelinkvis';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { MovedEvent } from 'pixi-viewport/dist/types';
import { ConstructionOutlined } from '@mui/icons-material';
import { CardToolTipVis, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis';
type Props = {
onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void;
......@@ -57,19 +57,27 @@ export const NLPixi = (props: Props) => {
antialias: true,
autoDensity: true,
eventMode: 'auto',
resolution: window.devicePixelRatio || 1,
resolution: window.devicePixelRatio || 2,
}),
[],
);
const nodeLayer = useMemo(() => new Container(), []);
const labelLayer = useMemo(() => {
const container = new Container();
container.alpha = 0;
container.renderable = false;
return container;
}, []);
const nodeMap = useRef(new Map<string, Sprite>());
const linkGfx = new Graphics();
const labelMap = useRef(new Map<string, Text>());
const viewport = useRef<Viewport>();
const layoutState = useRef<LayoutState>('reset');
const layoutStoppedCount = useRef(0);
const ref = useRef<HTMLDivElement>(null);
const mouseInCanvas = useRef<boolean>(false);
const [dragging, setDragging] = useState<boolean>(false);
const isSetup = useRef(false);
const ml = useML();
const searchResults = useSearchResultData();
......@@ -90,6 +98,8 @@ export const NLPixi = (props: Props) => {
width: 1000,
height: 1000,
LABEL_MAX_NODES: 1000,
LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
NODE_RADIUS: 5,
......@@ -131,6 +141,7 @@ export const NLPixi = (props: Props) => {
if (props.configuration.showPopUpOnHover) return;
(event as any).mouseDownTimeStamp = event.timeStamp;
setDragging(true);
},
onMouseUpNode(event: FederatedPointerEvent) {
......@@ -207,6 +218,29 @@ export const NLPixi = (props: Props) => {
}
setPopups([...popups]);
},
onZoom(event: FederatedPointerEvent) {
const scale = viewport.current!.transform.scale.x;
if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
labelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0;
if (labelLayer.alpha > 0) {
labelLayer.renderable = true;
const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0.
// Only change the fontSize for specific intervals, continuous change has too big of an impact on performance
const fontSize = scale < 0.1 ? 30 : scale < 0.2 ? 40 : scale < 0.3 ? 50 : 60;
const strokeWidth = fontSize / 2;
labelMap.current.forEach((text) => {
text.style.fontSize = fontSize;
text.style.strokeThickness = strokeWidth;
});
} else {
labelLayer.renderable = false;
}
}
},
}));
function resize() {
......@@ -255,6 +289,7 @@ export const NLPixi = (props: Props) => {
const nodeMeta = props.graph.nodes[node._id];
const texture = Assets.get(textureId(nodeMeta.selected));
gfx.texture = texture;
// Cluster colors
if (nodeMeta?.cluster) {
gfx.tint = nodeMeta.cluster >= 0 ? nodeColor(nodeMeta.cluster) : 0x000000;
......@@ -298,6 +333,7 @@ export const NLPixi = (props: Props) => {
const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2;
sprite.scale.set(scale, scale);
sprite.anchor.set(0.5, 0.5);
sprite.cullable = true;
sprite.eventMode = 'static';
sprite.on('mousedown', (e) => imperative.current.onMouseDown(e));
......@@ -314,13 +350,30 @@ export const NLPixi = (props: Props) => {
return sprite;
};
// /** UpdateRadius works just like UpdateColors, but also applies radius*/
// const UpdateRadius = (graph: GraphType, radius: number) => {
// // update for each node in graph
// graph.nodes.forEach((node: NodeType) => {
// createNode(node);
// });
// };
const createLinkLabel = (link: LinkTypeD3) => {
// check if link is already drawn, and if so, delete it
if (link && link?._id && labelMap.current.has(link._id)) {
labelMap.current.delete(link._id);
}
const linkMeta = props.graph.links[link._id];
const text = new Text(linkMeta.name, {
fontSize: 60,
fill: config.LINE_COLOR_DEFAULT,
stroke: 0xffffff,
strokeThickness: 30,
});
text.cullable = true;
text.anchor.set(0.5, 0.5);
text.scale.set(0.1, 0.1);
labelMap.current.set(link._id, text);
labelLayer.addChild(text);
updateLinkLabel(link);
return text;
};
const updateLink = (link: LinkTypeD3) => {
if (!props.graph || nodeMap.current.size === 0) return;
......@@ -354,53 +407,111 @@ export const NLPixi = (props: Props) => {
return;
}
if (linkGfx) {
// let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT;
let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML;
// let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT;
let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML;
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(linkMeta)) {
if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
color = dataColors.magenta[50];
// 0xaa00ff;
style = linkMeta.value * 1.8;
} else {
style = 0;
alpha = 0.2;
}
} 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 = linkMeta.value * 1.8;
}
} else {
color = dataColors.red[70];
// color = 0xff0000;
style = 1.0;
}
} else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) {
color = dataColors.green[50];
// color = 0x00ff00;
style = 3.0;
} else {
color = dataColors.red[70];
// color = 0xff0000;
style = 1.0;
}
} else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) {
color = dataColors.green[50];
// color = 0x00ff00;
style = 3.0;
}
// 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);
alpha = isLinkInSearchResults ? 1 : 0.05;
}
// 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);
alpha = isLinkInSearchResults ? 1 : 0.05;
}
linkGfx
.lineStyle(style, hslStringToHex(color), alpha)
.moveTo(source.x || 0, source.y || 0)
.lineTo(target.x || 0, target.y || 0);
};
linkGfx
.lineStyle(style, hslStringToHex(color), alpha)
.moveTo(source.x || 0, source.y || 0)
.lineTo(target.x || 0, target.y || 0);
const updateLinkLabel = (link: LinkTypeD3) => {
const text = labelMap.current.get(link._id);
if (!text) return;
const _source = link.source;
const _target = link.target;
if (!_source || !_target) {
return;
}
const source = nodeMap.current.get(link.source as string) as Sprite;
const target = nodeMap.current.get(link.target as string) as Sprite;
text.x = (source.x + target.x) / 2;
text.y = (source.y + target.y) / 2;
const length = Math.hypot(target.x - source.x, target.y - source.y);
// Skip rendering labels on very short edges
if (length < text.width + 10) {
// 10 to account for size of node
text.alpha = 0;
return;
} else {
throw Error('Link not found');
text.alpha = 1;
}
const rads = Math.atan2(target.y - source.y, target.x - source.x);
text.rotation = rads;
const degrees = Math.abs(text.angle % 360);
// Rotate edge labels to always be legible
if (degrees > 90 && degrees < 270) {
text.rotation = rads + Math.PI;
} else {
text.rotation = rads;
}
};
// const text = labelMap.current.get(link._id);
// if (!text) return;
// const source = link.source as NodeTypeD3;
// const target = link.target as NodeTypeD3;
// if (source.x == null || source.y == null || target.x == null || target.y == null) return;
// text.x = (source.x + target.x) / 2;
// text.y = (source.y + target.y) / 2;
// const rads = Math.atan2(target.y - source.y, target.x - source.x);
// const degrees = Math.abs(text.angle % 360);
// // Rotate edge labels to always be legible
// if (degrees > 90 && degrees < 270) {
// text.rotation = rads + Math.PI;
// } else {
// text.rotation = rads;
// }
async function loadAssets() {
if (!Assets.cache.has('texture')) {
Assets.addBundle('glyphs', {
......@@ -418,8 +529,10 @@ export const NLPixi = (props: Props) => {
loadAssets();
return () => {
nodeMap.current.clear();
labelMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
};
}, []);
......@@ -494,6 +607,7 @@ export const NLPixi = (props: Props) => {
linkGfx.beginFill();
graph.current.links.forEach((link: any) => {
updateLink(link);
updateLinkLabel(link);
});
linkGfx.endFill();
}
......@@ -507,6 +621,7 @@ export const NLPixi = (props: Props) => {
nodeMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
}
nodeMap.current.forEach((gfx, id) => {
......@@ -517,6 +632,14 @@ export const NLPixi = (props: Props) => {
}
});
labelMap.current.forEach((text, id) => {
if (!graph.current.links.find((link) => link._id === id)) {
labelLayer.removeChild(text);
text.destroy();
labelMap.current.delete(id);
}
});
linkGfx.clear();
graph.current.nodes.forEach((node) => {
......@@ -531,6 +654,16 @@ export const NLPixi = (props: Props) => {
}
});
if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
graph.current.links.forEach((link) => {
if (!forceClear && labelMap.current.has(link._id)) {
updateLinkLabel(link);
} else {
createLinkLabel(link);
}
});
}
// // update text colour (written after nodes so that text appears on top of nodes)
// nodes.forEach((node: NodeType) => {
// if (node.gfxAttributes !== undefined) {
......@@ -559,6 +692,7 @@ export const NLPixi = (props: Props) => {
*/
const setup = async () => {
nodeLayer.removeChildren();
labelLayer.removeChildren();
app.stage.removeChildren();
if (!props.graph) throw Error('Graph is undefined');
......@@ -587,10 +721,17 @@ export const NLPixi = (props: Props) => {
viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 });
viewport.current.addChild(linkGfx);
viewport.current.addChild(labelLayer);
viewport.current.addChild(nodeLayer);
viewport.current.on('moved', (event) => {
imperative.current.onMoved(event);
});
viewport.current.on('drag-end', (event) => {
setDragging(false);
});
viewport.current.on('zoomed', (event) => {
imperative.current.onZoom(event);
});
app.stage.eventMode = 'dynamic';
app.stage.on('mousedown', (e) => imperative.current.onMouseDown(e));
......@@ -629,17 +770,10 @@ export const NLPixi = (props: Props) => {
return (
<>
{popups.map((popup) => (
<Tooltip key={popup.node._id} open={true} boundaryElement={ref} showArrow={true}>
<Tooltip key={popup.node._id} open={true} interactive={!dragging} boundaryElement={ref} showArrow={true}>
<TooltipTrigger x={popup.pos.x} y={popup.pos.y} />
<TooltipContent>
<div>
<CardToolTipVis
type="popupvis"
name={props.graph.nodes[popup.node._id].label}
colorHeader={nodeColorHex(props.graph.nodes[popup.node._id].type)}
data={props.graph.nodes[popup.node._id].attributes}
/>
</div>
<NLPopup onClose={() => {}} data={{ node: props.graph.nodes[popup.node._id], pos: popup.pos }} key={popup.node._id} />
</TooltipContent>
</Tooltip>
))}
......@@ -647,14 +781,11 @@ export const NLPixi = (props: Props) => {
<Tooltip key={quickPopup.node._id} open={true} boundaryElement={ref} showArrow={true}>
<TooltipTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} />
<TooltipContent>
<div>
<CardToolTipVis
type="popupvis"
name={props.graph.nodes[quickPopup.node._id].label}
colorHeader={nodeColorHex(props.graph.nodes[quickPopup.node._id].type)}
data={props.graph.nodes[quickPopup.node._id].attributes}
/>
</div>
<NLPopup
onClose={() => {}}
data={{ node: props.graph.nodes[quickPopup.node._id], pos: quickPopup.pos }}
key={quickPopup.node._id}
/>
</TooltipContent>
</Tooltip>
)}
......
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