S
SolidJS8mo ago
sachaw

Force effect computation, or alternate approach?

I'm trying to write my own force directed graph, I am able to display nodes and links just fine with a random initial coordinate. I also have a simple function that calculates the next positions for the nodes. However I need to call that function at a rather high rate. When I put it inside of an interval, nothing gets updated. I'm pobbably missing something here. Any assistance would be much appreciated.
2 Replies
sachaw
sachawOP8mo ago
import { useLocalState } from "@hooks/useLocalState.js";
import { ReactiveMap } from "@solid-primitives/map";
import {
type Component,
For,
createComputed,
createEffect,
createMemo,
} from "solid-js";

interface Position {
x: number;
y: number;
}

export const DebugWindow: Component = () => {
const { localState } = useLocalState();
const width = 928;
const height = 600;

const positions = new ReactiveMap<string, Position>();

createComputed(() => {});

//initialize position
createEffect(() => {
for (const node of localState.serverStatus.mesh.nodes) {
if (!positions.has(node.id)) {
positions.set(node.id, {
x: (Math.random() / 10) * width,
y: (Math.random() / 10) * height,
});
}
}
});

createEffect(() => {
const intervalId = setInterval(() => {
const alpha = 0.1;
const distance = 30;
const strength = 0.1;

// Update positions logic
for (const node of localState.serverStatus.mesh.nodes) {
const position = positions.get(node.id);
if (!position) {
return;
}
let forceX = 1;
let forceY = 1;

for (const link of localState.serverStatus.mesh.edges) {
if (link.source === node.id) {
const target = positions.get(link.target);
if (target) {
const dx = target.x - position.x;
const dy = target.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const force = (strength * (distance - distance)) / distance;
forceX += force * dx;
forceY += force * dy;
}
}
}

position.x += forceX * alpha;
position.y += forceY * alpha;

positions.set(node.id, position);
}
}, 1000); // Update every 1 second

return () => clearInterval(intervalId);
});
import { useLocalState } from "@hooks/useLocalState.js";
import { ReactiveMap } from "@solid-primitives/map";
import {
type Component,
For,
createComputed,
createEffect,
createMemo,
} from "solid-js";

interface Position {
x: number;
y: number;
}

export const DebugWindow: Component = () => {
const { localState } = useLocalState();
const width = 928;
const height = 600;

const positions = new ReactiveMap<string, Position>();

createComputed(() => {});

//initialize position
createEffect(() => {
for (const node of localState.serverStatus.mesh.nodes) {
if (!positions.has(node.id)) {
positions.set(node.id, {
x: (Math.random() / 10) * width,
y: (Math.random() / 10) * height,
});
}
}
});

createEffect(() => {
const intervalId = setInterval(() => {
const alpha = 0.1;
const distance = 30;
const strength = 0.1;

// Update positions logic
for (const node of localState.serverStatus.mesh.nodes) {
const position = positions.get(node.id);
if (!position) {
return;
}
let forceX = 1;
let forceY = 1;

for (const link of localState.serverStatus.mesh.edges) {
if (link.source === node.id) {
const target = positions.get(link.target);
if (target) {
const dx = target.x - position.x;
const dy = target.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const force = (strength * (distance - distance)) / distance;
forceX += force * dx;
forceY += force * dy;
}
}
}

position.x += forceX * alpha;
position.y += forceY * alpha;

positions.set(node.id, position);
}
}, 1000); // Update every 1 second

return () => clearInterval(intervalId);
});
const MergedNodes = createMemo(() => {
//merge nodes and their positions
const mergedNodes = localState.serverStatus.mesh.nodes.map((node) => ({
...node,
...positions.get(node.id),
}));

return mergedNodes;
});

const MergedLinks = createMemo(() => {
//merge links and their positions
const mergedLinks = localState.serverStatus.mesh.edges.map((link) => {
const source = positions.get(link.source);
const target = positions.get(link.target);

if (source && target) {
return {
source,
target,
};
}
});

return mergedLinks;
});

return (
// biome-ignore lint/a11y/noSvgWithoutTitle: <explanation>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{
"max-width": "100%",
height: "auto",
}}
>
<g stroke="#999" stroke-opacity={0.6}>
<For each={MergedLinks()}>
{(link) => (
<line
stroke-width={1}
x1={link?.source.x}
y1={link?.source.y}
x2={link?.target.x}
y2={link?.target.y}
/>
)}
</For>
</g>
<g stroke={"#fff"} stroke-width={1.5}>
<For each={MergedNodes()}>
{(node) => (
<circle r={5} fill={"green"} cx={node.x} cy={node.y}>
<title>{node.id}</title>
</circle>
)}
</For>
</g>
</svg>
);
};
const MergedNodes = createMemo(() => {
//merge nodes and their positions
const mergedNodes = localState.serverStatus.mesh.nodes.map((node) => ({
...node,
...positions.get(node.id),
}));

return mergedNodes;
});

const MergedLinks = createMemo(() => {
//merge links and their positions
const mergedLinks = localState.serverStatus.mesh.edges.map((link) => {
const source = positions.get(link.source);
const target = positions.get(link.target);

if (source && target) {
return {
source,
target,
};
}
});

return mergedLinks;
});

return (
// biome-ignore lint/a11y/noSvgWithoutTitle: <explanation>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{
"max-width": "100%",
height: "auto",
}}
>
<g stroke="#999" stroke-opacity={0.6}>
<For each={MergedLinks()}>
{(link) => (
<line
stroke-width={1}
x1={link?.source.x}
y1={link?.source.y}
x2={link?.target.x}
y2={link?.target.y}
/>
)}
</For>
</g>
<g stroke={"#fff"} stroke-width={1.5}>
<For each={MergedNodes()}>
{(node) => (
<circle r={5} fill={"green"} cx={node.x} cy={node.y}>
<title>{node.id}</title>
</circle>
)}
</For>
</g>
</svg>
);
};
Thanks, is there a different way I should handle this, I need to perform the calculation many times legend, I'll take a look. Does it support edge labels? I tried D3 before this, which worked fine, but I need to be able to react to nodes or edges being pushed to my store and inseted reactively. The behaviour was either non-reactive or would reset the layout (positions) every time i'd be very interested if you know of any workarounds unlike many of the d3 primitives, the graph computation mutates it's internal state and isn't easy to hook into Nice, I'll have a look I wanted to use Cosmograph, but had the same issue
sachaw
sachawOP8mo ago
Not working correctly yet, but is reacting to changes (although positions are reseting and simulation does not restart) https://gist.github.com/sachaw/f9c73aa1806ab26cd52b7c3237b02200
Gist
graph.tsx
GitHub Gist: instantly share code, notes, and snippets.
Want results from more Discord servers?
Add your server