netviz
Version:
Network visualization library with multiple layout algorithms and rendering modes. Create interactive, publication-quality network visualizations with a simple, reactive API.
1 lines • 183 kB
Source Map (JSON)
{"version":3,"file":"netviz.mjs","sources":["../node_modules/force-in-a-box/dist/forceInABox.esm.js","../node_modules/d3-force-boundary/dist/d3-force-boundary.esm.js","../src/forces/forceTransport.js","../src/forces/forceExtent.js","../src/forces/edgeBundling.js","../src/utils/sample.js","../src/utils/drag.js","../src/utils/computeAutoFit.js","../src/utils/applyTransform.js","../node_modules/smart-labels/dist/smartLabels.es.js","../src/renderers/renderCanvas.js","../src/renderers/renderSVG.js","../src/defaults.js","../src/forceGraph.js","../src/utils/filterNetwork.js","../src/index.js"],"sourcesContent":["// https://github.com/john-guerra/forceInABox#readme v1.0.2 Copyright 2024 undefined\nimport * as d3 from 'd3';\n\nfunction forceInABox() {\n // d3 style\n function constant(_) {\n return () => _;\n }\n\n function index(d) {\n return d.index;\n }\n\n let id = index,\n nodes = [],\n links = [], //needed for the force version\n tree,\n size = [100, 100],\n forceNodeSize = constant(1), // The expected node size used for computing the cluster node\n forceCharge = constant(-1),\n forceLinkDistance = constant(100),\n forceLinkStrength = constant(0.1),\n foci = {},\n // oldStart = force.start,\n linkStrengthIntraCluster = 0.1,\n linkStrengthInterCluster = 0.001,\n // oldGravity = force.gravity(),\n templateNodes = [],\n offset = [0, 0],\n templateForce,\n groupBy = function (d) {\n return d.cluster;\n },\n template = \"treemap\",\n enableGrouping = true,\n strength = 0.1;\n // showingTemplate = false;\n\n function force(alpha) {\n if (!enableGrouping) {\n return force;\n }\n if (template === \"force\") {\n //Do the tick of the template force and get the new focis\n templateForce.tick();\n getFocisFromTemplate();\n }\n\n for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {\n node = nodes[i];\n node.vx += (foci[groupBy(node)].x - node.x) * k;\n node.vy += (foci[groupBy(node)].y - node.y) * k;\n }\n }\n\n function initialize() {\n if (!nodes) return;\n\n // let i,\n // n = nodes.length,\n // m = links.length,\n // nodeById = map(nodes, id),\n // link;\n\n if (template === \"treemap\") {\n initializeWithTreemap();\n } else {\n initializeWithForce();\n }\n }\n\n force.initialize = function (_) {\n nodes = _;\n initialize();\n };\n\n function getLinkKey(l) {\n let sourceID = groupBy(l.source),\n targetID = groupBy(l.target);\n\n return sourceID <= targetID\n ? sourceID + \"~\" + targetID\n : targetID + \"~\" + sourceID;\n }\n\n function computeClustersNodeCounts(nodes) {\n let clustersCounts = new Map(),\n tmpCount = {};\n\n nodes.forEach(function (d) {\n if (!clustersCounts.has(groupBy(d))) {\n clustersCounts.set(groupBy(d), { count: 0, sumforceNodeSize: 0 });\n }\n });\n\n nodes.forEach(function (d) {\n // if (!d.show) { return; }\n tmpCount = clustersCounts.get(groupBy(d));\n tmpCount.count = tmpCount.count + 1;\n tmpCount.sumforceNodeSize =\n tmpCount.sumforceNodeSize +\n Math.PI * (forceNodeSize(d) * forceNodeSize(d)) * 1.3;\n clustersCounts.set(groupBy(d), tmpCount);\n });\n\n return clustersCounts;\n }\n\n //Returns\n function computeClustersLinkCounts(links) {\n let dClusterLinks = new Map(),\n clusterLinks = [];\n\n links.forEach(function (l) {\n let key = getLinkKey(l),\n count;\n if (dClusterLinks.has(key)) {\n count = dClusterLinks.get(key);\n } else {\n count = 0;\n }\n count += 1;\n dClusterLinks.set(key, count);\n });\n\n dClusterLinks.forEach(function (value, key) {\n let source, target;\n source = key.split(\"~\")[0];\n target = key.split(\"~\")[1];\n if (source !== undefined && target !== undefined) {\n clusterLinks.push({\n source: source,\n target: target,\n count: value,\n });\n }\n });\n return clusterLinks;\n }\n\n //Returns the metagraph of the clusters\n function getGroupsGraph() {\n let gnodes = [],\n glinks = [],\n // edges = [],\n dNodes = new Map(),\n // totalSize = 0,\n c,\n i,\n cc,\n clustersCounts,\n clustersLinks;\n\n clustersCounts = computeClustersNodeCounts(nodes);\n clustersLinks = computeClustersLinkCounts(links);\n\n for (c of clustersCounts.keys()) {\n cc = clustersCounts.get(c);\n gnodes.push({\n id: c,\n size: cc.count,\n r: Math.sqrt(cc.sumforceNodeSize / Math.PI),\n }); // Uses approx meta-node size\n dNodes.set(c, i);\n // totalSize += size;\n }\n\n clustersLinks.forEach(function (l) {\n let source = dNodes.get(l.source),\n target = dNodes.get(l.target);\n if (source !== undefined && target !== undefined) {\n glinks.push({\n source: source,\n target: target,\n count: l.count,\n });\n }\n });\n\n return { nodes: gnodes, links: glinks };\n }\n\n function getGroupsTree() {\n let children = [],\n c,\n cc,\n clustersCounts;\n\n clustersCounts = computeClustersNodeCounts(force.nodes());\n\n for (c of clustersCounts.keys()) {\n cc = clustersCounts.get(c);\n children.push({ id: c, size: cc.count });\n }\n return { id: \"clustersTree\", children: children };\n }\n\n function getFocisFromTemplate() {\n //compute foci\n foci.none = { x: 0, y: 0 };\n templateNodes.forEach(function (d) {\n if (template === \"treemap\") {\n foci[d.data.id] = {\n x: d.x0 + (d.x1 - d.x0) / 2 - offset[0],\n y: d.y0 + (d.y1 - d.y0) / 2 - offset[1],\n };\n } else {\n foci[d.id] = {\n x: d.x - offset[0],\n y: d.y - offset[1],\n };\n }\n });\n return foci;\n }\n\n function initializeWithTreemap() {\n let treemap = d3.treemap().size(force.size());\n\n tree = d3\n .hierarchy(getGroupsTree())\n .sum(function (d) {\n return d.size;\n })\n .sort(function (a, b) {\n return b.height - a.height || b.value - a.value;\n });\n\n templateNodes = treemap(tree).leaves();\n getFocisFromTemplate();\n }\n\n function checkLinksAsObjects() {\n // Check if links come in the format of indexes instead of objects\n let linkCount = 0;\n if (nodes.length === 0) return;\n\n links.forEach(function (link) {\n let source, target;\n if (!nodes) return;\n source = link.source;\n target = link.target;\n if (typeof link.source !== \"object\") source = nodes[link.source];\n if (typeof link.target !== \"object\") target = nodes[link.target];\n if (source === undefined || target === undefined) {\n // console.error(link);\n throw Error(\n \"Error setting links, couldnt find nodes for a link (see it on the console)\"\n );\n }\n link.source = source;\n link.target = target;\n link.index = linkCount++;\n });\n }\n\n function initializeWithForce() {\n let net;\n\n if (!nodes || !nodes.length) {\n return;\n }\n\n if (nodes && nodes.length > 0) {\n if (groupBy(nodes[0]) === undefined) {\n throw Error(\n \"Couldnt find the grouping attribute for the nodes. Make sure to set it up with forceInABox.groupBy('clusterAttr') before calling .links()\"\n );\n }\n }\n\n checkLinksAsObjects();\n\n net = getGroupsGraph();\n templateForce = d3\n .forceSimulation(net.nodes)\n .force(\"x\", d3.forceX(size[0] / 2).strength(0.1))\n .force(\"y\", d3.forceY(size[1] / 2).strength(0.1))\n .force(\n \"collide\",\n d3\n .forceCollide(function (d) {\n return d.r;\n })\n .iterations(4)\n )\n .force(\"charge\", d3.forceManyBody().strength(forceCharge))\n .force(\n \"links\",\n d3\n .forceLink(net.nodes.length ? net.links : [])\n .distance(forceLinkDistance)\n .strength(forceLinkStrength)\n );\n\n // console.log(\"Initialize with force \", templateForce.nodes().length, \" \", templateForce.force(\"links\").links().length);\n\n // let i = 0;\n // while (i++ < 500) templateForce.tick();\n\n templateNodes = templateForce.nodes();\n\n getFocisFromTemplate();\n }\n\n function drawTreemap(container) {\n // Delete the circle Template if it exists\n container.selectAll(\"circle.cell\").remove();\n container.selectAll(\"line.cell\").remove();\n container\n .selectAll(\"rect.cell\")\n .data(templateNodes)\n .enter()\n .append(\"svg:rect\")\n .attr(\"class\", \"cell\")\n .attr(\"x\", function (d) {\n return d.x0;\n })\n .attr(\"y\", function (d) {\n return d.y0;\n })\n .attr(\"width\", function (d) {\n return d.x1 - d.x0;\n })\n .attr(\"height\", function (d) {\n return d.y1 - d.y0;\n });\n }\n\n function drawGraph(container) {\n // Delete the treemap if any\n container.selectAll(\"rect.cell\").remove();\n let templateLinksSel = container\n .selectAll(\"line.cell\")\n .data(templateForce.force(\"links\").links());\n templateLinksSel\n .enter()\n .append(\"line\")\n .attr(\"class\", \"cell\")\n .merge(templateLinksSel)\n .attr(\"x2\", function (d) {\n return d.source.x;\n })\n .attr(\"y2\", function (d) {\n return d.source.y;\n })\n .attr(\"x1\", function (d) {\n return d.target.x;\n })\n .attr(\"y1\", function (d) {\n return d.target.y;\n })\n .style(\"stroke-width\", \"1px\")\n .style(\"stroke-opacity\", \"0.5\");\n\n let templateNodesSel = container\n .selectAll(\"circle.cell\")\n .data(templateForce.nodes());\n templateNodesSel\n .enter()\n .append(\"svg:circle\")\n .attr(\"class\", \"cell\")\n .merge(templateNodesSel)\n .attr(\"cx\", function (d) {\n return d.x;\n })\n .attr(\"cy\", function (d) {\n return d.y;\n })\n .attr(\"r\", function (d) {\n return d.r;\n });\n\n templateForce\n .on(\"tick\", () => {\n // console.log(\"tick\");\n drawGraph(container);\n })\n .restart();\n\n templateNodesSel.exit().remove();\n templateLinksSel.exit().remove();\n }\n\n force.drawTemplate = function (container) {\n // showingTemplate = true;\n if (template === \"treemap\") {\n drawTreemap(container);\n } else {\n drawGraph(container);\n }\n return force;\n };\n\n //Backwards compatibility\n force.drawTreemap = force.drawTemplate;\n\n force.deleteTemplate = function (container) {\n // showingTemplate = false;\n container.selectAll(\".cell\").remove();\n\n if (templateForce) {\n templateForce.on(\"tick\", null).restart();\n }\n\n return force;\n };\n\n force.template = function (x) {\n if (!arguments.length) return template;\n template = x;\n initialize();\n return force;\n };\n\n force.groupBy = function (x) {\n if (!arguments.length) return groupBy;\n if (typeof x === \"string\") {\n groupBy = function (d) {\n return d[x];\n };\n return force;\n }\n groupBy = x;\n return force;\n };\n\n force.enableGrouping = function (x) {\n if (!arguments.length) return enableGrouping;\n enableGrouping = x;\n // update();\n return force;\n };\n\n force.strength = function (x) {\n if (!arguments.length) return strength;\n strength = x;\n return force;\n };\n\n force.getLinkStrength = function (e) {\n if (enableGrouping) {\n if (groupBy(e.source) === groupBy(e.target)) {\n if (typeof linkStrengthIntraCluster === \"function\") {\n return linkStrengthIntraCluster(e);\n } else {\n return linkStrengthIntraCluster;\n }\n } else {\n if (typeof linkStrengthInterCluster === \"function\") {\n return linkStrengthInterCluster(e);\n } else {\n return linkStrengthInterCluster;\n }\n }\n } else {\n // Not grouping return the intracluster\n if (typeof linkStrengthIntraCluster === \"function\") {\n return linkStrengthIntraCluster(e);\n } else {\n return linkStrengthIntraCluster;\n }\n }\n };\n\n force.id = function (_) {\n return arguments.length ? ((id = _), force) : id;\n };\n\n force.size = function (_) {\n return arguments.length ? ((size = _), force) : size;\n };\n\n force.linkStrengthInterCluster = function (_) {\n return arguments.length\n ? ((linkStrengthInterCluster = _), force)\n : linkStrengthInterCluster;\n };\n\n force.linkStrengthIntraCluster = function (_) {\n return arguments.length\n ? ((linkStrengthIntraCluster = _), force)\n : linkStrengthIntraCluster;\n };\n\n force.nodes = function (_) {\n return arguments.length ? ((nodes = _), force) : nodes;\n };\n\n force.links = function (_) {\n if (!arguments.length) return links;\n if (_ === null) links = [];\n else links = _;\n initialize();\n return force;\n };\n\n force.forceNodeSize = function (_) {\n return arguments.length\n ? ((forceNodeSize = typeof _ === \"function\" ? _ : constant(+_)),\n initialize(),\n force)\n : forceNodeSize;\n };\n\n // Legacy support\n force.nodeSize = force.forceNodeSize;\n\n force.forceCharge = function (_) {\n return arguments.length\n ? ((forceCharge = typeof _ === \"function\" ? _ : constant(+_)),\n initialize(),\n force)\n : forceCharge;\n };\n\n force.forceLinkDistance = function (_) {\n return arguments.length\n ? ((forceLinkDistance = typeof _ === \"function\" ? _ : constant(+_)),\n initialize(),\n force)\n : forceLinkDistance;\n };\n\n force.forceLinkStrength = function (_) {\n return arguments.length\n ? ((forceLinkStrength = typeof _ === \"function\" ? _ : constant(+_)),\n initialize(),\n force)\n : forceLinkStrength;\n };\n\n force.offset = function (_) {\n return arguments.length\n ? ((offset = typeof _ === \"function\" ? _ : constant(+_)), force)\n : offset;\n };\n\n force.getFocis = getFocisFromTemplate;\n\n return force;\n}\n\n// module.exports = forceInABox;\n\nexport { forceInABox as default };\n","// https://observablehq.com/@john-guerra/d3-force-boundary v0.0.2 Copyright 2022 John Alexis Guerra Gómez\nfunction constant(x) {\n return function() {\n return x;\n };\n}\n\nfunction forceBoundary(x0, y0, x1, y1) {\n var strength = constant(0.1),\n hardBoundary = true,\n border = constant( Math.min((x1 - x0)/2, (y1 - y0)/2) ),\n nodes,\n strengthsX,\n strengthsY,\n x0z, x1z,\n y0z, y1z,\n borderz,\n halfX, halfY;\n\n if (typeof x0 !== \"function\") x0 = constant(x0 == null ? -100 : +x0);\n if (typeof x1 !== \"function\") x1 = constant(x1 == null ? 100 : +x1);\n if (typeof y0 !== \"function\") y0 = constant(y0 == null ? -100 : +y0);\n if (typeof y1 !== \"function\") y1 = constant(y1 == null ? 100 : +y1);\n\n function getVx(halfX, x, strengthX, border, alpha) {\n return (halfX - x) * Math.min(2, Math.abs( halfX - x) / halfX) * strengthX * alpha;\n }\n\n function force(alpha) {\n for (var i = 0, n = nodes.length, node; i < n; ++i) {\n node = nodes[i];\n // debugger;\n if ((node.x < (x0z[i] + borderz[i]) || node.x > (x1z[i] - borderz[i])) ||\n (node.y < (y0z[i] + borderz[i]) || node.y > (y1z[i] - borderz[i]))) {\n node.vx += getVx(halfX[i], node.x, strengthsX[i], borderz[i], alpha);\n node.vy += getVx(halfY[i], node.y, strengthsY[i], borderz[i], alpha);\n } else if (node.y < (y0z[i] + borderz[i]) || node.y > (y1z[i] - borderz[i])) ;\n\n if (hardBoundary) {\n if (node.x >= x1z[i]) node.vx += x1z[i] - node.x;\n if (node.x <= x0z[i]) node.vx += x0z[i] - node.x;\n if (node.y >= y1z[i]) node.vy += y1z[i] - node.y;\n if (node.y <= y0z[i]) node.vy += y0z[i] - node.y;\n }\n }\n }\n\n function initialize() {\n if (!nodes) return;\n var i, n = nodes.length;\n strengthsX = new Array(n);\n strengthsY = new Array(n);\n x0z = new Array(n);\n y0z = new Array(n);\n x1z = new Array(n);\n y1z = new Array(n);\n halfY = new Array(n);\n halfX = new Array(n);\n borderz = new Array(n);\n\n for (i = 0; i < n; ++i) {\n strengthsX[i] = (isNaN(x0z[i] = +x0(nodes[i], i, nodes)) ||\n isNaN(x1z[i] = +x1(nodes[i], i, nodes))) ? 0 : +strength(nodes[i], i, nodes);\n strengthsY[i] = (isNaN(y0z[i] = +y0(nodes[i], i, nodes)) ||\n isNaN(y1z[i] = +y1(nodes[i], i, nodes))) ? 0 : +strength(nodes[i], i, nodes);\n halfX[i] = x0z[i] + (x1z[i] - x0z[i])/2,\n halfY[i] = y0z[i] + (y1z[i] - y0z[i])/2;\n borderz[i] = +border(nodes[i], i, nodes);\n }\n }\n\n force.initialize = function(_) {\n nodes = _;\n initialize();\n };\n\n force.x0 = function(_) {\n return arguments.length ? (x0 = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : x0;\n };\n\n force.x1 = function(_) {\n return arguments.length ? (x1 = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : x1;\n };\n\n force.y0 = function(_) {\n return arguments.length ? (y0 = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : y0;\n };\n\n force.y1 = function(_) {\n return arguments.length ? (y1 = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : y1;\n };\n\n force.strength = function(_) {\n return arguments.length ? (strength = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : strength;\n };\n\n force.border = function(_) {\n return arguments.length ? (border = typeof _ === \"function\" ? _ : constant(+_), initialize(), force) : border;\n };\n\n force.hardBoundary = function(_) {\n return arguments.length ? (hardBoundary = _, force) : hardBoundary;\n };\n\n return force;\n}\n\nexport default forceBoundary;\n","import * as d3 from \"d3\";\n\n/**\n * forceTransport - A D3 force that transports nodes to stay within bounds\n * by sorting them and distributing them evenly within the extent.\n *\n * Original source: https://observablehq.com/d/21d2053b3bc85bce\n * Implementation discussion: https://github.com/d3/d3-force/issues/89\n *\n * @param {Array} extent - [[x0, y0], [x1, y1]] bounding box\n * @param {number} margin - Margin from the extent edges (default: 0)\n * @param {number} strength - Force strength multiplier (default: 1)\n * @returns {Function} D3 force function\n */\nexport function forceTransport(extent, margin, strength) {\n let nodes;\n\n if (extent === undefined)\n extent = [\n [0, 0],\n [960, 500],\n ];\n if (margin === undefined) margin = 0;\n if (strength === undefined) strength = 1;\n\n const X = d3\n .scaleLinear()\n .range([extent[0][0] + margin, extent[1][0] - margin]);\n const Y = d3\n .scaleLinear()\n .range([extent[0][1] + margin, extent[1][1] - margin]);\n\n let indices = [];\n\n function force(alpha) {\n if (indices.length !== nodes.length) {\n indices = Uint32Array.from(d3.range(nodes.length));\n X.domain([-1, nodes.length]);\n Y.domain([-1, nodes.length]);\n }\n\n // Sort nodes by x position and distribute them evenly\n indices.sort((i, j) => nodes[i].x - nodes[j].x);\n for (let i = 0; i < nodes.length; ++i) {\n const node = nodes[indices[i]];\n const target = X(i);\n node.vx += (target - node.x) * strength;\n }\n\n // Sort nodes by y position and distribute them evenly\n indices.sort((i, j) => nodes[i].y - nodes[j].y);\n for (let i = 0; i < nodes.length; ++i) {\n const node = nodes[indices[i]];\n const target = Y(i);\n node.vy += (target - node.y) * strength * alpha;\n }\n }\n\n force.initialize = function (_) {\n nodes = _;\n };\n\n force.extent = function (_) {\n return arguments.length ? ((extent = _), force) : extent;\n };\n\n force.margin = function (_) {\n return arguments.length ? ((margin = +_), force) : margin;\n };\n\n force.strength = function (_) {\n return arguments.length ? ((strength = +_), force) : strength;\n };\n\n return force;\n}\n","/**\n * forceExtent - A D3 force that clamps nodes to stay within bounds\n *\n * Original source: https://observablehq.com/d/21d2053b3bc85bce\n * Implementation discussion: https://github.com/d3/d3-force/issues/89\n *\n * @param {Array} extent - [[x0, y0], [x1, y1]] bounding box\n * @returns {Function} D3 force function\n */\nexport function forceExtent(extent) {\n let nodes;\n\n if (extent === undefined)\n extent = [\n [0, 0],\n [960, 500],\n ];\n\n function clamp(x, min, max) {\n return Math.max(min, Math.min(max, x));\n }\n\n function force() {\n for (let i = 0; i < nodes.length; ++i) {\n const node = nodes[i];\n const r = node.radius || 0;\n node.x = clamp(node.x, extent[0][0] - r, extent[1][0] + r);\n node.y = clamp(node.y, extent[0][1] - r, extent[1][1] + r);\n }\n }\n\n force.initialize = function (_) {\n nodes = _;\n };\n\n force.extent = function (_) {\n return arguments.length ? ((extent = _), force) : extent;\n };\n\n return force;\n}\n","/**\n * Force Edge Bundling\n *\n * Bundles edges together using a force simulation.\n *\n * Original source: https://observablehq.com/@john-guerra/force-edge-bundling\n * Adapted from: https://github.com/upphiminn/d3.ForceBundle\n *\n * LICENSE: GNU General Public License v2\n * Copyright (C) John Alexis Guerra Gómez\n *\n * This is an adaptation of the Force Directed Edge Bundling d3 plugin to work\n * with ES6 and modern D3, with bug fixes from:\n * https://github.com/upphiminn/d3.ForceBundle/pull/11\n *\n * Note: Make sure no two nodes have the same x,y coordinates\n */\n\n/**\n * ForceEdgeBundling - Core edge bundling algorithm\n * @returns {Function} Edge bundling force function\n */\nexport function ForceEdgeBundling() {\n let data_nodes = {}, // {'nodeid':{'x':,'y':},..}\n data_edges = [], // [{'source':'nodeid1', 'target':'nodeid2'},..]\n compatibility_list_for_edge = [],\n subdivision_points_for_edge = [],\n K = 0.1, // global bundling constant controlling edge stiffness\n S_initial = 0.1, // init. distance to move points\n P_initial = 1, // init. subdivision number\n P_rate = 2, // subdivision rate increase\n C = 6, // number of cycles to perform\n I_initial = 90, // init. number of iterations for cycle\n I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3\n compatibility_threshold = 0.6,\n eps = 1e-6,\n P = null;\n\n /*** Geometry Helper Methods ***/\n function vector_dot_product(p, q) {\n return p.x * q.x + p.y * q.y;\n }\n\n function edge_as_vector(P) {\n return {\n x: data_nodes[P.target].x - data_nodes[P.source].x,\n y: data_nodes[P.target].y - data_nodes[P.source].y,\n };\n }\n\n function edge_length(e) {\n // handling nodes that are on the same location, so that K/edge_length != Inf\n if (\n Math.abs(data_nodes[e.source].x - data_nodes[e.target].x) < eps &&\n Math.abs(data_nodes[e.source].y - data_nodes[e.target].y) < eps\n ) {\n return eps;\n }\n\n return Math.sqrt(\n Math.pow(data_nodes[e.source].x - data_nodes[e.target].x, 2) +\n Math.pow(data_nodes[e.source].y - data_nodes[e.target].y, 2)\n );\n }\n\n function custom_edge_length(e) {\n return Math.sqrt(\n Math.pow(e.source.x - e.target.x, 2) +\n Math.pow(e.source.y - e.target.y, 2)\n );\n }\n\n function edge_midpoint(e) {\n let middle_x = (data_nodes[e.source].x + data_nodes[e.target].x) / 2.0;\n let middle_y = (data_nodes[e.source].y + data_nodes[e.target].y) / 2.0;\n\n return {\n x: middle_x,\n y: middle_y,\n };\n }\n\n function compute_divided_edge_length(e_idx) {\n let length = 0;\n\n for (let i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {\n let segment_length = euclidean_distance(\n subdivision_points_for_edge[e_idx][i],\n subdivision_points_for_edge[e_idx][i - 1]\n );\n length += segment_length;\n }\n\n return length;\n }\n\n function euclidean_distance(p, q) {\n return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2));\n }\n\n function project_point_on_line(p, Q) {\n let L = Math.sqrt(\n (Q.target.x - Q.source.x) * (Q.target.x - Q.source.x) +\n (Q.target.y - Q.source.y) * (Q.target.y - Q.source.y)\n );\n let r =\n ((Q.source.y - p.y) * (Q.source.y - Q.target.y) -\n (Q.source.x - p.x) * (Q.target.x - Q.source.x)) /\n (L * L);\n\n return {\n x: Q.source.x + r * (Q.target.x - Q.source.x),\n y: Q.source.y + r * (Q.target.y - Q.source.y),\n };\n }\n\n /*** Initialization Methods ***/\n function initialize_edge_subdivisions() {\n for (let i = 0; i < data_edges.length; i++) {\n if (P_initial === 1) {\n subdivision_points_for_edge[i] = []; //0 subdivisions\n } else {\n subdivision_points_for_edge[i] = [];\n subdivision_points_for_edge[i].push(data_nodes[data_edges[i].source]);\n subdivision_points_for_edge[i].push(data_nodes[data_edges[i].target]);\n }\n }\n }\n\n function initialize_compatibility_lists() {\n for (let i = 0; i < data_edges.length; i++) {\n compatibility_list_for_edge[i] = []; //0 compatible edges.\n }\n }\n\n function filter_self_loops(edgelist) {\n let filtered_edge_list = [];\n\n for (let e = 0; e < edgelist.length; e++) {\n if (\n data_nodes[edgelist[e].source].x != data_nodes[edgelist[e].target].x ||\n data_nodes[edgelist[e].source].y != data_nodes[edgelist[e].target].y\n ) {\n //or smaller than eps\n filtered_edge_list.push(edgelist[e]);\n }\n }\n\n return filtered_edge_list;\n }\n\n /*** Force Calculation Methods ***/\n function apply_spring_force(e_idx, i, kP) {\n let prev = subdivision_points_for_edge[e_idx][i - 1];\n let next = subdivision_points_for_edge[e_idx][i + 1];\n let crnt = subdivision_points_for_edge[e_idx][i];\n let x = prev.x - crnt.x + next.x - crnt.x;\n let y = prev.y - crnt.y + next.y - crnt.y;\n\n x *= kP;\n y *= kP;\n\n return {\n x: x,\n y: y,\n };\n }\n\n function apply_electrostatic_force(e_idx, i) {\n let sum_of_forces = {\n x: 0,\n y: 0,\n };\n let compatible_edges_list = compatibility_list_for_edge[e_idx];\n\n for (let oe = 0; oe < compatible_edges_list.length; oe++) {\n let force = {\n x:\n subdivision_points_for_edge[compatible_edges_list[oe]][i].x -\n subdivision_points_for_edge[e_idx][i].x,\n y:\n subdivision_points_for_edge[compatible_edges_list[oe]][i].y -\n subdivision_points_for_edge[e_idx][i].y,\n };\n\n if (Math.abs(force.x) > eps || Math.abs(force.y) > eps) {\n let diff =\n 1 /\n Math.pow(\n custom_edge_length({\n source: subdivision_points_for_edge[compatible_edges_list[oe]][i],\n target: subdivision_points_for_edge[e_idx][i],\n }),\n 1\n );\n\n sum_of_forces.x += force.x * diff;\n sum_of_forces.y += force.y * diff;\n }\n }\n\n return sum_of_forces;\n }\n\n function apply_resulting_forces_on_subdivision_points(e_idx, P, S) {\n let kP = K / (edge_length(data_edges[e_idx]) * (P + 1)); // kP=K/|P|(number of segments), where |P| is the initial length of edge P.\n // (length * (num of sub division pts - 1))\n let resulting_forces_for_subdivision_points = [\n {\n x: 0,\n y: 0,\n },\n ];\n\n for (let i = 1; i < P + 1; i++) {\n // exclude initial end points of the edge 0 and P+1\n let resulting_force = {\n x: 0,\n y: 0,\n };\n\n let spring_force = apply_spring_force(e_idx, i, kP);\n let electrostatic_force = apply_electrostatic_force(e_idx, i, S);\n\n resulting_force.x = S * (spring_force.x + electrostatic_force.x);\n resulting_force.y = S * (spring_force.y + electrostatic_force.y);\n\n resulting_forces_for_subdivision_points.push(resulting_force);\n }\n\n resulting_forces_for_subdivision_points.push({\n x: 0,\n y: 0,\n });\n\n return resulting_forces_for_subdivision_points;\n }\n\n /*** Edge Division Calculation Methods ***/\n function update_edge_divisions(P) {\n for (let e_idx = 0; e_idx < data_edges.length; e_idx++) {\n if (P === 1) {\n subdivision_points_for_edge[e_idx].push(\n data_nodes[data_edges[e_idx].source]\n ); // source\n subdivision_points_for_edge[e_idx].push(\n edge_midpoint(data_edges[e_idx])\n ); // mid point\n subdivision_points_for_edge[e_idx].push(\n data_nodes[data_edges[e_idx].target]\n ); // target\n } else {\n let divided_edge_length = compute_divided_edge_length(e_idx);\n let segment_length = divided_edge_length / (P + 1);\n let current_segment_length = segment_length;\n let new_subdivision_points = [];\n new_subdivision_points.push(data_nodes[data_edges[e_idx].source]); //source\n\n for (let i = 1; i < subdivision_points_for_edge[e_idx].length; i++) {\n let old_segment_length = euclidean_distance(\n subdivision_points_for_edge[e_idx][i],\n subdivision_points_for_edge[e_idx][i - 1]\n );\n\n while (old_segment_length > current_segment_length) {\n let percent_position = current_segment_length / old_segment_length;\n let new_subdivision_point_x =\n subdivision_points_for_edge[e_idx][i - 1].x;\n let new_subdivision_point_y =\n subdivision_points_for_edge[e_idx][i - 1].y;\n\n new_subdivision_point_x +=\n percent_position *\n (subdivision_points_for_edge[e_idx][i].x -\n subdivision_points_for_edge[e_idx][i - 1].x);\n new_subdivision_point_y +=\n percent_position *\n (subdivision_points_for_edge[e_idx][i].y -\n subdivision_points_for_edge[e_idx][i - 1].y);\n new_subdivision_points.push({\n x: new_subdivision_point_x,\n y: new_subdivision_point_y,\n });\n\n old_segment_length -= current_segment_length;\n current_segment_length = segment_length;\n }\n current_segment_length -= old_segment_length;\n }\n new_subdivision_points.push(data_nodes[data_edges[e_idx].target]); //target\n subdivision_points_for_edge[e_idx] = new_subdivision_points;\n }\n }\n }\n\n /*** Edge compatibility measures ***/\n function angle_compatibility(P, Q) {\n return Math.abs(\n vector_dot_product(edge_as_vector(P), edge_as_vector(Q)) /\n (edge_length(P) * edge_length(Q))\n );\n }\n\n function scale_compatibility(P, Q) {\n let lavg = (edge_length(P) + edge_length(Q)) / 2.0;\n return (\n 2.0 /\n (lavg / Math.min(edge_length(P), edge_length(Q)) +\n Math.max(edge_length(P), edge_length(Q)) / lavg)\n );\n }\n\n function position_compatibility(P, Q) {\n let lavg = (edge_length(P) + edge_length(Q)) / 2.0;\n let midP = {\n x: (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,\n y: (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0,\n };\n let midQ = {\n x: (data_nodes[Q.source].x + data_nodes[Q.target].x) / 2.0,\n y: (data_nodes[Q.source].y + data_nodes[Q.target].y) / 2.0,\n };\n\n return lavg / (lavg + euclidean_distance(midP, midQ));\n }\n\n function edge_visibility(P, Q) {\n let I0 = project_point_on_line(data_nodes[Q.source], {\n source: data_nodes[P.source],\n target: data_nodes[P.target],\n });\n let I1 = project_point_on_line(data_nodes[Q.target], {\n source: data_nodes[P.source],\n target: data_nodes[P.target],\n }); //send actual edge points positions\n let midI = {\n x: (I0.x + I1.x) / 2.0,\n y: (I0.y + I1.y) / 2.0,\n };\n let midP = {\n x: (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0,\n y: (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0,\n };\n\n return Math.max(\n 0,\n 1 - (2 * euclidean_distance(midP, midI)) / euclidean_distance(I0, I1)\n );\n }\n\n function visibility_compatibility(P, Q) {\n return Math.min(edge_visibility(P, Q), edge_visibility(Q, P));\n }\n\n function compatibility_score(P, Q) {\n return (\n angle_compatibility(P, Q) *\n scale_compatibility(P, Q) *\n position_compatibility(P, Q) *\n visibility_compatibility(P, Q)\n );\n }\n\n function are_compatible(P, Q) {\n return compatibility_score(P, Q) >= compatibility_threshold;\n }\n\n function compute_compatibility_lists() {\n for (let e = 0; e < data_edges.length - 1; e++) {\n for (let oe = e + 1; oe < data_edges.length; oe++) {\n // don't want any duplicates\n if (are_compatible(data_edges[e], data_edges[oe])) {\n compatibility_list_for_edge[e].push(oe);\n compatibility_list_for_edge[oe].push(e);\n }\n }\n }\n }\n\n /*** Main Bundling Loop Methods ***/\n let forcebundle = function () {\n let S = S_initial;\n let I = I_initial;\n let P = P_initial;\n\n initialize_edge_subdivisions();\n initialize_compatibility_lists();\n update_edge_divisions(P);\n compute_compatibility_lists();\n\n for (let cycle = 0; cycle < C; cycle++) {\n for (let iteration = 0; iteration < I; iteration++) {\n let forces = [];\n for (let edge = 0; edge < data_edges.length; edge++) {\n forces[edge] = apply_resulting_forces_on_subdivision_points(\n edge,\n P,\n S\n );\n }\n for (let e = 0; e < data_edges.length; e++) {\n for (let i = 0; i < P + 1; i++) {\n subdivision_points_for_edge[e][i].x += forces[e][i].x;\n subdivision_points_for_edge[e][i].y += forces[e][i].y;\n }\n }\n }\n // prepare for next cycle\n S = S / 2;\n P = P * P_rate;\n I = I_rate * I;\n\n update_edge_divisions(P);\n }\n return subdivision_points_for_edge;\n };\n\n /*** Getters/Setters Methods ***/\n forcebundle.nodes = function (nl) {\n if (arguments.length === 0) {\n return data_nodes;\n } else {\n data_nodes = nl;\n }\n\n return forcebundle;\n };\n\n forcebundle.edges = function (ll) {\n if (arguments.length === 0) {\n return data_edges;\n } else {\n data_edges = filter_self_loops(ll); //remove edges to from to the same point\n }\n\n return forcebundle;\n };\n\n forcebundle.bundling_stiffness = function (k) {\n if (arguments.length === 0) {\n return K;\n } else {\n K = k;\n }\n\n return forcebundle;\n };\n\n forcebundle.step_size = function (step) {\n if (arguments.length === 0) {\n return S_initial;\n } else {\n S_initial = step;\n }\n\n return forcebundle;\n };\n\n forcebundle.cycles = function (c) {\n if (arguments.length === 0) {\n return C;\n } else {\n C = c;\n }\n\n return forcebundle;\n };\n\n forcebundle.iterations = function (i) {\n if (arguments.length === 0) {\n return I_initial;\n } else {\n I_initial = i;\n }\n\n return forcebundle;\n };\n\n forcebundle.iterations_rate = function (i) {\n if (arguments.length === 0) {\n return I_rate;\n } else {\n I_rate = i;\n }\n\n return forcebundle;\n };\n\n forcebundle.subdivision_points_seed = function (p) {\n if (arguments.length == 0) {\n return P;\n } else {\n P = p;\n }\n\n return forcebundle;\n };\n\n forcebundle.subdivision_rate = function (r) {\n if (arguments.length === 0) {\n return P_rate;\n } else {\n P_rate = r;\n }\n\n return forcebundle;\n };\n\n forcebundle.compatibility_threshold = function (t) {\n if (arguments.length === 0) {\n return compatibility_threshold;\n } else {\n compatibility_threshold = t;\n }\n\n return forcebundle;\n };\n\n return forcebundle;\n}\n\n/**\n * edgeBundling - Convenience wrapper for ForceEdgeBundling\n *\n * @param {Object} data - {nodes, links} where nodes have x,y coords\n * @param {Object} options - Configuration options\n * @returns {Object} Edge bundling instance with update() method\n */\nexport function edgeBundling(\n {\n nodes, // Array of nodes including x and y coords e.g. [{id: \"a\", x: 10, y:10}, ...]\n links, // Array of links in D3 forceSimulation format e.g. [{source: \"a\", target: \"b\"}, ...]\n },\n {\n id = (d) => d.id,\n pathAttr = \"path\", // name of the attribute to save the paths\n bundling_stiffness = 0.1, // global bundling constant controlling edge stiffness\n step_size = 0.1, // init. distance to move points\n subdivision_rate = 2, // subdivision rate increase\n cycles = 6, // number of cycles to perform\n iterations = 90, // init. number of iterations for cycle\n iterations_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3\n compatibility_threshold = 0.6, // \"which pairs of edges should be considered compatible (default is set to 0.6, 60% compatiblity)\"\n } = {}\n) {\n // The library wants the links as the index positions in the nodes array\n const dNodes = new Map(nodes.map((d, i) => [id(d), i]));\n const linksIs = links.map((l) => ({\n source: dNodes.get(typeof l.source === \"object\" ? id(l.source) : l.source),\n target: dNodes.get(typeof l.target === \"object\" ? id(l.target) : l.target),\n }));\n\n const edgeBundling = ForceEdgeBundling()\n .nodes(nodes)\n .edges(linksIs)\n .bundling_stiffness(bundling_stiffness)\n .step_size(step_size)\n .subdivision_rate(subdivision_rate)\n .cycles(cycles)\n .iterations(iterations)\n .iterations_rate(iterations_rate)\n .compatibility_threshold(compatibility_threshold);\n\n edgeBundling.update = () => {\n const paths = edgeBundling();\n links.map((l, i) => (l[pathAttr] = paths[i]));\n };\n\n edgeBundling.update();\n\n return edgeBundling;\n}\n","/**\n * sample - Samples an array to limit the number of elements\n * Used for edge bundling to limit the number of links processed\n *\n * Original source: https://observablehq.com/@john-guerra/force-directed-graph\n * From 89207a2280891f15@1859.js lines 885-890\n *\n * @param {Array} array - Array to sample\n * @param {number} n - Maximum number of elements to return\n * @returns {Array} Sampled array\n */\nexport function sample(array, n) {\n if (n >= array.length) return array;\n\n return array.filter((d, i) => i % Math.floor(array.length / n) === 0);\n}\n","import * as d3 from \"d3\";\n\n/**\n * drag - Custom drag behavior that works with zoom transforms\n *\n * Original source: https://observablehq.com/@john-guerra/force-directed-graph\n * From 89207a2280891f15@1859.js lines 830-875\n *\n * @param {Object} simulation - D3 force simulation\n * @param {HTMLElement} node - DOM node (SVG or Canvas element)\n * @param {Object} opts - Options object with x, y scales and minDistanceForDrag\n * @returns {Function} D3 drag behavior\n */\nexport function drag(simulation, node, opts) {\n function dragsubject(event) {\n const transform = d3.zoomTransform(node);\n let [x, y] = transform.invert([event.x, event.y]);\n x = opts.x.invert(x);\n y = opts.y.invert(y);\n let subject = simulation.find(x, y);\n\n let d = Math.hypot(x - subject.x, y - subject.y);\n\n return d < opts.minDistanceForDrag\n ? {\n circle: subject,\n x: transform.applyX(opts.x(subject.x)),\n y: transform.applyY(opts.y(subject.y)),\n }\n : null;\n }\n\n function dragstarted(event) {\n if (!event.active) simulation.alphaTarget(0.3).restart();\n event.subject.circle.fx = event.subject.circle.x;\n event.subject.circle.fy = event.subject.circle.y;\n }\n\n function dragged(event) {\n const transform = d3.zoomTransform(node);\n event.subject.circle.fx = opts.x.invert(transform.invertX(event.x));\n event.subject.circle.fy = opts.y.invert(transform.invertY(event.y));\n }\n\n function dragended(event) {\n if (!event.active) simulation.alphaTarget(0);\n event.subject.circle.fx = null;\n event.subject.circle.fy = null;\n }\n\n return d3\n .drag()\n .subject(dragsubject)\n .on(\"start\", dragstarted)\n .on(\"drag\", dragged)\n .on(\"end\", dragended);\n}\n","import * as d3 from \"d3\";\n\n/**\n * computeAutoFit - Adjusts x/y scales to fit nodes in viewport\n * with optional aspect ratio preservation\n *\n * Original source: https://observablehq.com/@john-guerra/force-directed-graph\n * From 89207a2280891f15@1859.js lines 979-1022\n *\n * @param {Object} opts - Options object with nodes, x, y scales, width, height, autoFit, keepAspectRatio\n */\nexport function computeAutoFit(opts) {\n if (opts.autoFit) {\n const yExtent = d3.extent(opts.nodes, (d) => d.y);\n const xExtent = d3.extent(opts.nodes, (d) => d.x);\n\n opts.x.domain(d3.extent(opts.nodes, (d) => d.x));\n opts.y.domain(d3.extent(opts.nodes, (d) => d.y));\n\n if (opts.keepAspectRatio) {\n const ratio = opts.width / opts.height;\n const newRatio = (xExtent[1] - xExtent[0]) / (yExtent[1] - yExtent[0]);\n\n if (newRatio < ratio) {\n // Adjust x axis to fit\n const d =\n (opts.width / opts.height) * (yExtent[1] - yExtent[0]) -\n (xExtent[1] - xExtent[0]);\n\n opts.x.domain([xExtent[0] - d / 2, xExtent[1] + d / 2]);\n opts.y.domain(yExtent);\n } else {\n // Adjust y axis to fit\n const d =\n (opts.height / opts.width) * (xExtent[1] - xExtent[0]) -\n (yExtent[1] - yExtent[0]);\n\n opts.y.domain([yExtent[0] - d / 2, yExtent[1] + d / 2]);\n opts.x.domain(xExtent);\n }\n }\n }\n}\n","/**\n * applyTransform - Applies zoom transform to node/link coordinates\n *\n * Original source: https://observablehq.com/@john-guerra/force-directed-graph\n * From 89207a2280891f15@1859.js lines 823-828\n *\n * @param {Object} d - Node or point with x, y coordinates\n * @param {Object} transform - D3 zoom transform\n * @param {Object} opts - Options object with x, y scales\n * @returns {Object} Transformed point with x, y\n */\nexport function applyTransform(d, transform, opts) {\n const [x, y] = transform.apply([opts.x(d.x), opts.y(d.y)]);\n return { ...d, x, y };\n}\n","// smart-labels v0.0.11 Copyright (c) 2024 John Alexis Guerra Gómez\nimport * as d3 from 'd3';\n\n// Based on https://observablehq.com/@d3/voronoi-labels by Mike Bostock\n\n// ISC License\n\n// Copyright 2018–2023 Observable, Inc.\n// Permission to use, copy, modify, and/or distribute this software for any\n// purpose with or without fee is hereby granted, provided that the above\n// copyright notice and this permission notice appear in all copies.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\nfunction smartLabels(\n data,\n {\n x = (d) => d[0], // x coordinate accessor, expected to be in pixels\n y = (d) => d[1], // y coordinate accessor, expected to be in pixels\n r = () => 3, // radius accessor, expected to be in pixels\n label = (d, i) => i, // Accessor for the label\n fill = \"#333\", // label fill color\n stroke = \"white\", // label stroke color\n width = null,\n height = null,\n target = null, // Where do you want it to draw\n renderer = \"svg\", // canvas or svg\n font = () => \"10pt sans-serif\",\n hover = true, // Show label of the hovered point\n onHover = (i) => i, // callback when hovered, will pass the index of the selected element\n hoverFont = () => \"bolder 12pt sans-serif\",\n labelsInCentroids = true,\n threshold = 2000, // Areas over this size would get labels\n alwaysShow = (d) => false, // If returns true for the node, it will always show the label\n showLabel = (d, cell) => alwaysShow(d) || -d3.polygonArea(cell) > threshold, // If true, show the label\n\n backgroundFill = \"#fefefe01\", // What to paint the bg rect of the labels. Needed for the onHover\n strokeWidth = 5,\n\n showVoronoi = false,\n voronoiStroke = \"#ccc\",\n\n showAnchors = false,\n anchorsStroke = \"orange\",\n anchorsFill = \"none\",\n\n useOcclusion = true,\n occludedStyle = \"opacity: 0.2\", // css style rules to be used on occluded labels\n\n // For debugging\n showPoints = false,\n pointsFill = \"#ccc\",\n pointsSelectedFill = \"firebrick\",\n pointsStroke = \"#ccc\",\n debug = false,\n selected = null,\n padding = 3, // label padding in pixels\n } = {}\n) {\n if (!data || data?.length === 0) {\n console.log(\"smartLabels: No data to render\");\n return target;\n }\n\n data = data.filter(\n (d, index) =>\n x(d, index) !== undefined &&\n x(d, index) !== null &&\n y(d, index) !== undefined &&\n y(d, index) !== null\n );\n\n if (typeof font === \"string\") font = () => font;\n if (typeof hoverFont === \"string\") hoverFont = () => hoverFont;\n\n let xExtent = d3.extent(data, x),\n yExtent = d3.extent(data, y);\n width = width || xExtent[1] - xExtent[0];\n height = height || yExtent[1] - yExtent[0];\n\n if (debug) console.log(\"✅ smartLabels renderer\", renderer);\n\n function checkIfTargetMatchesRenderer(target, renderer) {\n if (target && target.node) {\n target = target.node();\n }\n if (renderer.toLocaleLowerCase() === \"canvas\") {\n return target instanceof HTMLCanvasElement;\n } else {\n return target instanceof SVGElement;\n }\n }\n\n if (target && !checkIfTargetMatchesRenderer(target, renderer)) {\n if (debug)\n console.log(\n \"❌ smartLabels Target doesn't match the renderer\",\n target,\n renderer\n );\n throw new Error(\n \"Smartlabels Target doesn't match the renderer\",\n target,\n renderer\n );\n }\n\n // Try to reuse the target\n if (renderer.toLocaleLowerCase() === \"canvas\") {\n if (target) {\n target = d3.select(target.node ? target.node() : target);\n } else {\n target = d3.create(\"canvas\").attr(\"width\", width).attr(\"height\", height);\n }\n useOcclusion = false;\n } else {\n if (target) {\n target = d3.select(target.node ? target.node() : target);\n } else {\n target = d3.create(\"svg\").attr(\"viewBox\", [0, 0, width, height]);\n }\n target = target || d3.create(\"svg\").attr(\"viewBox\", [0, 0, width, height]);\n }\n\n const delaunay = d3.Delaunay.from(data, x, y);\n const voronoi = delaunay.voronoi([\n xExtent[0] - 1,\n yExtent[0] - 1,\n xExtent[1] + 1,\n yExtent[1] + 1,\n ]);\n\n let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);\n // Replace null cells with the nearest one\n cells = cells\n .map(([d, cell], index) => [d, getNearestCell(d, cell, index)])\n .map(([d, cell]) => ({ d, cell, show: showLabel(d, cell) }));\n\n // cells can be null when we have duplicated coords\n // https://github.com/d3/d3-delaunay/issues/106\n function getNearestCell(d, cell, index) {\n if (!cell) {\n const i = delaunay.find(x(d, index), y(d, index));\n if (i === -1) {\n console.log(\"couldn't find cell\", i, d, x(d, index), y(d, index));\n return null;\n }\n cell = cells[i][1];\n }\n return cell;\n }\n\n function renderSVG() {\n let anchors = null;\n if (useOcclusion) {\n target\n .selectAll(\"style.smartLabels\")\n .data([0])\n .join(\"style\")\n .attr(\"class\", \"smartLabels\").html(`\n svg g.labels > text.occluded:not(.selected) { ${occludedStyle} }\n `);\n }\n\n const mouseRect = target\n .selectAll(\"rect.smartLabels\")\n .data([0])\n .join(\"rect\")\n .attr(\"class\", \"smartLabels\")\n .attr(\"width\", width)\n .attr(\"height\", height)\n .attr(\"fill\", backgroundFill)\n .attr(\"stroke\", \"none\");\n\n const orient = {\n top: (text) =>\n text\n .attr(\"text-anchor\", \"middle\")\n .attr(\"y\", (d, i) => -(r(d, i) + padding)),\n right: (text) =>\n text\n .attr(\"text-anchor\", \"start\")\n .attr(\"dy\", \"0.35em\")\n .attr(\"x\", (d, i) => r(d, i) + padding),\n bottom: (text) =>\n text\n .attr(\"text-anchor\", \"middle\")\n .attr(\"dy\", \"0.71em\")\n .attr(\"y\", (d, i) => r(d, i) + padding),\n left: (text) =>\n text\n .attr(\"text-anchor\", \"end\")\n .attr(\"dy\", \"0.35em\")\n .attr(\"x\", (d, i