function ForceGraph(
{
nodes,
links
},
_opts = {}
) {
let opts = {
...defaultOpts,
..._opts,
edgeBundling: {
...defaultOpts.edgeBundling,
..._opts.edgeBundling
},
forceInABox: {
...defaultOpts.forceInABox,
..._opts.forceInABox
},
smartLabels: {
...defaultOpts.smartLabels,
..._opts.smartLabels
}
};
if (opts.nodeRadius && typeof opts.nodeRadius !== "function") {
const numberNodeRadius = opts.nodeRadius;
opts.nodeRadius = () => numberNodeRadius;
}
if (opts.nodeStroke && typeof opts.nodeStroke !== "function") {
const nodeStroke = opts.nodeStroke;
opts.nodeStroke = () => nodeStroke;
}
opts.linkSource =
opts.linkSource || (({ source }) => opts.nodeId(source) || source);
opts.linkTarget =
opts.linkTarget || (({ target }) => opts.nodeId(target) || target);
// Compute values.
const N = d3.map(nodes, opts.nodeId).map(intern);
const LS = d3.map(links, opts.linkSource).map(intern);
const LT = d3.map(links, opts.linkTarget).map(intern);
if (opts.nodeLabel === undefined) opts.nodeLabel = (_, i) => N[i];
const T = opts.nodeLabel == null ? null : d3.map(nodes, opts.nodeLabel);
const R = opts.nodeRadius == null ? null : d3.map(nodes, opts.nodeRadius);
const G =
opts.nodeGroup == null ? null : d3.map(nodes, opts.nodeGroup).map(intern);
const NS =
opts.nodeStroke == null ? null : d3.map(nodes, opts.nodeStroke).map(intern);
const W =
typeof opts.linkStrokeWidth !== "function"
? null
: d3.map(links, opts.linkStrokeWidth);
const L =
typeof opts.linkStroke !== "function"
? null
: d3.map(links, opts.linkStroke);
const LO =
typeof opts.linkStrokeOpacity !== "function"
? null
: d3.map(links, opts.linkStrokeOpacity);
opts.x = d3.scaleLinear().domain([0, opts.width]).range([0, opts.width]);
opts.y = d3.scaleLinear().domain([0, opts.height]).range([0, opts.height]);
// Copy nodes
if (opts._this?.nodes) {
// Make a shallow copy to protect against mutation, while
// recycling old nodes to preserve position and velocity.
const oldNodes = new Map(opts._this?.nodes.map((d) => [d.id, d]));
nodes = nodes.map((n, i) =>
Object.assign(
oldNodes.get(opts.nodeId(n)) || { id: N[i], groupBy: G && G[i] }
)
);
} else {
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (n, i) => ({
id: N[i],
groupBy: G && G[i] // we need the groupBy for forceInABox
}));
}
links = d3.map(links, (_, i) => ({ source: LS[i], target: LT[i] }));
// Initialize towards the middle
for (let n of nodes) {
const randomFactor = 3; // how far from the center should nodes be initialized
n.x =
n.x !== undefined
? n.x
: (Math.random() - 0.5) * (opts.width / randomFactor) + opts.width / 2;
n.y =
n.y !== undefined
? n.y
: (Math.random() - 0.5) * (opts.height / randomFactor) +
opts.height / 2;
}
// Compute default domains.
if (G && opts.nodeGroups === undefined) opts.nodeGroups = d3.sort(G);
if (opts.debug) console.log("color", opts.color);
// Construct the scales.
const color = opts.color
? opts.color
: opts.nodeGroup == null
? null
: d3.scaleOrdinal(opts.nodeGroups, opts.colors);
let simulation =
opts._this?.simulation?.alpha(opts.simulationRestartAlpha)?.restart() ||
d3.forceSimulation();
simulation.nodes(nodes);
if (opts.forces) {
for (let k in opts.forces) {
simulation.force(k, opts.forces[k]({ nodes, links }));
}
} else {
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({ index: i }) => N[i]);
if (opts.nodeStrength !== undefined) forceNode.strength(opts.nodeStrength);
if (opts.linkStrength !== undefined) forceLink.strength(opts.linkStrength);
if (opts.forceXStrength === undefined) {
opts.forceXStrength = (opts.height / opts.width) * opts.centeringStrength;
}
if (opts.forceYStrength === undefined) {
opts.forceYStrength = (opts.width / opts.height) * opts.centeringStrength;
}
const forceCollide = opts.collide
? d3
.forceCollide((_, i) => R[i] + opts.collidePadding)
.iterations(opts.collideIterations)
: null;
const forceX = d3.forceX(opts.width / 2).strength(opts.forceXStrength);
const forceY = d3.forceY(opts.height / 2).strength(opts.forceYStrength);
let groupingForce;
if (opts.useForceInABox) {
groupingForce = forceInABox()
.size([opts.width, opts.height - 100]) // Size of the chart
.template(opts.forceInABox.template) // Either treemap or force
.groupBy("groupBy") // We use a fixed groupBy attribute
.strength(opts.forceInABox.strength) // Strength to foci
.links(links) // The graph links. Must be called after setting the grouping attribute (Force template only)
// .enableGrouping(useGroupInABox)
.linkStrengthInterCluster(opts.forceInABox.linkStrengthInterCluster) // linkStrength between nodes of different clusters
.linkStrengthIntraCluster(opts.forceInABox.linkStrengthIntraCluster) // linkStrength between nodes of the same cluster
.forceLinkDistance(opts.forceInABox.forceLinkDistance) // linkDistance between meta-nodes on the template (Force template only)
.forceLinkStrength(opts.forceInABox.forceLinkStrength) // linkStrength between meta-nodes of the template (Force template only)
.forceCharge(opts.forceInABox.forceCharge) // Charge between the meta-nodes (Force template only)
.forceNodeSize(opts.forceInABox.forceNodeSize); // Used to compute the template force nodes size (Force template only)
}
simulation
.force(
"link",
forceLink
.distance(opts.linkDistance)
.strength(
opts.useForceInABox
? groupingForce.getLinkStrength
: opts.linkStrength || 0.1
)
)
.force("charge", forceNode)
.force("x", opts.useForceInABox ? null : opts.disjoint ? forceX : null)
.force("y", opts.useForceInABox ? null : opts.disjoint ? forceY : null)
.force(
"center",
opts.useForceInABox
? null
: !opts.disjoint
? d3.forceCenter(opts.width / 2, opts.height / 2)
: null
)
.force("collide", forceCollide)
.force("forceInABox", opts.useForceInABox ? groupingForce : null);
}
// ***** Edge bundling *****
opts.bundling =
opts.useEdgeBundling &&
edgeBundling(
{ nodes, links: sample(links, opts.edgeBundling.max_links) },
{
...opts.edgeBundling
}
);
// ***** Edge bundling *****
simulation.alphaTarget(opts.alphaTarget).restart();
if (opts.extent) {
simulation.force(
"boundary",
opts.extentForce === "forceBoundary"
? forceBoundary(...opts.extent.flat(2))
.border(opts.extentBorder)
.hardBoundary(true)
.strength(opts.extentStrength)
: opts.extentForce === "forceExtent"
? forceExtent(opts.extent)
: forceTransport(opts.extent, 5, opts.extentStrength * 10)
);
}
opts = {
...opts,
nodes,
links,
N,
LS,
LT,
T,
G,
W,
L,
LO,
R,
NS,
drag,
simulation,
color
};
const { target, ticked } =
opts.renderer === "canvas" ? renderCanvas(opts) : renderSVG(opts);
simulation.on("tick", ticked);
(opts.invalidation || invalidation).then(() => {
console.log("🚫 Invalidation -> Stopping simulation");
simulation.stop();
});
function intern(value) {
return value !== null && typeof value === "object"
? value.valueOf()
: value;
}
ticked();
return Object.assign(target, {
scales: { color },
simulation,
nodes,
links
});
}