netviz
Version:
Network visualization library with multiple layout algorithms and rendering modes. Create interactive, publication-quality network visualizations with a simple, reactive API.
180 lines (159 loc) • 4.89 kB
JavaScript
import * as d3 from "d3";
import { computeAutoFit } from "../utils/computeAutoFit.js";
import { applyTransform } from "../utils/applyTransform.js";
import smartLabels from "smart-labels";
/**
* renderSVG - SVG renderer for force-directed graph
*
* Original source: https://observablehq.com/@john-guerra/force-directed-graph
* From 89207a2280891f15@1859.js lines 653-821
*
* @param {Object} opts - Options object with all configuration
* @returns {Object} {target: SVG element, ticked: render function}
*/
export function renderSVG(opts) {
let svg;
try {
if (opts._this?.tagName !== "svg") {
throw new Error("recreating svg");
}
svg = d3.select(opts._this);
} catch {
svg = d3.create("svg");
}
const line = d3
.line()
.curve(opts.linkCurve)
.x((d) => d.x)
.y((d) => d.y);
svg
.attr("width", opts.width)
.attr("height", opts.height)
.attr("viewBox", [0, 0, opts.width, opts.height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
const linkG = svg
.selectAll("g#gLinks")
.data([0])
.join("g")
.attr("id", "gLinks");
const link = linkG
.attr(
"stroke",
typeof opts.linkStroke !== "function" ? opts.linkStroke : null
)
.attr("fill", "none")
.attr("stroke-opacity", (_, i) =>
opts.LO ? opts.LO[i] : opts.linkStrokeOpacity
)
.attr(
"stroke-width",
typeof opts.linkStrokeWidth !== "function" ? opts.linkStrokeWidth : null
)
.attr("stroke-linecap", opts.linkStrokeLinecap)
.selectAll("path")
.data(opts.links)
.join("path");
const nodeG = svg
.selectAll("g#gNodes")
.data([0])
.join("g")
.attr("id", "gNodes");
const node = nodeG
.attr("fill", opts.nodeFill)
.attr("stroke-opacity", opts.nodeStrokeOpacity)
.attr("stroke-width", opts.nodeStrokeWidth)
.selectAll("circle.node")
.data(opts.nodes)
.join("circle")
.attr("class", "node")
.attr("stroke", (_, i) => (opts.NS ? opts.NS[i] : opts.nodeStroke))
.attr("r", (_, i) => opts.R[i]);
let label = null;
if (!opts.useSmartLabels) {
label = nodeG
.selectAll("text.label")
.data(opts.nodes)
.join("text")
.attr("fill", opts.nodeLabelFill)
.attr("stroke", opts.nodeLabelStroke || "none")
.style("text-anchor", opts.nodeLabelAlign)
.style("text-anchor", opts.nodeLabelTextAnchor)
.style("font", opts.nodeLabelFont)
.attr("class", "label")
.text((_, i) => opts.T[i]);
}
node.call(opts.drag(opts.simulation, svg.node(), opts));
const zoom = d3
.zoom()
.extent([
[0, 0],
[opts.width, opts.height],
])
.scaleExtent(opts.zoomScaleExtent)
.on("zoom", ({ transform }) => ticked(transform));
svg.call(
opts.useZoom ? zoom : () => {} // do not use zoom
);
if (opts.W) link.attr("stroke-width", ({ index: i }) => opts.W[i]);
if (opts.L) link.attr("stroke", ({ index: i }) => opts.L[i]);
if (opts.G) node.attr("fill", ({ index: i }) => opts.color(opts.G[i]));
if (opts.T) node.append("title").text(({ index: i }) => opts.T[i]);
function ticked(transform = d3.zoomTransform(svg.node())) {
computeAutoFit(opts);
if (opts.useEdgeBundling && opts.bundling) {
for (let l of opts.links) {
delete l.path;
}
if (
(!opts.edgeBundling.min_alpha_to_bundle &&
opts.edgeBundling.min_alpha_to_bundle !== 0) ||
opts.simulation.alpha() < opts.edgeBundling.min_alpha_to_bundle
) {
opts.bundling.update();
}
}
link
.attr(
"display",
opts.drawLinksWhenAlphaIs === null ||
opts.simulation.alpha() <= opts.drawLinksWhenAlphaIs
? "block"
: "none"
)
.attr("d", (l) => {
return line(
opts.useEdgeBundling && l.path
? l.path.map((d) => applyTransform(d, transform, opts))
: [
applyTransform(l.source, transform, opts),
applyTransform(l.target, transform, opts),
]
);
});
node
.attr("cx", (d) => applyTransform(d, transform, opts).x)
.attr("cy", (d) => applyTransform(d, transform, opts).y);
if (opts.useSmartLabels && opts.T) {
smartLabels(opts.nodes, {
...opts.smartLabels,
target: nodeG,
label: (_, i) => opts.T[i],
x: (d) => transform.applyX(opts.x(d.x)),
y: (d) => transform.applyY(opts.y(d.y)),
width: opts.width,
height: opts.height,
});
} else if (label) {
label
.attr(
"x",
(d) => applyTransform(d, transform, opts).x + opts.nodeLabelDx
)
.attr(
"y",
(d) => applyTransform(d, transform, opts).y + opts.nodeLabelDy
);
}
}
return { target: svg.node(), ticked };
}