Skip to content

Commit

Permalink
feat: use rectangle collision in d3-force auto layout (#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
lihebi committed May 4, 2023
1 parent 588c8c7 commit 9c2683d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 31 deletions.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"ansi-to-react": "^6.1.6",
"crypto-js": "^4.1.1",
"d3-force": "^3.0.0",
"d3-quadtree": "^3.0.1",
"formik": "^2.2.9",
"graphql": "^16.6.0",
"jwt-decode": "^3.1.2",
Expand Down
110 changes: 81 additions & 29 deletions ui/src/lib/store/canvasSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
ReactFlowInstance,
} from "reactflow";
import { node } from "prop-types";
import { quadtree } from "d3-quadtree";

// TODO add node's data typing.
type NodeData = {
Expand Down Expand Up @@ -950,25 +951,15 @@ export const createCanvasSlice: StateCreator<MyState, [], [], CanvasSlice> = (
*/
autoForce: (scopeId) => {
// 1. get all the nodes and edges in the scope
// 2. get
type NodeType = {
id: string;
x: number;
y: number;
r: number;
width: number;
height: number;
};
const nodes = get().nodes.filter(
(node) => node.parentNode === (scopeId === "ROOT" ? undefined : scopeId)
);
if (nodes.length == 0) return;
const edges = get().edges;
const tmpNodes: NodeType[] = nodes.map((node) => ({
id: node.id,
x: node.position.x + (node.width! + node.height!) / 2,
y: node.position.y + (node.width! + node.height!) / 2,
r: (node.width! + node.height!) / 2,
x: node.position.x + node.width! / 2,
y: node.position.y + node.height! / 2,
width: node.width!,
height: node.height!,
}));
Expand Down Expand Up @@ -999,18 +990,15 @@ export const createCanvasSlice: StateCreator<MyState, [], [], CanvasSlice> = (
// .force("charge", forceManyBody().strength(-1000))
// .force("x", forceX())
// .force("y", forceY())
.force(
"collide",
forceCollide().radius((d: any) => d.r)
)
.force("collide", forceCollideRect())
// .force("link", d3.forceLink(edges).id(d => d.id))
// .force("charge", d3.forceManyBody())
// .force("center", forceCenter(0, 0))
.stop();
simulation.tick(3000);
simulation.tick(1);
tmpNodes.forEach((node) => {
node.x -= (node.width! + node.height!) / 2;
node.y -= (node.width! + node.height!) / 2;
node.x -= node.width! / 2;
node.y -= node.height! / 2;
});
// The nodes will all have new positions now. I'll need to make the graph to be top-left, i.e., the leftmost is 20, the topmost is 20.
// get the min x and y
Expand All @@ -1019,8 +1007,9 @@ export const createCanvasSlice: StateCreator<MyState, [], [], CanvasSlice> = (
let y1s = tmpNodes.map((node) => node.y);
let miny = Math.min(...y1s);
// calculate the offset, leave 50 padding for the scope.
const offsetx = 50 - minx;
const offsety = 50 - miny;
const padding = 50;
const offsetx = padding - minx;
const offsety = padding - miny;
// move the nodes
tmpNodes.forEach((node) => {
node.x += offsetx;
Expand Down Expand Up @@ -1052,12 +1041,12 @@ export const createCanvasSlice: StateCreator<MyState, [], [], CanvasSlice> = (
const scope = nodesMap.get(scopeId);
nodesMap.set(scopeId, {
...scope!,
width: maxx - minx + 100,
height: maxy - minx + 100,
width: maxx - minx + padding * 2,
height: maxy - minx + padding * 2,
style: {
...scope!.style,
width: maxx - minx + 100,
height: maxy - minx + 100,
width: maxx - minx + padding * 2,
height: maxy - minx + padding * 2,
},
});
}
Expand All @@ -1081,6 +1070,68 @@ export const createCanvasSlice: StateCreator<MyState, [], [], CanvasSlice> = (
},
});

type NodeType = {
id: string;
x: number;
y: number;
width: number;
height: number;
};

function forceCollideRect() {
let nodes;

function force(alpha) {
const padding = 5;
const quad = quadtree(
nodes,
(d: NodeType) => d.x,
(d) => d.y
);
for (const d of nodes) {
quad.visit((q: any, x1, y1, x2, y2) => {
let updated = false;
if (q.data && q.data !== d) {
let x = d.x - q.data.x,
y = d.y - q.data.y,
xSpacing = padding + (q.data.width + d.width) / 2,
ySpacing = padding + (q.data.height + d.height) / 2,
absX = Math.abs(x),
absY = Math.abs(y),
l,
lx,
ly;

if (absX < xSpacing && absY < ySpacing) {
l = Math.sqrt(x * x + y * y);

lx = (absX - xSpacing) / l;
ly = (absY - ySpacing) / l;

// the one that's barely within the bounds probably triggered the collision
if (Math.abs(lx) > Math.abs(ly)) {
lx = 0;
} else {
ly = 0;
}
d.x -= x *= lx;
d.y -= y *= ly;
q.data.x += x;
q.data.y += y;

updated = true;
}
}
return updated;
});
}
}

force.initialize = (_) => (nodes = _);

return force;
}

/**
* Compute the rectangle of that is tightly fit to the children.
* @param node
Expand Down Expand Up @@ -1115,12 +1166,13 @@ function fitChildren(
let maxy = Math.max(...y2s);
let width = maxx - minx;
let height = maxy - miny;
const padding = 50;
return {
// leave a 50 padding for the scope.
x: minx - 50,
y: miny - 50,
width: width + 100,
height: height + 100,
x: minx - padding,
y: miny - padding,
width: width + padding * 2,
height: height + padding * 2,
};
}

Expand Down
2 changes: 1 addition & 1 deletion ui/src/lib/store/settingSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const createSettingSlice: StateCreator<MyState, [], [], SettingSlice> = (
},
autoRunLayout: localStorage.getItem("autoRunLayout")
? JSON.parse(localStorage.getItem("autoRunLayout")!)
: false,
: true,
setAutoRunLayout: (b: boolean) => {
set({ autoRunLayout: b });
// also write to local storage
Expand Down
2 changes: 1 addition & 1 deletion ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5928,7 +5928,7 @@ d3-force@^3.0.0:
dependencies:
d3-color "1 - 3"

"d3-quadtree@1 - 3":
"d3-quadtree@1 - 3", d3-quadtree@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
Expand Down

0 comments on commit 9c2683d

Please sign in to comment.