Skip to content

Commit 1ce9f61

Browse files
authored
Make ForceSimulation generic over its nodes and links, i.e. ForceSimulation<Node, Link> (#527)
* Make `ForceSimulation` generic over its nodes and links, i.e. `ForceSimulation<Node, Link>` * Expose `links` via `children` snippet of `ForceSimulation` * Fix breakages * Add changeset
1 parent 22fe631 commit 1ce9f61

5 files changed

Lines changed: 107 additions & 27 deletions

File tree

.changeset/crazy-friends-talk.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'layerchart': minor
3+
---
4+
5+
- Made `ForceSimulation` generic over its nodes and links, i.e. `ForceSimulation<Node, Link>.`
6+
- Exposed `links` via `children` snippet of `ForceSimulation`.

packages/layerchart/src/lib/components/ForceSimulation.svelte

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<script lang="ts" module>
2-
import { forceSimulation, type Force, type Simulation, type SimulationNodeDatum } from 'd3-force';
2+
import {
3+
forceSimulation,
4+
type Force,
5+
type Simulation,
6+
type SimulationLinkDatum,
7+
type SimulationNodeDatum,
8+
} from 'd3-force';
39
import type { Snippet } from 'svelte';
410
511
type Forces = Record<string, Force<any, any>>;
@@ -9,14 +15,24 @@
915
links?: TLink[];
1016
};
1117
12-
export type LinkPosition = {
18+
export type LinkPosition = Prettify<{
1319
x1: number;
1420
y1: number;
1521
x2: number;
1622
y2: number;
17-
};
23+
}>;
24+
25+
type NodeDatumFor<NodeDatum> = Prettify<NodeDatum & SimulationNodeDatum>;
26+
type LinkDatumFor<NodeDatum, LinkDatum> = Prettify<
27+
LinkDatum & SimulationLinkDatum<NodeDatumFor<NodeDatum>>
28+
>;
29+
30+
type SimulationFor<NodeDatum, LinkDatum> = Simulation<
31+
NodeDatumFor<NodeDatum>,
32+
LinkDatumFor<NodeDatum, LinkDatum>
33+
>;
1834
19-
export type ForceSimulationProps = {
35+
export type ForceSimulationProps<NodeDatum, LinkDatum> = {
2036
/**
2137
* Force simulation parameters
2238
*/
@@ -26,7 +42,7 @@
2642
* An object with arrays of nodes and links,
2743
* to be used for position calculation.
2844
*/
29-
data: Data;
45+
data: Data<NodeDatum, LinkDatum>;
3046
3147
/**
3248
* Current alpha value of the simulation
@@ -94,17 +110,19 @@
94110
children?: Snippet<
95111
[
96112
{
97-
nodes: any[];
98-
simulation: Simulation<SimulationNodeDatum, undefined>;
113+
nodes: NodeDatumFor<NodeDatum>[];
114+
links: LinkDatumFor<NodeDatum, LinkDatum>[];
99115
linkPositions: LinkPosition[];
116+
simulation: SimulationFor<NodeDatum, LinkDatum>;
100117
},
101118
]
102119
>;
103120
};
104121
</script>
105122

106-
<script lang="ts">
123+
<script lang="ts" generics="NodeDatum, LinkDatum = undefined">
107124
import { watch } from 'runed';
125+
import type { Prettify } from '@layerstack/utils';
108126
109127
let {
110128
forces,
@@ -121,16 +139,23 @@
121139
onEnd: onEndProp = () => {},
122140
children,
123141
cloneNodes = false,
124-
}: ForceSimulationProps = $props();
142+
}: ForceSimulationProps<NodeDatum, LinkDatum> = $props();
125143
126144
// MARK: Public Props
127145
128146
// MARK: Private Props
129147
130-
let simulatedNodes: SimulationNodeDatum[] = $state([]);
131148
let linkPositions: LinkPosition[] = $state([]);
149+
let simulatedNodes: NodeDatumFor<NodeDatum>[] = $state([]);
150+
let simulatedLinks: LinkDatumFor<NodeDatum, LinkDatum>[] = $derived(
151+
(data.links ?? []) as LinkDatumFor<NodeDatum, LinkDatum>[]
152+
);
132153
133-
const simulation = forceSimulation().stop();
154+
// This casting is unfortunately necessary, due to unfortunate
155+
// overloading choices made, over at `@typed/d3-force`:
156+
const simulation: SimulationFor<NodeDatum, LinkDatum> = (
157+
forceSimulation() as SimulationFor<NodeDatum, LinkDatum>
158+
).stop();
134159
135160
// d3.Simulation does not provide a `.forces()` getter, so we need to
136161
// keep track of previous forces ourselves, for diffing against `forces`.
@@ -259,12 +284,10 @@
259284
}
260285
261286
function updateLinkPositions() {
262-
const links = data.links ?? [];
263-
264287
// Keeping the link positions in sync with the simulation
265288
// so we don't need to recalculate _all_ link positions on each tick
266289
// which bogs down the simulation
267-
linkPositions = links.map((link: any) => ({
290+
linkPositions = simulatedLinks.map((link: any) => ({
268291
x1: link.source.x ?? 0,
269292
y1: link.source.y ?? 0,
270293
x2: link.target.x ?? 0,
@@ -275,7 +298,8 @@
275298
// MARK: Pull State
276299
277300
function pullNodesFromSimulation() {
278-
simulatedNodes = cloneNodes ? structuredClone(simulation.nodes()) : simulation.nodes();
301+
const simulationNodes = simulation.nodes();
302+
simulatedNodes = cloneNodes ? structuredClone(simulationNodes) : simulationNodes;
279303
}
280304
281305
function pullAlphaFromSimulation() {
@@ -398,4 +422,9 @@
398422
});
399423
</script>
400424

401-
{@render children?.({ nodes: simulatedNodes, simulation, linkPositions })}
425+
{@render children?.({
426+
nodes: simulatedNodes,
427+
links: simulatedLinks,
428+
simulation,
429+
linkPositions,
430+
})}

packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@
4848
<Group center>
4949
{#each nodes as node, i}
5050
{#if i > 0}
51-
<Circle cx={node.x} cy={node.y} r={node.r} fill={groupColor(node.group)} />
51+
<Circle
52+
cx={node.x}
53+
cy={node.y}
54+
r={node.r}
55+
fill={groupColor(node.group.toString())}
56+
/>
5257
{/if}
5358
{/each}
5459
</Group>

packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.svelte

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
<script lang="ts">
2-
import { forceManyBody, forceLink, forceX, forceY } from 'd3-force';
2+
import {
3+
forceManyBody,
4+
forceLink,
5+
forceX,
6+
forceY,
7+
type SimulationNodeDatum,
8+
type SimulationLinkDatum,
9+
} from 'd3-force';
310
import { curveLinear } from 'd3-shape';
411
import { scaleOrdinal } from 'd3-scale';
512
import { schemeCategory10 } from 'd3-scale-chromatic';
@@ -10,8 +17,19 @@
1017
1118
let { data } = $props();
1219
13-
const nodes = data.miserables.nodes;
14-
const links = data.miserables.links;
20+
type Node = {
21+
id: string;
22+
group: number;
23+
};
24+
25+
type Link = {
26+
source: string;
27+
target: string;
28+
value: number;
29+
};
30+
31+
const nodes: (Node & SimulationNodeDatum)[] = data.miserables.nodes;
32+
const links: (Link & SimulationLinkDatum<Node & SimulationNodeDatum>)[] = data.miserables.links;
1533
1634
const colorScale = scaleOrdinal(schemeCategory10);
1735
@@ -20,6 +38,10 @@
2038
const chargeForce = forceManyBody().strength(-30).theta(0.9);
2139
const xForce = forceX();
2240
const yForce = forceY();
41+
42+
function keyForLink(link: Link & SimulationLinkDatum<Node & SimulationNodeDatum>): any {
43+
return link.value + link.index!;
44+
}
2345
</script>
2446

2547
<h1>Examples</h1>
@@ -41,8 +63,8 @@
4163
}}
4264
data={{ nodes, links }}
4365
>
44-
{#snippet children({ nodes, linkPositions })}
45-
{#each links as link, i (link.value + link.index)}
66+
{#snippet children({ nodes, links, linkPositions })}
67+
{#each links as link, i (keyForLink(link))}
4668
<Link
4769
data={link}
4870
explicitCoords={linkPositions[i]}
@@ -52,7 +74,7 @@
5274
{/each}
5375

5476
{#each nodes as node}
55-
<Circle cx={node.x} cy={node.y} r={3} fill={colorScale(node.group)} />
77+
<Circle cx={node.x} cy={node.y} r={3} fill={colorScale(node.group.toString())} />
5678
{/each}
5779
{/snippet}
5880
</ForceSimulation>

packages/layerchart/src/routes/docs/examples/ForceGraph/+page.svelte

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
<script lang="ts">
2-
import { forceCollide, forceManyBody, forceLink, forceCenter } from 'd3-force';
2+
import {
3+
forceCollide,
4+
forceManyBody,
5+
forceLink,
6+
forceCenter,
7+
type SimulationNodeDatum,
8+
type SimulationLinkDatum,
9+
} from 'd3-force';
310
import { curveLinear } from 'd3-shape';
411
import { scaleOrdinal } from 'd3-scale';
512
import { schemeCategory10 } from 'd3-scale-chromatic';
@@ -11,8 +18,19 @@
1118
1219
let { data } = $props();
1320
14-
const nodes = data.miserables.nodes;
15-
const links = data.miserables.links;
21+
type Node = {
22+
id: string;
23+
group: number;
24+
};
25+
26+
type Link = {
27+
source: string;
28+
target: string;
29+
value: number;
30+
};
31+
32+
const nodes: (Node & SimulationNodeDatum)[] = data.miserables.nodes;
33+
const links: (Link & SimulationLinkDatum<Node & SimulationNodeDatum>)[] = data.miserables.links;
1634
1735
const colorScale = scaleOrdinal(schemeCategory10);
1836
@@ -288,7 +306,7 @@
288306
cx={node.x}
289307
cy={node.y}
290308
r={nodeRadius}
291-
fill={colorScale(node.group)}
309+
fill={colorScale(node.group.toString())}
292310
stroke-width={nodeStrokeWidth}
293311
class="stroke-surface-content"
294312
onpointermove={(e) => context.tooltip.show(e, node)}

0 commit comments

Comments
 (0)