UNPKG

netviz

Version:

Network visualization library with multiple layout algorithms and rendering modes. Create interactive, publication-quality network visualizations with a simple, reactive API.

179 lines (159 loc) 4.97 kB
import * as d3 from "d3"; import { computeAutoFit } from "../utils/computeAutoFit.js"; import { applyTransform } from "../utils/applyTransform.js"; import smartLabels from "smart-labels"; /** * renderCanvas - Canvas renderer for force-directed graph * * Original source: https://observablehq.com/@john-guerra/force-directed-graph * From 89207a2280891f15@1859.js lines 490-651 * * @param {Object} opts - Options object with all configuration * @returns {Object} {target: canvas element, ticked: render function} */ export function renderCanvas(opts) { let context; try { if (opts._this?.tagName !== "CANVAS") { throw new Error("recreating canvas"); } context = opts._this?.getContext("2d"); } catch (_e) { // Create new canvas const canvas = document.createElement("canvas"); canvas.width = opts.width; canvas.height = opts.height; context = canvas.getContext("2d"); } const line = d3 .line() .curve(opts.linkCurve) .x((d) => d.x) .y((d) => d.y) .context(context); const zoom = d3 .zoom() .extent([ [0, 0], [opts.width, opts.height], ]) .scaleExtent(opts.zoomScaleExtent) .on("zoom", ({ transform }) => ticked(transform)); function drawNodesAndLinks(transform = d3.zoomTransform(context.canvas)) { if ( opts.drawLinksWhenAlphaIs === null || opts.simulation.alpha() < opts.drawLinksWhenAlphaIs ) { context.save(); // constant opacity if (!opts.LO) { context.globalAlpha = opts.linkStrokeOpacity; } for (const [i, link] of opts.links.entries()) { context.beginPath(); drawLink(link, transform); // Dynamic opacity if (opts.LO) context.globalAlpha = opts.LO[i]; context.strokeStyle = opts.L ? opts.L[i] : opts.linkStroke; context.lineWidth = opts.W ? opts.W[i] : opts.linkStrokeWidth; context.stroke(); } context.restore(); } context.save(); context.globalAlpha = opts.nodeStrokeOpacity; for (const [i, node] of opts.nodes.entries()) { context.beginPath(); drawNode(node, i, transform); context.fillStyle = opts.G ? opts.color(opts.G[i]) : opts.nodeFill; context.strokeStyle = opts.NS ? opts.NS[i] : opts.nodeStroke; context.fill(); context.stroke(); } context.restore(); } function ticked(transform = d3.zoomTransform(context.canvas)) { context.save(); 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(); } } context.clearRect(0, 0, opts.width, opts.height); drawNodesAndLinks(transform); if (opts.nodeLabel) { // Draw Labels context.save(); context.fillStyle = opts.nodeLabelFill; context.textAlign = opts.nodeLabelTextAlign; context.textAnchor = opts.nodeLabelTextAnchor; if (opts.T) { context.font = opts.nodeLabelFont; context.beginPath(); if (opts.useSmartLabels) { smartLabels(opts.nodes, { ...opts.smartLabels, target: context.canvas, 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, renderer: opts.renderer, onHover: () => drawNodesAndLinks(transform), }); } else { for (const [i, node] of opts.nodes.entries()) { drawLabel(node, i, transform); } } context.stroke(); } context.restore(); } context.restore(); } function drawLink(l, transform) { line( opts.useEdgeBundling && l.path ? l.path.map((d) => applyTransform(d, transform, opts)) : [ applyTransform(l.source, transform, opts), applyTransform(l.target, transform, opts), ] ); } function drawNode(d, i, transform) { d = applyTransform(d, transform, opts); context.moveTo(d.x + opts.R[i], d.y); context.arc(d.x, d.y, opts.R[i], 0, 2 * Math.PI); } function drawLabel(d, i, transform) { d = applyTransform(d, transform, opts); context.fillText(opts.T[i], d.x, d.y - opts.R[i] - 2); } return { target: d3 .select(context.canvas) .call( opts .drag(opts.simulation, context.canvas, opts) .on("start.render drag.render end.render", () => { return ticked(d3.zoomTransform(context.canvas)); }) ) .call( opts.useZoom ? zoom : () => {} // do not use zoom ) .node(), ticked, }; }