UNPKG

mgraph.forcelayout

Version:
3 lines (2 loc) 19.6 kB
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(n="undefined"!=typeof globalThis?globalThis:n||self).mgraphCreateLayout=e()}(this,(function(){"use strict";function n(n){if(!n)throw new Error("Eventify cannot use a falsy object as events subject");for(const e of["on","off","fire"])if(Object.prototype.hasOwnProperty.call(n,e))throw new Error(`Subject already has property '${e}'`);const e=new Map;return n.on=function(t,o,r){if("function"!=typeof o)throw new Error("Callback is expected to be a function");return e.has(t)||e.set(t,[]),e.get(t).push({callback:o,ctx:r}),n},n.off=function(t,o){if(void 0===t)return e.clear(),n;if(e.has(t))if("function"!=typeof o)e.delete(t);else{const n=e.get(t).filter((n=>n.callback!==o));n.length?e.set(t,n):e.delete(t)}return n},n.fire=function(t,...o){const r=e.get(t);if(r)for(const{callback:n,ctx:e}of r)n.apply(e,o);return n},n}function e(n,t){if(n||(n={}),t)for(const o in t)if(Object.prototype.hasOwnProperty.call(t,o)){const r=Object.prototype.hasOwnProperty.call(n,o),i=typeof t[o];!r||typeof n[o]!==i?n[o]=t[o]:"object"===i&&null!==t[o]&&(n[o]=e(n[o],t[o]))}return n}class t{constructor(n){this.seed=n}nextDouble(){let n=this.seed;return n=n+2127912214+(n<<12)>>>0,n=(3345072700^n^n>>>19)>>>0,n=n+374761393+(n<<5)>>>0,n=(n+3550635116^n<<9)>>>0,n=n+4251993797+(n<<3)>>>0,n=(3042594569^n^n>>>16)>>>0,this.seed=n,(268435455&n)/268435456}next(n){return Math.floor(this.nextDouble()*n)}uniform(){return this.nextDouble()}gaussian(){let n,e,t;do{n=2*this.nextDouble()-1,e=2*this.nextDouble()-1,t=n*n+e*e}while(t>=1||0===t);return n*Math.sqrt(-2*Math.log(t)/t)}levy(){const n=1.5,e=Math.pow(o(2.5)*Math.sin(Math.PI*n/2)/(o(1.25)*n*Math.pow(2,.25)),1/n);return this.gaussian()*e/Math.pow(Math.abs(this.gaussian()),1/n)}}function o(n){return Math.sqrt(2*Math.PI/n)*Math.pow(1/Math.E*(n+1/(12*n-1/(10*n))),n)}function r(n){const e="number"==typeof n?n:Date.now();return new t(e)}const i=Object.assign(r,{random:r,randomIterator:function(n,e){const t=e||r();if("function"!=typeof t.next)throw new Error("customRandom does not match expected API: next() function is missing");return{forEach(e){const o=n;for(let n=o.length-1;n>0;n--){const r=t.next(n+1),i=o[r];o[r]=o[n],o[n]=i,e(i)}o.length&&e(o[0])},shuffle(){const e=n;for(let n=e.length-1;n>0;n--){const o=t.next(n+1);[e[n],e[o]]=[e[o],e[n]]}return e}}}}),s=n=>{if(!Number.isInteger(n)||n<0)throw new Error("Index must be a non‑negative integer");switch(n){case 0:return"x";case 1:return"y";case 2:return"z";default:return`c${n+1}`}},a=n=>{if(!Number.isInteger(n)||n<=0)throw new Error("Dimension must be a positive integer");return(e,{indent:t=0,join:o="\n",escapeNewlines:r=!1}={})=>{const i=" ".repeat(t),a=Array.from({length:n},((n,t)=>(0===t?"":i)+e.replace(/{var}/g,s(t)))).join(o);return r?a.replace(/\n/g,"\\n"):a}},d=n=>new Function("bodies","settings","random",c(n)),c=n=>{const e=a(n);return`\n const boundingBox = {\n ${e("min_{var}: 0, max_{var}: 0,",{indent:4})}\n };\n \n return {\n box: boundingBox,\n update: updateBoundingBox,\n reset: resetBoundingBox,\n getBestNewPosition(neighbors) {\n let ${e("base_{var} = 0",{join:", "})};\n \n if (neighbors.length) {\n for (let i = 0; i < neighbors.length; ++i) {\n const pos = neighbors[i].pos;\n ${e("base_{var} += pos.{var};",{indent:10})}\n }\n ${e("base_{var} /= neighbors.length;",{indent:8})}\n } else {\n ${e("base_{var} = (boundingBox.min_{var} + boundingBox.max_{var}) / 2;",{indent:8})}\n }\n \n const len = settings.springLength;\n return {\n ${e("{var}: base_{var} + (random.nextDouble() - 0.5) * len,",{indent:8})}\n };\n }\n };\n\n function updateBoundingBox() {\n if (!bodies.length) return;\n ${e("let min_{var} = Infinity;",{indent:4})}\n ${e("let max_{var} = -Infinity;",{indent:4})}\n for (let i = 0, l = bodies.length; i < l; ++i) {\n const pos = bodies[i].pos;\n ${e("if (pos.{var} < min_{var}) min_{var} = pos.{var};",{indent:6})}\n ${e("if (pos.{var} > max_{var}) max_{var} = pos.{var};",{indent:6})}\n }\n ${e("boundingBox.min_{var} = min_{var};",{indent:4})}\n ${e("boundingBox.max_{var} = max_{var};",{indent:4})}\n }\n\n function resetBoundingBox() {\n ${e("boundingBox.min_{var} = boundingBox.max_{var} = 0;",{indent:4})}\n }\n `},u=(n,e=!1)=>{const t=f(n,e);return new Function(t)().Body},f=(n,e)=>{const t=a(n);return`${(()=>{const o=e?Array.from({length:n},((n,e)=>{const t=s(e);return`\n let _${t};\n Object.defineProperty(this, '${t}', {\n get: () => _${t},\n set: (val) => { if (!Number.isFinite(val)) throw new Error('Non‑finite ${t}'); _${t} = val; }\n });`})).join(""):"";return`function Vector(${t("{var}",{join:", "})}) {\n ${o}\n if (typeof arguments[0] === 'object') {\n const src = arguments[0];\n ${t("this.{var} = src.{var};",{indent:4})}\n } else {\n ${t("this.{var} = Number.isFinite({var}) ? {var} : 0;",{indent:4})}\n }\n}\nVector.prototype.reset = function () { ${t("this.{var} = 0;",{join:" "})} };`})()}\n${`function Body(${t("{var}",{join:", "})}) {\n this.isPinned = false;\n this.pos = new Vector(${t("{var}",{join:", "})});\n this.force = new Vector();\n this.velocity = new Vector();\n this.mass = 1;\n this.springCount = 0;\n this.springLength = 0;\n}\nBody.prototype.reset = function () {\n this.force.reset();\n this.springCount = 0;\n this.springLength = 0;\n};\nBody.prototype.setPosition = function(${t("{var}",{join:", "})}) {\n ${t("this.pos.{var} = Number.isFinite({var}) ? {var} : 0;",{indent:2})}\n};`}\nreturn { Body, Vector };`},p=n=>new Function("options",h(n)),h=n=>`\n if (!Number.isFinite(options.dragCoefficient))\n throw new Error('dragCoefficient must be finite');\n return {\n update(body) {\n ${a(n)("body.force.{var} -= options.dragCoefficient * body.velocity.{var};",{indent:6})}\n }\n };`,l=n=>new Function("options","random",g(n)),g=n=>{const e=a(n);return`\n if (!Number.isFinite(options.springCoefficient)) throw new Error('springCoefficient must be finite');\n if (!Number.isFinite(options.springLength)) throw new Error('springLength must be finite');\n\n return {\n update(spring) {\n const b1 = spring.from;\n const b2 = spring.to;\n const len = spring.length < 0 ? options.springLength : spring.length;\n ${e("let d{var} = b2.pos.{var} - b1.pos.{var};",{indent:6})}\n let r = Math.hypot(${e("d{var}",{join:", "})});\n if (r === 0) {\n ${e("d{var} = (random.nextDouble() - 0.5) / 50;",{indent:8})}\n r = Math.hypot(${e("d{var}",{join:", "})});\n }\n const delta = r - len;\n const k = (spring.coefficient > 0 ? spring.coefficient : options.springCoefficient) * delta / r;\n ${e("b1.force.{var} += k * d{var};",{indent:6})}\n b1.springCount += 1;\n b1.springLength += r;\n ${e("b2.force.{var} -= k * d{var};",{indent:6})}\n b2.springCount += 1;\n b2.springLength += r;\n }\n };`},v=n=>new Function("bodies","timeStep","adaptiveTimeStepWeight",m(n)),m=n=>{const e=a(n);return`\n const n = bodies.length;\n if (!n) return 0;\n ${e("let d{var} = 0, t{var} = 0;",{indent:2})}\n for (let i = 0; i < n; ++i) {\n const body = bodies[i];\n if (body.isPinned) continue;\n let dt = timeStep;\n if (adaptiveTimeStepWeight && body.springCount)\n dt = adaptiveTimeStepWeight * body.springLength / body.springCount;\n const coeff = dt / body.mass;\n ${e("body.velocity.{var} += coeff * body.force.{var};",{indent:4})}\n ${e("const v{var} = body.velocity.{var};",{indent:4})}\n const v = Math.hypot(${e("v{var}",{join:", "})});\n if (v > 1) {\n const inv = 1 / v;\n ${e("body.velocity.{var} *= inv;",{indent:6})}\n }\n ${e("d{var} = dt * body.velocity.{var};",{indent:4})}\n ${e("body.pos.{var} += d{var};",{indent:4})}\n ${e("t{var} += Math.abs(d{var});",{indent:4})}\n }\n return (${e("t{var} * t{var}",{join:" + "})}) / n;`},y=n=>{const e=2**n,t=a(n),o=(n,e)=>Array.from({length:e},((e,t)=>`${n}quad${t} = null;`)).join("\n");return`\n\nfunction InsertStack() { this.stack = []; this.popIdx = 0; }\nInsertStack.prototype = {\n isEmpty() { return this.popIdx === 0; },\n push(node, body) {\n const item = this.stack[this.popIdx] || (this.stack[this.popIdx] = {});\n item.node = node; item.body = body; this.popIdx++; },\n pop() { return this.popIdx ? this.stack[--this.popIdx] : undefined; },\n reset() { this.popIdx = 0; }\n};\n\nfunction QuadNode() {\n this.body = null;\n${o(" this.",e)}\n this.mass = 0;\n ${t("this.mass_{var} = 0;",{indent:2})}\n ${t("this.min_{var} = 0; this.max_{var} = 0;",{indent:2,join:"\n "})}\n}\n\nfunction isSamePosition(p1, p2) {\n return ${t("Math.abs(p1.{var} - p2.{var}) < 1e-8",{join:" && "})};\n}\n${function(){const n=`function getChild(node, idx) {\n${Array.from({length:e},((n,e)=>` if (idx === ${e}) return node.quad${e};`)).join("\n")}\n return null; }`,t=`function setChild(node, idx, child) {\n${Array.from({length:e},((n,e)=>`${e?" else ":" "}if (idx === ${e}) node.quad${e} = child;`)).join("\n")} }`;return`${n}\n${t}`}()}\n\nfunction createQuadTree(options = {}, random) {\n let gravity = typeof options.gravity === 'number' ? options.gravity : -1;\n let theta = typeof options.theta === 'number' ? options.theta : 0.8;\n const updateQueue = [];\n const insertStack = new InsertStack();\n const nodesCache = [];\n let currentInCache = 0;\n let root = newNode();\n\n return { insertBodies, getRoot: () => root, updateBodyForce: update, options: opts };\n\n function opts(newOpts) {\n if (newOpts) {\n if (typeof newOpts.gravity === 'number') gravity = newOpts.gravity;\n if (typeof newOpts.theta === 'number') theta = newOpts.theta;\n return this;\n }\n return { gravity, theta };\n }\n\n function newNode() {\n let node = nodesCache[currentInCache];\n if (node) {\n${o(" node.",e)}\n node.body = null;\n node.mass = ${t("node.mass_{var} = ",{join:""})}0;\n ${t("node.min_{var} = node.max_{var} = 0;",{indent:6})}\n } else {\n node = new QuadNode();\n nodesCache[currentInCache] = node;\n }\n currentInCache++;\n return node;\n }\n\n \nfunction update(sourceBody) {\n const queue = updateQueue;\n let queueLength = 1, shiftIdx = 0, pushIdx = 1;\n queue[0] = root;\n ${t("let f{var} = 0;",{indent:2})}\n while (queueLength) {\n const node = queue[shiftIdx++];\n queueLength--;\n const body = node.body;\n const different = body && body !== sourceBody;\n ${t("let d{var};",{indent:4})}\n let r, v;\n if (different) {\n ${t("d{var} = body.pos.{var} - sourceBody.pos.{var};",{indent:6})}\n r = Math.hypot(${t("d{var}",{join:", "})});\n if (r === 0) { ${t("d{var} = (Math.random() - 0.5) / 50;",{indent:8})} r = Math.hypot(${t("d{var}",{join:", "})}); }\n v = gravity * body.mass * sourceBody.mass / (r ** 3);\n ${t("f{var} += v * d{var};",{indent:6})}\n } else if (node.body !== sourceBody) {\n ${t("d{var} = node.mass_{var} / node.mass - sourceBody.pos.{var};",{indent:6})}\n r = Math.hypot(${t("d{var}",{join:", "})});\n if (r === 0) { ${t("d{var} = (Math.random() - 0.5) / 50;",{indent:8})} r = Math.hypot(${t("d{var}",{join:", "})}); }\n if ((node.max_${s(0)} - node.min_${s(0)}) / r < theta) {\n v = gravity * node.mass * sourceBody.mass / (r ** 3);\n ${t("f{var} += v * d{var};",{indent:8})}\n } else {\n${(()=>{const n=" ".repeat(11);return Array.from({length:e},((e,t)=>`${n}if (node.quad${t}) {\n${n} queue[pushIdx++] = node.quad${t};\n${n} queueLength++;\n${n}}`)).join("\n")})()}\n }\n }\n }\n ${t("sourceBody.force.{var} += f{var};",{indent:2})}\n}\n \nfunction insertBodies(bodies) {\n ${t("let {var}min = Infinity, {var}max = -Infinity;",{indent:2})}\n for (let i = 0; i < bodies.length; ++i) {\n const pos = bodies[i].pos;\n ${t("if (pos.{var} < {var}min) {var}min = pos.{var};",{indent:4})}\n ${t("if (pos.{var} > {var}max) {var}max = pos.{var};",{indent:4})}\n }\n let maxSideLength = 0;\n ${t("if ({var}max - {var}min > maxSideLength) maxSideLength = {var}max - {var}min;",{indent:2})}\n currentInCache = 0;\n root = newNode();\n ${t("root.min_{var} = {var}min;",{indent:2})}\n ${t("root.max_{var} = {var}min + maxSideLength;",{indent:2})}\n for (let i = bodies.length - 1; i >= 0; --i) insert(bodies[i], root);\n}\n\nfunction insert(newBody, startNode) {\n insertStack.reset(); insertStack.push(startNode, newBody);\n while (!insertStack.isEmpty()) {\n const { node, body } = insertStack.pop();\n if (!node.body) {\n ${t("const {var} = body.pos.{var};",{indent:6})}\n node.mass += body.mass;\n ${t("node.mass_{var} += body.mass * {var};",{indent:6})}\n let quadIdx = 0;\n ${t("let min_{var} = node.min_{var};",{indent:6})}\n ${t("let max_{var} = (min_{var} + node.max_{var}) / 2;",{indent:6})}\n${(e=>{const t=" ".repeat(e);return Array.from({length:n},((n,e)=>{const o=s(e);return`${t}if (${o} > max_${o}) {\n${t} quadIdx += ${2**e};\n${t} min_${o} = max_${o};\n${t} max_${o} = node.max_${o};\n${t}}`})).join("\n")})(6)}\n let child = getChild(node, quadIdx);\n if (!child) {\n child = newNode();\n ${t("child.min_{var} = min_{var};",{indent:8})}\n ${t("child.max_{var} = max_{var};",{indent:8})}\n child.body = body;\n setChild(node, quadIdx, child);\n } else {\n insertStack.push(child, body);\n }\n } else {\n const oldBody = node.body;\n node.body = null;\n if (isSamePosition(oldBody.pos, body.pos)) {\n for (let retries = 3; retries-- && isSamePosition(oldBody.pos, body.pos);) {\n const off = Math.random();\n ${t("const d{var} = (node.max_{var} - node.min_{var}) * off;",{indent:10})}\n ${t("oldBody.pos.{var} = node.min_{var} + d{var};",{indent:10})}\n }\n }\n insertStack.push(node, oldBody);\n insertStack.push(node, body);\n }\n }\n}\n}\n\nreturn createQuadTree;`},b={};function $(t){if(t){if(void 0!==t.springCoeff)throw new Error("springCoeff was renamed to springCoefficient");if(void 0!==t.dragCoeff)throw new Error("dragCoeff was renamed to dragCoefficient")}t=e(t,{springLength:10,springCoefficient:.8,gravity:-12,theta:.8,dragCoefficient:.9,timeStep:.5,adaptiveTimeStepWeight:0,dimensions:2,debug:!1});let o=b[t.dimensions];if(!o){const n=t.dimensions;o={Body:u(n,t.debug),createQuadTree:(r=n,new Function(y(r))),createBounds:d(n),createDragForce:p(n),createSpringForce:l(n),integrate:v(n)},b[n]=o}var r;const s=o.Body,a=o.createQuadTree,c=o.createBounds,f=o.createDragForce,h=o.createSpringForce,g=o.integrate,m=i.random(42),$=[],B=[],_=a()(t,m),I=c($,t,m),j=h(t,m),N=f(t),C=[],S=new Map;let M=0;P("nbody",(function(){if(0===$.length)return;_.insertBodies($);let n=$.length;for(;n--;){const e=$[n];e.isPinned||(e.reset(),_.updateBodyForce(e),N.update(e))}})),P("spring",(function(){let n=B.length;for(;n--;)j.update(B[n])}));const k={bodies:$,quadTree:_,springs:B,settings:t,addForce:P,removeForce:function(n){const e=C.indexOf(S.get(n));if(e<0)return;C.splice(e,1),S.delete(n)},getForces:function(){return S},step(){for(let n=0;n<C.length;++n)C[n](M);const n=g($,t.timeStep,t.adaptiveTimeStepWeight);return M+=1,n},addBody(n){if(!n)throw new Error("Body is required");return $.push(n),n},addBodyAt(n){if(!n)throw new Error("Body position is required");const e=function(n){return new s(n)}(n);return $.push(e),e},removeBody(n){if(!n)return;const e=$.indexOf(n);return e<0?void 0:($.splice(e,1),0===$.length&&I.reset(),!0)},addSpring(n,e,t,o){if(!n||!e)throw new Error("Cannot add null spring to force simulator");"number"!=typeof t&&(t=-1);const r=new w(n,e,t,o>=0?o:-1);return B.push(r),r},getTotalMovement:()=>g.totalMovement||0,removeSpring(n){if(!n)return;const e=B.indexOf(n);return e>-1?(B.splice(e,1),!0):void 0},getBestNewBodyPosition:n=>I.getBestNewPosition(n),getBBox:E,getBoundingBox:E,invalidateBBox(){console.warn("invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call")},gravity(n){return void 0!==n?(t.gravity=n,_.options({gravity:n}),this):t.gravity},theta(n){return void 0!==n?(t.theta=n,_.options({theta:n}),this):t.theta},random:m};return function(n,e){for(const t in n)x(n,e,t)}(t,k),n(k),k;function E(){return I.update(),I.box}function P(n,e){if(S.has(n))throw new Error("Force "+n+" is already added");S.set(n,e),C.push(e)}}class w{constructor(n,e,t,o){this.from=n,this.to=e,this.length=t,this.coefficient=o}}function x(n,e,t){if(!n.hasOwnProperty(t))return;if("function"==typeof e[t])return;const o=Number.isFinite(n[t]);e[t]=o?function(o){if(void 0!==o){if(!Number.isFinite(o))throw new Error("Value of "+t+" should be a valid number.");return n[t]=o,e}return n[t]}:function(o){return void 0!==o?(n[t]=o,e):n[t]}}const B=()=>{};function _(e,t={}){if(!e)throw new Error("Graph structure cannot be undefined");if(Array.isArray(t))throw new Error("Physics settings is expected to be an object");const o=(t.createSimulator||$)(t),r=new Map,i={};let s=0;const a=o.settings.springTransform||B;s=0,e.forEachNode((n=>{h(n.id),s++})),e.forEachLink(l),e.on("changed",p);let d=!1;const c=n=>{var e;d!==n&&(d=n,e=n,f.fire("stable",e))};function u(n){r.forEach(n)}const f={step(){if(0===s)return c(!0),!0;const n=o.step();f.lastMove=n,f.fire("step");const e=n/s<=.01;return c(e),e},getNodePosition:n=>v(n).pos,setNodePosition(n,...e){v(n).setPosition(...e)},getLinkPosition(n){const e=i[n];if(e)return{from:e.from.pos,to:e.to.pos}},getGraphRect:()=>o.getBBox(),forEachBody:u,pinNode(n,e){v(n.id).isPinned=Boolean(e)},isNodePinned:n=>v(n.id).isPinned,dispose(){e.off("changed",p),f.fire("disposed")},getBody:n=>r.get(n),getSpring:function(n,t){let o;if(void 0===t)o="object"!=typeof n?n:n.id;else{const r=e.hasLink(n,t);if(!r)return;o=r.id}return i[o]},getForceVectorLength:function(){let n=0,e=0;return u((t=>{n+=Math.abs(t.force.x),e+=Math.abs(t.force.y)})),Math.hypot(n,e)},simulator:o,graph:e,lastMove:0};function p(n){n.forEach((n=>{"add"===n.changeType?(n.node&&h(n.node.id),n.link&&l(n.link)):"remove"===n.changeType&&(n.node&&function(n){const e=n.id,t=r.get(e);t&&(r.delete(e),o.removeBody(t))}(n.node),n.link&&function(n){const t=i[n.id];if(t){const r=e.getNode(n.fromId),s=e.getNode(n.toId);r&&g(r.id),s&&g(s.id),delete i[n.id],o.removeSpring(t)}}(n.link))})),s=e.getNodesCount()}function h(n){if(r.has(n))return;const t=e.getNode(n);if(!t)throw new Error(`initBody() was called with unknown node id: ${n}`);let i=t.position;if(!i){const n=function(n){const t=e.getLinks(n.id);if(!t)return[];return Array.from(t).reduce(((e,t)=>{const o=t.fromId!==n.id?r.get(t.fromId):r.get(t.toId);return o&&o.pos&&e.push(o),e}),[])}(t);i=o.getBestNewBodyPosition(n)}const s=o.addBodyAt(i);s.id=n,r.set(n,s),g(n),function(n){return n?.isPinned||n?.data&&n.data.isPinned}(t)&&(s.isPinned=!0)}function l(n){g(n.fromId),g(n.toId);const e=r.get(n.fromId),t=r.get(n.toId);if(!e||!t)return;const s=o.addSpring(e,t,n.length);a(n,s),i[n.id]=s}function g(n){const t=r.get(n);if(!t)return;const o=e.getLinks(n),i=1+(o?Array.from(o).length:0)/3;if(Number.isNaN(i))throw new Error("Node mass should be a number");t.mass=i}function v(n){return r.has(n)||h(n),r.get(n)}return n(f),f}return _.simulator=$,_})); //# sourceMappingURL=mgraph.forcelayout.umd.min.js.map