Skip to content

Commit 3bc61bf

Browse files
authored
Merge pull request #395 from FalkorDB/fix-overlapping-nodes
fix overlapping nodes
2 parents d925841 + 09ec36c commit 3bc61bf

File tree

12 files changed

+1561
-2355
lines changed

12 files changed

+1561
-2355
lines changed

app/components/code-graph.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import dynamic from 'next/dynamic';
1515
import { Position } from "./graphView";
1616
import { prepareArg } from '../utils';
1717
import { NodeObject, ForceGraphMethods } from "react-force-graph-2d";
18+
import { handleZoomToFit } from "@/lib/utils";
1819

1920
const GraphView = dynamic(() => import('./graphView'));
2021

@@ -68,6 +69,7 @@ export function CodeGraph({
6869
const [cooldownTicks, setCooldownTicks] = useState<number | undefined>(0)
6970
const [cooldownTime, setCooldownTime] = useState<number>(0)
7071
const containerRef = useRef<HTMLDivElement>(null);
72+
const [zoomedNodes, setZoomedNodes] = useState<Node[]>([]);
7173

7274
useEffect(() => {
7375
setData({ ...graph.Elements })
@@ -167,17 +169,17 @@ export function CodeGraph({
167169
}
168170

169171
const deleteNeighbors = (nodes: Node[]) => {
170-
172+
171173
if (nodes.length === 0) return;
172-
174+
173175
const expandedNodes: Node[] = []
174-
176+
175177
graph.Elements = {
176178
nodes: graph.Elements.nodes.filter(node => {
177179
if (!node.collapsed) return true
178-
180+
179181
const isTarget = graph.Elements.links.some(link => link.target.id === node.id && nodes.some(n => n.id === link.source.id));
180-
182+
181183
if (!isTarget) return true
182184

183185
const deleted = graph.NodesMap.delete(Number(node.id))
@@ -190,7 +192,7 @@ export function CodeGraph({
190192
}),
191193
links: graph.Elements.links
192194
}
193-
195+
194196
deleteNeighbors(expandedNodes)
195197

196198
graph.removeLinks()
@@ -239,12 +241,12 @@ export function CodeGraph({
239241
}
240242
graph.visibleLinks(true, [chartNode!.id])
241243
setData({ ...graph.Elements })
244+
setZoomedNodes([node])
245+
return
242246
}
243-
247+
248+
handleZoomToFit(chartRef, 4, (n: NodeObject<Node>) => n.id === node.id)
244249
setSearchNode(chartNode)
245-
setTimeout(() => {
246-
chart.zoomToFit(1000, 150, (n: NodeObject<Node>) => n.id === chartNode!.id);
247-
}, 0)
248250
}
249251
}
250252

@@ -307,7 +309,9 @@ export function CodeGraph({
307309
graph={graph}
308310
onValueChange={(node) => setSearchNode(node)}
309311
icon={<Search />}
310-
handleSubmit={handleSearchSubmit}
312+
handleSubmit={(node) => {
313+
handleSearchSubmit(node)
314+
}}
311315
node={searchNode}
312316
/>
313317
<Labels categories={graph.Categories} onClick={onCategoryClick} />
@@ -380,6 +384,7 @@ export function CodeGraph({
380384
/>
381385
<button
382386
className="pointer-events-auto bg-white p-2 rounded-md"
387+
title='downloadImage'
383388
onClick={handleDownloadImage}
384389
>
385390
<Download />
@@ -420,6 +425,8 @@ export function CodeGraph({
420425
setCooldownTicks={setCooldownTicks}
421426
cooldownTime={cooldownTime}
422427
setCooldownTime={setCooldownTime}
428+
setZoomedNodes={setZoomedNodes}
429+
zoomedNodes={zoomedNodes}
423430
/>
424431
</div>
425432
: <div className="flex flex-col items-center justify-center h-full text-gray-400">

app/components/graphView.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11

2-
import ForceGraph2D, { ForceGraphMethods } from 'react-force-graph-2d';
2+
import ForceGraph2D, { ForceGraphMethods, NodeObject } from 'react-force-graph-2d';
33
import { Graph, GraphData, Link, Node } from './model';
44
import { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react';
55
import { Path } from '../page';
6+
import { handleZoomToFit } from '@/lib/utils';
67

78
export interface Position {
89
x: number,
@@ -30,6 +31,8 @@ interface Props {
3031
setCooldownTicks: Dispatch<SetStateAction<number | undefined>>
3132
cooldownTime: number | undefined
3233
setCooldownTime: Dispatch<SetStateAction<number>>
34+
setZoomedNodes: Dispatch<SetStateAction<Node[]>>
35+
zoomedNodes: Node[]
3336
}
3437

3538
const PATH_COLOR = "#ffde21"
@@ -56,7 +59,9 @@ export default function GraphView({
5659
cooldownTicks,
5760
cooldownTime,
5861
setCooldownTicks,
59-
setCooldownTime
62+
setCooldownTime,
63+
zoomedNodes,
64+
setZoomedNodes
6065
}: Props) {
6166

6267
const parentRef = useRef<HTMLDivElement>(null)
@@ -86,14 +91,9 @@ export default function GraphView({
8691
}, [parentRef])
8792

8893
useEffect(() => {
89-
setCooldownTime(4000)
94+
setCooldownTime(2000)
9095
setCooldownTicks(undefined)
91-
}, [graph.Id])
92-
93-
useEffect(() => {
94-
setCooldownTime(1000)
95-
setCooldownTicks(undefined)
96-
}, [graph.getElements().length])
96+
}, [graph.Id, graph.getElements().length])
9797

9898
const unsetSelectedObjects = (evt?: MouseEvent) => {
9999
if (evt?.ctrlKey || (!selectedObj && selectedObjects.length === 0)) return
@@ -144,13 +144,35 @@ export default function GraphView({
144144
}
145145
}
146146

147+
const avoidOverlap = (nodes: Position[]) => {
148+
const spacing = NODE_SIZE * 2.5;
149+
nodes.forEach((nodeA, i) => {
150+
nodes.forEach((nodeB, j) => {
151+
if (i !== j) {
152+
const dx = nodeA.x - nodeB.x;
153+
const dy = nodeA.y - nodeB.y;
154+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
155+
156+
if (distance < spacing) {
157+
const pushStrength = (spacing - distance) / distance * 0.5;
158+
nodeA.x += dx * pushStrength;
159+
nodeA.y += dy * pushStrength;
160+
nodeB.x -= dx * pushStrength;
161+
nodeB.y -= dy * pushStrength;
162+
}
163+
}
164+
});
165+
});
166+
};
167+
147168
return (
148169
<div ref={parentRef} className="relative w-fill h-full">
149170
<ForceGraph2D
150171
ref={chartRef}
151172
height={parentHeight}
152173
width={parentWidth}
153174
graphData={data}
175+
onEngineTick={() => avoidOverlap(data.nodes as Position[])}
154176
nodeVisibility="visible"
155177
linkVisibility="visible"
156178
linkCurvature="curve"
@@ -278,6 +300,8 @@ export default function GraphView({
278300
onEngineStop={() => {
279301
setCooldownTicks(0)
280302
setCooldownTime(0)
303+
handleZoomToFit(chartRef, zoomedNodes.length === 1 ? 4 : 1, (n: NodeObject<Node>) => zoomedNodes.some(node => node.id === n.id))
304+
setZoomedNodes([])
281305
}}
282306
cooldownTicks={cooldownTicks}
283307
cooldownTime={cooldownTime}

e2e/config/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const GRAPH_ID = "GraphRAG-SDK";
22
export const PROJECT_NAME = "GraphRAG-SDK";
3+
export const PROJECT_CLICK = "flask";
34
export const CHAT_OPTTIONS_COUNT = 1;
45
export const Node_Question = "how many nodes do we have?";
56
export const Edge_Question = "how many edges do we have?";

e2e/config/testData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const searchData: { searchInput: string; completedSearchInput?: string; }[] = [
22
{ searchInput: "test"},
33
{ searchInput: "set"},
4-
{ searchInput: "low", completedSearchInput: "lower" },
4+
{ searchInput: "low", completedSearchInput: "lower_items" },
55
{ searchInput: "as", completedSearchInput: "ask"},
66
];
77

e2e/logic/POM/codeGraph.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Locator, Page } from "playwright";
1+
import { Download, Locator, Page } from "playwright";
22
import BasePage from "../../infra/ui/basePage";
33
import { waitForElementToBeVisible, waitForStableText, waitToBeEnabled } from "../utils";
44

@@ -228,9 +228,13 @@ export default class CodeGraph extends BasePage {
228228
private get copyToClipboardNodePanelDetails(): Locator {
229229
return this.page.locator(`//div[@data-name='node-details-panel']//button[@title='Copy src to clipboard']`);
230230
}
231-
232-
private get nodeToolTip(): Locator {
233-
return this.page.locator("//div[contains(@class, 'graph-tooltip')]");
231+
232+
private get nodeToolTip(): (node: string) => Locator {
233+
return (node: string) => this.page.locator(`//div[contains(@class, 'force-graph-container')]/div[contains(text(), '${node}')]`);
234+
}
235+
236+
private get downloadImageBtn(): Locator {
237+
return this.page.locator("//button[@title='downloadImage']");
234238
}
235239

236240
/* NavBar functionality */
@@ -423,6 +427,7 @@ export default class CodeGraph extends BasePage {
423427
const button = this.searchBarOptionBtn(buttonNum);
424428
await button.waitFor({ state : "visible"})
425429
await button.click();
430+
await this.page.waitForTimeout(4000);
426431
}
427432

428433
async getSearchBarInputValue(): Promise<string> {
@@ -463,20 +468,20 @@ export default class CodeGraph extends BasePage {
463468
}
464469

465470
async nodeClick(x: number, y: number): Promise<void> {
466-
for (let attempt = 1; attempt <= 2; attempt++) {
471+
for (let attempt = 1; attempt <= 3; attempt++) {
467472
await this.canvasElement.hover({ position: { x, y } });
468473
await this.page.waitForTimeout(500);
469-
470-
if (await waitForElementToBeVisible(this.nodeToolTip)) {
471-
await this.canvasElement.click({ position: { x, y }, button: 'right' });
474+
await this.canvasElement.click({ position: { x, y }, button: 'right' });
475+
if (await this.elementMenu.isVisible()) {
472476
return;
473477
}
474478
await this.page.waitForTimeout(1000);
475479
}
476-
477-
throw new Error("Tooltip not visible after multiple attempts!");
480+
481+
throw new Error(`Failed to click, elementMenu not visible after multiple attempts.`);
478482
}
479483

484+
480485
async selectCodeGraphCheckbox(checkbox: string): Promise<void> {
481486
await this.codeGraphCheckbox(checkbox).click();
482487
}
@@ -637,4 +642,60 @@ export default class CodeGraph extends BasePage {
637642
return { scaleX, scaleY };
638643
}
639644

645+
async downloadImage(): Promise<Download> {
646+
await this.page.waitForLoadState('networkidle');
647+
const [download] = await Promise.all([
648+
this.page.waitForEvent('download'),
649+
this.downloadImageBtn.click(),
650+
]);
651+
652+
return download;
653+
}
654+
655+
async rightClickAtCanvasCenter(): Promise<void> {
656+
const boundingBox = await this.canvasElement.boundingBox();
657+
if (!boundingBox) throw new Error('Canvas bounding box not found');
658+
const centerX = boundingBox.x + boundingBox.width / 2;
659+
const centerY = boundingBox.y + boundingBox.height / 2;
660+
await this.page.mouse.click(centerX, centerY, { button: 'right' });
661+
}
662+
663+
async hoverAtCanvasCenter(): Promise<void> {
664+
const boundingBox = await this.canvasElement.boundingBox();
665+
if (!boundingBox) throw new Error('Canvas bounding box not found');
666+
const centerX = boundingBox.x + boundingBox.width / 2;
667+
const centerY = boundingBox.y + boundingBox.height / 2;
668+
await this.page.mouse.move(centerX, centerY);
669+
}
670+
671+
async isNodeToolTipVisible(node: string): Promise<boolean> {
672+
return await this.nodeToolTip(node).isVisible();
673+
}
674+
675+
async waitForCanvasAnimationToEnd(timeout = 5000): Promise<void> {
676+
const canvasHandle = await this.canvasElement.elementHandle();
677+
678+
if (!canvasHandle) {
679+
throw new Error("Canvas element not found!");
680+
}
681+
682+
await this.page.waitForFunction(
683+
(canvas) => {
684+
const ctx = (canvas as HTMLCanvasElement).getContext('2d');
685+
if (!ctx) return false;
686+
687+
const imageData1 = ctx.getImageData(0, 0, (canvas as HTMLCanvasElement).width, (canvas as HTMLCanvasElement).height).data;
688+
689+
return new Promise<boolean>((resolve) => {
690+
setTimeout(() => {
691+
const imageData2 = ctx.getImageData(0, 0, (canvas as HTMLCanvasElement).width, (canvas as HTMLCanvasElement).height).data;
692+
resolve(JSON.stringify(imageData1) === JSON.stringify(imageData2));
693+
}, 500);
694+
});
695+
},
696+
canvasHandle as any,
697+
{ timeout }
698+
);
699+
}
700+
640701
}

e2e/logic/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const waitForStableText = async (locator: Locator, timeout: number = 5000
3434
return stableText;
3535
};
3636

37-
export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5):Promise<boolean> => {
37+
export const waitForElementToBeVisible = async (locator:Locator,time=500,retry=10):Promise<boolean> => {
3838

3939
while(retry > 0){
4040
if(await locator.isVisible()){
@@ -48,4 +48,9 @@ export const waitForElementToBeVisible = async (locator:Locator,time=400,retry=5
4848

4949
export function findNodeByName(nodes: { name: string }[], nodeName: string): any {
5050
return nodes.find((node) => node.name === nodeName);
51-
}
51+
}
52+
53+
export function findFirstNodeWithSrc(nodes: { src?: string }[]): any {
54+
return nodes.find((node) => node.src !== undefined);
55+
}
56+

0 commit comments

Comments
 (0)