CirclePackForce.svelte component
Generates an SVG force simulation using d3-force. The values here are defaults which you will likely have to customize because every force simulation is different. This technique comes from @plmrry.
| Param | Default | Required | Description |
|---|---|---|---|
manyBodyStrength number |
5 |
The value passed into the .strength method on forceManyBody, which is used as the 'charge' property on the simulation. See the documentation for more. |
|
xStrength number |
0.1 |
The value passed into the .strength method on forceX, which is used as the 'x' property on the simulation. See the documentation for more. |
|
nodeColor (string | undefined) |
None | Set a color manually otherwise it will default to the zScale. |
|
nodeStroke string |
'#fff' |
The circle's stroke color. | |
nodeStrokeWidth number |
1 |
The circle's stroke width, in pixels. | |
groupBy boolean |
true |
Group the nodes by the return value of the x-scale. If false, align all the nodes to the canvas center. |
Used in these examples:
<!--
@component
Generates an SVG force simulation using [d3-force](https://github.com/d3/d3-force). The values here are defaults which you will likely have to customize because every force simulation is different. This technique comes from @plmrry.
-->
<script>
import { getContext } from 'svelte';
import { forceSimulation, forceX, forceManyBody, forceCollide, forceCenter } from 'd3-force';
const { data, width, height, xScale, xGet, rGet, zGet } = getContext('LayerCake');
/**
* @typedef {Object} Props
* @property {number} [manyBodyStrength=5] - The value passed into the `.strength` method on `forceManyBody`, which is used as the `'charge'` property on the simulation. See [the documentation](https://github.com/d3/d3-force#manyBody_strength) for more.
* @property {number} [xStrength=0.1] - The value passed into the `.strength` method on `forceX`, which is used as the `'x'` property on the simulation. See [the documentation](https://github.com/d3/d3-force#x_strength) for more.
* @property {string|undefined} [nodeColor] - Set a color manually otherwise it will default to the `zScale`.
* @property {string} [nodeStroke='#fff'] - The circle's stroke color.
* @property {number} [nodeStrokeWidth=1] - The circle's stroke width, in pixels.
* @property {boolean} [groupBy=true] - Group the nodes by the return value of the x-scale. If `false`, align all the nodes to the canvas center.
*/
/** @type {Props} */
let {
manyBodyStrength = 5,
xStrength = 0.1,
nodeColor,
nodeStroke = '#fff',
nodeStrokeWidth = 1,
groupBy = true
} = $props();
/* --------------------------------------------
* Make a copy because the simulation will alter the objects
*/
const initialNodes = $data.map(d => ({ ...d }));
const simulation = forceSimulation(initialNodes);
let nodes = $state([]);
simulation.on('tick', () => {
nodes = simulation.nodes();
});
/* ----------------------------------------------
* When variables change, set forces and restart the simulation
*/
$effect(() => {
simulation
.force(
'x',
forceX()
.x(/** @param {any} d */ d => {
return groupBy === true ? $xGet(d) + $xScale.bandwidth() / 2 : $width / 2;
})
.strength(xStrength)
)
.force('center', forceCenter($width / 2, $height / 2))
.force('charge', forceManyBody().strength(manyBodyStrength))
.force(
'collision',
forceCollide().radius(/** @param {any} d */ d => {
return $rGet(d) + nodeStrokeWidth / 2; // Divide this by two because an svg stroke is drawn halfway out
})
)
.force('center', forceCenter($width / 2, $height / 2))
.alpha(1)
.restart();
});
</script>
{#each nodes as point}
<circle
class="node"
r={$rGet(point)}
fill={nodeColor || $zGet(point)}
stroke={nodeStroke}
stroke-width={nodeStrokeWidth}
cx={point.x}
cy={point.y}
>
<!-- <title>{point[$custom.title]}</title> -->
</circle>
{/each}