polly-graph 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +3160 -20
- package/dist/index.css +196 -0
- package/dist/index.d.cts +970 -16
- package/dist/index.d.ts +970 -16
- package/dist/index.js +3153 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9120,28 +9120,32 @@ function shouldRenderControl(config, key) {
|
|
|
9120
9120
|
return value;
|
|
9121
9121
|
}
|
|
9122
9122
|
|
|
9123
|
-
// src/shared/icons/fit.svg?raw
|
|
9124
|
-
var fit_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 9V5H9" />\n <path d="M19 9V5H15" />\n <path d="M5 15V19H9" />\n <path d="M19 15V19H15" />\n</svg>';
|
|
9125
|
-
|
|
9126
|
-
// src/shared/icons/reset.svg?raw
|
|
9127
|
-
var reset_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M20 12a8 8 0 1 1-2.3-5.7" />\n <path d="M20 4.5v4h-4" />\n</svg>';
|
|
9128
|
-
|
|
9129
|
-
// src/shared/icons/plus.svg?raw
|
|
9130
|
-
var plus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M5 12h14m-7-7v14" />\n</svg>';
|
|
9131
|
-
|
|
9132
|
-
// src/shared/icons/minus.svg?raw
|
|
9133
|
-
var minus_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M19 12H5" />\n</svg>';
|
|
9134
|
-
|
|
9135
|
-
// src/shared/icons/caret.svg?raw
|
|
9136
|
-
var caret_default = '<svg\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M9 20L16.5 12L9 4" />\n</svg>';
|
|
9137
|
-
|
|
9138
9123
|
// src/shared/icons/index.ts
|
|
9124
|
+
var fitIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9125
|
+
<path d="M5 9V5H9" />
|
|
9126
|
+
<path d="M19 9V5H15" />
|
|
9127
|
+
<path d="M5 15V19H9" />
|
|
9128
|
+
<path d="M19 15V19H15" />
|
|
9129
|
+
</svg>`;
|
|
9130
|
+
var resetIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9131
|
+
<path d="M20 12a8 8 0 1 1-2.3-5.7" />
|
|
9132
|
+
<path d="M20 4.5v4h-4" />
|
|
9133
|
+
</svg>`;
|
|
9134
|
+
var plusIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9135
|
+
<path d="M5 12h14m-7-7v14" />
|
|
9136
|
+
</svg>`;
|
|
9137
|
+
var minusIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9138
|
+
<path d="M5 12h14" />
|
|
9139
|
+
</svg>`;
|
|
9140
|
+
var caretIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9141
|
+
<path d="M6 9l6 6 6-6" />
|
|
9142
|
+
</svg>`;
|
|
9139
9143
|
var icons = {
|
|
9140
|
-
fit:
|
|
9141
|
-
reset:
|
|
9142
|
-
plus:
|
|
9143
|
-
minus:
|
|
9144
|
-
caret:
|
|
9144
|
+
fit: fitIconSvg,
|
|
9145
|
+
reset: resetIconSvg,
|
|
9146
|
+
plus: plusIconSvg,
|
|
9147
|
+
minus: minusIconSvg,
|
|
9148
|
+
caret: caretIconSvg
|
|
9145
9149
|
};
|
|
9146
9150
|
var iconSvg = {
|
|
9147
9151
|
fit: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -10069,16 +10073,3143 @@ function createV2Graph(config) {
|
|
|
10069
10073
|
graph.initialize(config);
|
|
10070
10074
|
return graph;
|
|
10071
10075
|
}
|
|
10076
|
+
|
|
10077
|
+
// src/force-graph-wrapper/core/force-graph-wrapper.ts
|
|
10078
|
+
import ForceGraph from "force-graph";
|
|
10079
|
+
|
|
10080
|
+
// src/force-graph-wrapper/ui/graph-controls.ts
|
|
10081
|
+
function createGraphControls(container, actions, config) {
|
|
10082
|
+
let root2 = null;
|
|
10083
|
+
function mount() {
|
|
10084
|
+
if (!config.enabled) return;
|
|
10085
|
+
root2 = document.createElement("div");
|
|
10086
|
+
root2.className = "fg-controls";
|
|
10087
|
+
const position = config.position || "bottom-left";
|
|
10088
|
+
root2.classList.add(`fg-pos-${position}`);
|
|
10089
|
+
const orientation = config.orientation || "vertical";
|
|
10090
|
+
root2.classList.add(`fg-orient-${orientation}`);
|
|
10091
|
+
if (config.offset) {
|
|
10092
|
+
root2.style.setProperty("--fg-controls-offset-x", `${config.offset.x}px`);
|
|
10093
|
+
root2.style.setProperty("--fg-controls-offset-y", `${config.offset.y}px`);
|
|
10094
|
+
}
|
|
10095
|
+
appendControls2(root2, config, actions);
|
|
10096
|
+
container.appendChild(root2);
|
|
10097
|
+
}
|
|
10098
|
+
function destroy() {
|
|
10099
|
+
if (root2 && root2.parentNode) {
|
|
10100
|
+
root2.parentNode.removeChild(root2);
|
|
10101
|
+
root2 = null;
|
|
10102
|
+
}
|
|
10103
|
+
}
|
|
10104
|
+
return { mount, destroy };
|
|
10105
|
+
}
|
|
10106
|
+
function appendControls2(root2, config, actions) {
|
|
10107
|
+
const controls = [
|
|
10108
|
+
{
|
|
10109
|
+
key: "zoomIn",
|
|
10110
|
+
icon: getControlIcon("zoom-in"),
|
|
10111
|
+
label: "Zoom In",
|
|
10112
|
+
action: () => actions.zoomIn()
|
|
10113
|
+
},
|
|
10114
|
+
{
|
|
10115
|
+
key: "zoomOut",
|
|
10116
|
+
icon: getControlIcon("zoom-out"),
|
|
10117
|
+
label: "Zoom Out",
|
|
10118
|
+
action: () => actions.zoomOut()
|
|
10119
|
+
},
|
|
10120
|
+
{
|
|
10121
|
+
key: "fit",
|
|
10122
|
+
icon: getControlIcon("fit"),
|
|
10123
|
+
label: "Fit View",
|
|
10124
|
+
action: () => actions.fitView()
|
|
10125
|
+
},
|
|
10126
|
+
{
|
|
10127
|
+
key: "reset",
|
|
10128
|
+
icon: getControlIcon("reset"),
|
|
10129
|
+
label: "Reset View",
|
|
10130
|
+
action: () => actions.resetView()
|
|
10131
|
+
}
|
|
10132
|
+
];
|
|
10133
|
+
controls.forEach((control) => {
|
|
10134
|
+
if (shouldShowControl(config, control.key)) {
|
|
10135
|
+
const button = createControlButton2(control.icon, control.label, control.action);
|
|
10136
|
+
root2.appendChild(button);
|
|
10137
|
+
}
|
|
10138
|
+
});
|
|
10139
|
+
}
|
|
10140
|
+
function shouldShowControl(config, key) {
|
|
10141
|
+
return config.show?.[key] !== false;
|
|
10142
|
+
}
|
|
10143
|
+
function createControlButton2(icon, label, onClick) {
|
|
10144
|
+
const button = document.createElement("button");
|
|
10145
|
+
button.className = "fg-control-btn";
|
|
10146
|
+
button.type = "button";
|
|
10147
|
+
button.innerHTML = icon;
|
|
10148
|
+
button.setAttribute("aria-label", label);
|
|
10149
|
+
button.setAttribute("title", label);
|
|
10150
|
+
button.addEventListener("click", (e) => {
|
|
10151
|
+
e.preventDefault();
|
|
10152
|
+
e.stopPropagation();
|
|
10153
|
+
onClick();
|
|
10154
|
+
});
|
|
10155
|
+
return button;
|
|
10156
|
+
}
|
|
10157
|
+
|
|
10158
|
+
// src/force-graph-wrapper/ui/graph-legends.ts
|
|
10159
|
+
function createGraphLegends(container, config) {
|
|
10160
|
+
let root2 = null;
|
|
10161
|
+
function mount() {
|
|
10162
|
+
if (!config.enabled) return;
|
|
10163
|
+
root2 = document.createElement("div");
|
|
10164
|
+
root2.className = "fg-legends";
|
|
10165
|
+
const position = config.position || "top-right";
|
|
10166
|
+
root2.classList.add(`fg-pos-${position}`);
|
|
10167
|
+
if (config.offset) {
|
|
10168
|
+
root2.style.setProperty("--fg-legends-offset-x", `${config.offset.x}px`);
|
|
10169
|
+
root2.style.setProperty("--fg-legends-offset-y", `${config.offset.y}px`);
|
|
10170
|
+
}
|
|
10171
|
+
container.appendChild(root2);
|
|
10172
|
+
}
|
|
10173
|
+
function destroy() {
|
|
10174
|
+
if (root2 && root2.parentNode) {
|
|
10175
|
+
root2.parentNode.removeChild(root2);
|
|
10176
|
+
root2 = null;
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
function update(nodeTypes, colorMap) {
|
|
10180
|
+
if (!root2 || !config.enabled) return;
|
|
10181
|
+
root2.innerHTML = "";
|
|
10182
|
+
const validTypes = nodeTypes.filter((type) => type && type.trim()).sort((a2, b) => a2.localeCompare(b));
|
|
10183
|
+
if (validTypes.length === 0) return;
|
|
10184
|
+
const displayTypes = config.maxItems ? validTypes.slice(0, config.maxItems) : validTypes;
|
|
10185
|
+
if (config.showTitle) {
|
|
10186
|
+
const titleElement = document.createElement("div");
|
|
10187
|
+
titleElement.className = "fg-legend-title";
|
|
10188
|
+
titleElement.textContent = config.title || "Legend";
|
|
10189
|
+
root2.appendChild(titleElement);
|
|
10190
|
+
}
|
|
10191
|
+
displayTypes.forEach((type) => {
|
|
10192
|
+
const itemElement = createLegendItem(type, colorMap[type] || "#ccc");
|
|
10193
|
+
root2.appendChild(itemElement);
|
|
10194
|
+
});
|
|
10195
|
+
if (config.maxItems && validTypes.length > config.maxItems) {
|
|
10196
|
+
const moreElement = document.createElement("div");
|
|
10197
|
+
moreElement.className = "fg-legend-item fg-legend-more";
|
|
10198
|
+
moreElement.innerHTML = `
|
|
10199
|
+
<div class="fg-legend-dot" style="background-color: #999;"></div>
|
|
10200
|
+
<span class="fg-legend-label">and ${validTypes.length - config.maxItems} more...</span>
|
|
10201
|
+
`;
|
|
10202
|
+
root2.appendChild(moreElement);
|
|
10203
|
+
}
|
|
10204
|
+
}
|
|
10205
|
+
function createLegendItem(type, color2) {
|
|
10206
|
+
const itemElement = document.createElement("div");
|
|
10207
|
+
itemElement.className = "fg-legend-item";
|
|
10208
|
+
const dotElement = document.createElement("div");
|
|
10209
|
+
dotElement.className = "fg-legend-dot";
|
|
10210
|
+
dotElement.style.backgroundColor = color2;
|
|
10211
|
+
const labelElement = document.createElement("span");
|
|
10212
|
+
labelElement.className = "fg-legend-label";
|
|
10213
|
+
labelElement.textContent = type;
|
|
10214
|
+
itemElement.appendChild(dotElement);
|
|
10215
|
+
itemElement.appendChild(labelElement);
|
|
10216
|
+
return itemElement;
|
|
10217
|
+
}
|
|
10218
|
+
return { mount, destroy, update };
|
|
10219
|
+
}
|
|
10220
|
+
|
|
10221
|
+
// src/force-graph-wrapper/workers/physics-worker-manager.ts
|
|
10222
|
+
var PhysicsWorkerManagerImpl = class {
|
|
10223
|
+
worker = null;
|
|
10224
|
+
workerUrl = null;
|
|
10225
|
+
messageId = 0;
|
|
10226
|
+
pendingPromises = /* @__PURE__ */ new Map();
|
|
10227
|
+
promiseTimeouts = /* @__PURE__ */ new Map();
|
|
10228
|
+
constructor() {
|
|
10229
|
+
this.initializeWorker();
|
|
10230
|
+
}
|
|
10231
|
+
/**
|
|
10232
|
+
* Initialize the web worker
|
|
10233
|
+
*/
|
|
10234
|
+
initializeWorker() {
|
|
10235
|
+
try {
|
|
10236
|
+
const workerScript = this.getWorkerScript();
|
|
10237
|
+
const blob = new Blob([workerScript], { type: "application/javascript" });
|
|
10238
|
+
this.workerUrl = URL.createObjectURL(blob);
|
|
10239
|
+
this.worker = new Worker(this.workerUrl);
|
|
10240
|
+
this.setupWorkerEventListeners();
|
|
10241
|
+
} catch {
|
|
10242
|
+
this.worker = null;
|
|
10243
|
+
}
|
|
10244
|
+
}
|
|
10245
|
+
/**
|
|
10246
|
+
* Get the worker script content
|
|
10247
|
+
*/
|
|
10248
|
+
getWorkerScript() {
|
|
10249
|
+
return `
|
|
10250
|
+
/**
|
|
10251
|
+
* Physics Web Worker for Force Graph
|
|
10252
|
+
* Runs D3 force simulation in a dedicated worker thread for consistent timing
|
|
10253
|
+
*/
|
|
10254
|
+
|
|
10255
|
+
// D3 modules embedded locally to avoid CDN dependency
|
|
10256
|
+
// D3 Dispatch v3.0.1
|
|
10257
|
+
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).d3=n.d3||{})}(this,(function(n){"use strict";var e={value:()=>{}};function t(){for(var n,e=0,t=arguments.length,o={};e<t;++e){if(!(n=arguments[e]+"")||n in o||/[\\s.]/.test(n))throw new Error("illegal type: "+n);o[n]=[]}return new r(o)}function r(n){this._=n}function o(n,e){return n.trim().split(/^|\\s+/).map((function(n){var t="",r=n.indexOf(".");if(r>=0&&(t=n.slice(r+1),n=n.slice(0,r)),n&&!e.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:t}}))}function i(n,e){for(var t,r=0,o=n.length;r<o;++r)if((t=n[r]).name===e)return t.value}function f(n,t,r){for(var o=0,i=n.length;o<i;++o)if(n[o].name===t){n[o]=e,n=n.slice(0,o).concat(n.slice(o+1));break}return null!=r&&n.push({name:t,value:r}),n}r.prototype=t.prototype={constructor:r,on:function(n,e){var t,r=this._,l=o(n+"",r),a=-1,u=l.length;if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++a<u;)if(t=(n=l[a]).type)r[t]=f(r[t],n.name,e);else if(null==e)for(t in r)r[t]=f(r[t],n.name,null);return this}for(;++a<u;)if((t=(n=l[a]).type)&&(t=i(r[t],n.name)))return t},copy:function(){var n={},e=this._;for(var t in e)n[t]=e[t].slice();return new r(n)},call:function(n,e){if((t=arguments.length-2)>0)for(var t,r,o=new Array(t),i=0;i<t;++i)o[i]=arguments[i+2];if(!this._.hasOwnProperty(n))throw new Error("unknown type: "+n);for(i=0,t=(r=this._[n]).length;i<t;++i)r[i].value.apply(e,o)},apply:function(n,e,t){if(!this._.hasOwnProperty(n))throw new Error("unknown type: "+n);for(var r=this._[n],o=0,i=r.length;o<i;++o)r[o].value.apply(e,t)}},n.dispatch=t,Object.defineProperty(n,"__esModule",{value:!0})}))
|
|
10258
|
+
|
|
10259
|
+
// D3 Quadtree v3.0.1
|
|
10260
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function i(t,i,e,n){if(isNaN(i)||isNaN(e))return t;var r,s,h,o,a,u,l,_,f,c=t._root,x={data:n},y=t._x0,d=t._y0,p=t._x1,v=t._y1;if(!c)return t._root=x,t;for(;c.length;)if((u=i>=(s=(y+p)/2))?y=s:p=s,(l=e>=(h=(d+v)/2))?d=h:v=h,r=c,!(c=c[_=l<<1|u]))return r[_]=x,t;if(o=+t._x.call(null,c.data),a=+t._y.call(null,c.data),i===o&&e===a)return x.next=c,r?r[_]=x:t._root=x,t;do{r=r?r[_]=new Array(4):t._root=new Array(4),(u=i>=(s=(y+p)/2))?y=s:p=s,(l=e>=(h=(d+v)/2))?d=h:v=h}while((_=l<<1|u)==(f=(a>=h)<<1|o>=s));return r[f]=c,r[_]=x,t}function e(t,i,e,n,r){this.node=t,this.x0=i,this.y0=e,this.x1=n,this.y1=r}function n(t){return t[0]}function r(t){return t[1]}function s(t,i,e){var s=new h(null==i?n:i,null==e?r:e,NaN,NaN,NaN,NaN);return null==t?s:s.addAll(t)}function h(t,i,e,n,r,s){this._x=t,this._y=i,this._x0=e,this._y0=n,this._x1=r,this._y1=s,this._root=void 0}function o(t){for(var i={data:t.data},e=i;t=t.next;)e=e.next={data:t.data};return i}var a=s.prototype=h.prototype;a.copy=function(){var t,i,e=new h(this._x,this._y,this._x0,this._y0,this._x1,this._y1),n=this._root;if(!n)return e;if(!n.length)return e._root=o(n),e;for(t=[{source:n,target:e._root=new Array(4)}];n=t.pop();)for(var r=0;r<4;++r)(i=n.source[r])&&(i.length?t.push({source:i,target:n.target[r]=new Array(4)}):n.target[r]=o(i));return e},a.add=function(t){const e=+this._x.call(null,t),n=+this._y.call(null,t);return i(this.cover(e,n),e,n,t)},a.addAll=function(t){var e,n,r,s,h=t.length,o=new Array(h),a=new Array(h),u=1/0,l=1/0,_=-1/0,f=-1/0;for(n=0;n<h;++n)isNaN(r=+this._x.call(null,e=t[n]))||isNaN(s=+this._y.call(null,e))||(o[n]=r,a[n]=s,r<u&&(u=r),r>_&&(_=r),s<l&&(l=s),s>f&&(f=s));if(u>_||l>f)return this;for(this.cover(u,l).cover(_,f),n=0;n<h;++n)i(this,o[n],a[n],t[n]);return this},a.cover=function(t,i){if(isNaN(t=+t)||isNaN(i=+i))return this;var e=this._x0,n=this._y0,r=this._x1,s=this._y1;if(isNaN(e))r=(e=Math.floor(t))+1,s=(n=Math.floor(i))+1;else{for(var h,o,a=r-e||1,u=this._root;e>t||t>=r||n>i||i>=s;)switch(o=(i<n)<<1|t<e,(h=new Array(4))[o]=u,u=h,a*=2,o){case 0:r=e+a,s=n+a;break;case 1:e=r-a,s=n+a;break;case 2:r=e+a,n=s-a;break;case 3:e=r-a,n=s-a}this._root&&this._root.length&&(this._root=u)}return this._x0=e,this._y0=n,this._x1=r,this._y1=s,this},a.data=function(){var t=[];return this.visit((function(i){if(!i.length)do{t.push(i.data)}while(i=i.next)})),t},a.extent=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},a.find=function(t,i,n){var r,s,h,o,a,u,l,_=this._x0,f=this._y0,c=this._x1,x=this._y1,y=[],d=this._root;for(d&&y.push(new e(d,_,f,c,x)),null==n?n=1/0:(_=t-n,f=i-n,c=t+n,x=i+n,n*=n);u=y.pop();)if(!(!(d=u.node)||(s=u.x0)>c||(h=u.y0)>x||(o=u.x1)<_||(a=u.y1)<f))if(d.length){var p=(s+o)/2,v=(h+a)/2;y.push(new e(d[3],p,v,o,a),new e(d[2],s,v,p,a),new e(d[1],p,h,o,v),new e(d[0],s,h,p,v)),(l=(i>=v)<<1|t>=p)&&(u=y[y.length-1],y[y.length-1]=y[y.length-1-l],y[y.length-1-l]=u)}else{var w=t-+this._x.call(null,d.data),N=i-+this._y.call(null,d.data),g=w*w+N*N;if(g<n){var A=Math.sqrt(n=g);_=t-A,f=i-A,c=t+A,x=i+A,r=d.data}}return r},a.remove=function(t){if(isNaN(s=+this._x.call(null,t))||isNaN(h=+this._y.call(null,t)))return this;var i,e,n,r,s,h,o,a,u,l,_,f,c=this._root,x=this._x0,y=this._y0,d=this._x1,p=this._y1;if(!c)return this;if(c.length)for(;;){if((u=s>=(o=(x+d)/2))?x=o:d=o,(l=h>=(a=(y+p)/2))?y=a:p=a,i=c,!(c=c[_=l<<1|u]))return this;if(!c.length)break;(i[_+1&3]||i[_+2&3]||i[_+3&3])&&(e=i,f=_)}for(;c.data!==t;)if(n=c,!(c=c.next))return this;return(r=c.next)&&delete c.next,n?(r?n.next=r:delete n.next,this):i?(r?i[_]=r:delete i[_],(c=i[0]||i[1]||i[2]||i[3])&&c===(i[3]||i[2]||i[1]||i[0])&&!c.length&&(e?e[f]=c:this._root=c),this):(this._root=r,this)},a.removeAll=function(t){for(var i=0,e=t.length;i<e;++i)this.remove(t[i]);return this},a.root=function(){return this._root},a.size=function(){var t=0;return this.visit((function(i){if(!i.length)do{++t}while(i=i.next)})),t},a.visit=function(t){var i,n,r,s,h,o,a=[],u=this._root;for(u&&a.push(new e(u,this._x0,this._y0,this._x1,this._y1));i=a.pop();)if(!t(u=i.node,r=i.x0,s=i.y0,h=i.x1,o=i.y1)&&u.length){var l=(r+h)/2,_=(s+o)/2;(n=u[3])&&a.push(new e(n,l,_,h,o)),(n=u[2])&&a.push(new e(n,r,_,l,o)),(n=u[1])&&a.push(new e(n,l,s,h,_)),(n=u[0])&&a.push(new e(n,r,s,l,_))}return this},a.visitAfter=function(t){var i,n=[],r=[];for(this._root&&n.push(new e(this._root,this._x0,this._y0,this._x1,this._y1));i=n.pop();){var s=i.node;if(s.length){var h,o=i.x0,a=i.y0,u=i.x1,l=i.y1,_=(o+u)/2,f=(a+l)/2;(h=s[0])&&n.push(new e(h,o,a,_,f)),(h=s[1])&&n.push(new e(h,_,a,u,f)),(h=s[2])&&n.push(new e(h,o,f,_,l)),(h=s[3])&&n.push(new e(h,_,f,u,l))}r.push(i)}for(;i=r.pop();)t(i.node,i.x0,i.y0,i.x1,i.y1);return this},a.x=function(t){return arguments.length?(this._x=t,this):this._x},a.y=function(t){return arguments.length?(this._y=t,this):this._y},t.quadtree=s,Object.defineProperty(t,"__esModule",{value:!0})}))
|
|
10261
|
+
|
|
10262
|
+
// D3 Timer v3.0.1
|
|
10263
|
+
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";var n,e,o=0,i=0,r=0,l=0,u=0,a=0,s="object"==typeof performance&&performance.now?performance:Date,c="object"==typeof window&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(t){setTimeout(t,17)};function f(){return u||(c(_),u=s.now()+a)}function _(){u=0}function m(){this._call=this._time=this._next=null}function p(t,n,e){var o=new m;return o.restart(t,n,e),o}function w(){f(),++o;for(var t,e=n;e;)(t=u-e._time)>=0&&e._call.call(void 0,t),e=e._next;--o}function d(){u=(l=s.now())+a,o=i=0;try{w()}finally{o=0,function(){var t,o,i=n,r=1/0;for(;i;)i._call?(r>i._time&&(r=i._time),t=i,i=i._next):(o=i._next,i._next=null,i=t?t._next=o:n=o);e=t,y(r)}(),u=0}}function h(){var t=s.now(),n=t-l;n>1e3&&(a-=n,l=t)}function y(t){o||(i&&(i=clearTimeout(i)),t-u>24?(t<1/0&&(i=setTimeout(d,t-s.now()-a)),r&&(r=clearInterval(r))):(r||(l=s.now(),r=setInterval(h,1e3)),o=1,c(d)))}m.prototype=p.prototype={constructor:m,restart:function(t,o,i){if("function"!=typeof t)throw new TypeError("callback is not a function");i=(null==i?f():+i)+(null==o?0:+o),this._next||e===this||(e?e._next=this:n=this,e=this),this._call=t,this._time=i,y()},stop:function(){this._call&&(this._call=null,this._time=1/0,y())}},t.interval=function(t,n,e){var o=new m,i=n;return null==n?(o.restart(t,n,e),o):(o._restart=o.restart,o.restart=function(t,n,e){n=+n,e=null==e?f():+e,o._restart((function r(l){l+=i,o._restart(r,i+=n,e),t(l)}),n,e)},o.restart(t,n,e),o)},t.now=f,t.timeout=function(t,n,e){var o=new m;return n=null==n?0:+n,o.restart((e=>{o.stop(),t(e+n)}),n,e),o},t.timer=p,t.timerFlush=w,Object.defineProperty(t,"__esModule",{value:!0})}))
|
|
10264
|
+
|
|
10265
|
+
// D3 Force v3.0.0
|
|
10266
|
+
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("d3-quadtree"),require("d3-dispatch"),require("d3-timer")):"function"==typeof define&&define.amd?define(["exports","d3-quadtree","d3-dispatch","d3-timer"],t):t((n="undefined"!=typeof globalThis?globalThis:n||self).d3=n.d3||{},n.d3,n.d3,n.d3)}(this,(function(n,t,e,r){"use strict";function i(n){return function(){return n}}function u(n){return 1e-6*(n()-.5)}function o(n){return n.x+n.vx}function f(n){return n.y+n.vy}function a(n){return n.index}function c(n,t){var e=n.get(t);if(!e)throw new Error("node not found: "+t);return e}const l=4294967296;function h(n){return n.x}function v(n){return n.y}var y=Math.PI*(3-Math.sqrt(5));n.forceCenter=function(n,t){var e,r=1;function i(){var i,u,o=e.length,f=0,a=0;for(i=0;i<o;++i)f+=(u=e[i]).x,a+=u.y;for(f=(f/o-n)*r,a=(a/o-t)*r,i=0;i<o;++i)(u=e[i]).x-=f,u.y-=a}return null==n&&(n=0),null==t&&(t=0),i.initialize=function(n){e=n},i.x=function(t){return arguments.length?(n=+t,i):n},i.y=function(n){return arguments.length?(t=+n,i):t},i.strength=function(n){return arguments.length?(r=+n,i):r},i},n.forceCollide=function(n){var e,r,a,c,l=1,h=1;function v(){for(var n,i,h,y,d,g,x,s=e.length,p=0;p<h;++p)for(i=t.quadtree(e,o,f).visitAfter(y),n=0;n<s;++n)h=e[n],g=r[h.index],x=g*g,y=h.x+h.vx,d=h.y+h.vy,i.visit(M);function M(n,t,e,r,i){var o=n.data,f=n.r,l=g+f;if(!o)return t>y+l||r<y-l||e>d+l||i<d-l;if(o.index>h.index){var v=y-o.x-o.vx,s=d-o.y-o.vy,p=v*v+s*s;p<l*l&&(0===v&&(p+=(v=u(a))*v),0===s&&(p+=(s=u(a))*s),p=(l-(p=Math.sqrt(p)))/p*c,h.vx+=(v*=p)*(l=(f*=f)/(x+f)),h.vy+=(s*=p)*l,o.vx-=v*(l=1-l),o.vy-=s*l)}}}function y(n){if(n.data)return n.r=r[n.data.index];for(var t=n.r=0;t<4;++t)n[t]&&n[t].r>n.r&&(n.r=n[t].r)}function d(){if(e){var t,i,u=e.length;for(r=new Array(u),t=0;t<u;++t)i=e[t],r[i.index]=+n(i,t,e)}}return"function"!=typeof n&&(n=i(null==n?1:+n)),v.initialize=function(n,t){e=n,a=t,d()},v.iterations=function(n){return arguments.length?(h=+n,v):h},v.strength=function(n){return arguments.length?(c=+n,v):c},v.radius=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),d(),v):n},v},n.forceLink=function(n){var t,e,r,o,f,l,h=a,v=function(n){return 1/Math.min(o[n.source.index],o[n.target.index])},y=i(30),d=1;function g(r){for(var i=0,o=n.length;i<d;++i)for(var a,c,h,v,y,g,x,s=0;s<o;++s)c=(a=n[s]).source,v=(h=a.target).x+h.vx-c.x-c.vx||u(l),y=h.y+h.vy-c.y-c.vy||u(l),v*=g=((g=Math.sqrt(v*v+y*y))-e[s])/g*r*t[s],y*=g,h.vx-=v*(x=f[s]),h.vy-=y*x,c.vx+=v*(x=1-x),c.vy+=y*x}function x(){if(r){var i,u,a=r.length,l=n.length,v=new Map(r.map(((n,t)=>[h(n,t,r),n])));for(i=0,o=new Array(a);i<l;++i)(u=n[i]).index=i,"object"!=typeof u.source&&(u.source=c(v,u.source)),"object"!=typeof u.target&&(u.target=c(v,u.target)),o[u.source.index]=(o[u.source.index]||0)+1,o[u.target.index]=(o[u.target.index]||0)+1;for(i=0,f=new Array(l);i<l;++i)u=n[i],f[i]=o[u.source.index]/(o[u.source.index]+o[u.target.index]);t=new Array(l),s(),e=new Array(l),p()}}function s(){if(r)for(var e=0,i=n.length;e<i;++e)t[e]=+v(n[e],e,n)}function p(){if(r)for(var t=0,i=n.length;t<i;++t)e[t]=+y(n[t],t,n)}return null==n&&(n=[]),g.initialize=function(n,t){r=n,l=t,x()},g.links=function(t){return arguments.length?(n=t,x(),g):n},g.id=function(n){return arguments.length?(h=n,g):h},g.iterations=function(n){return arguments.length?(d=+n,g):d},g.strength=function(n){return arguments.length?(v="function"==typeof n?n:i(+n),s(),g):v},g.distance=function(n){return arguments.length?(y="function"==typeof n?n:i(+n),p(),g):y},g},n.forceManyBody=function(){var n,e,r,o,f,a=i(-30),c=1,l=1/0,y=.81;function d(r){var i,u=n.length,f=t.quadtree(n,h,v).visitAfter(x);for(o=r,i=0;i<u;++i)e=n[i],f.visit(s)}function g(){if(n){var t,e,r=n.length;for(f=new Array(r),t=0;t<r;++t)e=n[t],f[e.index]=+a(e,t,n)}}function x(n){var t,e,r,i,u,o=0,a=0;if(n.length){for(r=i=u=0;u<4;++u)(t=n[u])&&(e=Math.abs(t.value))&&(o+=t.value,a+=e,r+=e*t.x,i+=e*t.y);n.x=r/a,n.y=i/a}else{(t=n).x=t.data.x,t.y=t.data.y;do{o+=f[t.data.index]}while(t=t.next)}n.value=o}function s(n,t,i,a){if(!n.value)return!0;var h=n.x-e.x,v=n.y-e.y,d=a-t,g=h*h+v*v;if(d*d/y<g)return g<l&&(0===h&&(g+=(h=u(r))*h),0===v&&(g+=(v=u(r))*v),g<c&&(g=Math.sqrt(c*g)),e.vx+=h*n.value*o/g,e.vy+=v*n.value*o/g),!0;if(!(n.length||g>=l)){(n.data!==e||n.next)&&(0===h&&(g+=(h=u(r))*h),0===v&&(g+=(v=u(r))*v),g<c&&(g=Math.sqrt(c*g)));do{n.data!==e&&(d=f[n.data.index]*o/g,e.vx+=h*d,e.vy+=v*d)}while(n=n.next)}}return d.initialize=function(t,e){n=t,r=e,g()},d.strength=function(n){return arguments.length?(a="function"==typeof n?n:i(+n),g(),d):a},d.distanceMin=function(n){return arguments.length?(c=n*n,d):Math.sqrt(c)},d.distanceMax=function(n){return arguments.length?(l=n*n,d):Math.sqrt(l)},d.theta=function(n){return arguments.length?(y=n*n,d):Math.sqrt(y)},d},n.forceRadial=function(n,t,e){var r,u,o,f=i(.1);function a(n){for(var i=0,f=r.length;i<f;++i){var a=r[i],c=a.x-t||1e-6,l=a.y-e||1e-6,h=Math.sqrt(c*c+l*l),v=(o[i]-h)*u[i]*n/h;a.vx+=c*v,a.vy+=l*v}}function c(){if(r){var t,e=r.length;for(u=new Array(e),o=new Array(e),t=0;t<e;++t)o[t]=+n(r[t],t,r),u[t]=isNaN(o[t])?0:+f(r[t],t,r)}}return"function"!=typeof n&&(n=i(+n)),null==t&&(t=0),null==e&&(e=0),a.initialize=function(n){r=n,c()},a.strength=function(n){return arguments.length?(f="function"==typeof n?n:i(+n),c(),a):f},a.radius=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),c(),a):n},a.x=function(n){return arguments.length?(t=+n,a):t},a.y=function(n){return arguments.length?(e=+n,a):e},a},n.forceSimulation=function(n){var t,i=1,u=.001,o=1-Math.pow(u,1/300),f=0,a=.6,c=new Map,h=r.timer(g),v=e.dispatch("tick","end"),d=function(){let n=1;return()=>(n=(1664525*n+1013904223)%l)/l}();function g(){x(),v.call("tick",t),i<u&&(h.stop(),v.call("end",t))}function x(e){var r,u,l=n.length;void 0===e&&(e=1);for(var h=0;h<e;++h)for(i+=(f-i)*o,c.forEach((function(n){n(i)})),r=0;r<l;++r)null==(u=n[r]).fx?u.x+=u.vx*=a:(u.x=u.fx,u.vx=0),null==u.fy?u.y+=u.vy*=a:(u.y=u.fy,u.vy=0);return t}function s(){for(var t,e=0,r=n.length;e<r;++e){if((t=n[e]).index=e,null!=t.fx&&(t.x=t.fx),null!=t.fy&&(t.y=t.fy),isNaN(t.x)||isNaN(t.y)){var i=10*Math.sqrt(.5+e),u=e*y;t.x=i*Math.cos(u),t.y=i*Math.sin(u)}(isNaN(t.vx)||isNaN(t.vy))&&(t.vx=t.vy=0)}}function p(t){return t.initialize&&t.initialize(n,d),t}return null==n&&(n=[]),s(),t={tick:x,restart:function(){return h.restart(g),t},stop:function(){return h.stop(),t},nodes:function(e){return arguments.length?(n=e,s(),c.forEach(p),t):n},alpha:function(n){return arguments.length?(i=+n,t):i},alphaMin:function(n){return arguments.length?(u=+n,t):u},alphaDecay:function(n){return arguments.length?(o=+n,t):+o},alphaTarget:function(n){return arguments.length?(f=+n,t):f},velocityDecay:function(n){return arguments.length?(a=1-n,t):1-a},randomSource:function(n){return arguments.length?(d=n,c.forEach(p),t):d},force:function(n,e){return arguments.length>1?(null==e?c.delete(n):c.set(n,p(e)),t):c.get(n)},find:function(t,e,r){var i,u,o,f,a,c=0,l=n.length;for(null==r?r=1/0:r*=r,c=0;c<l;++c)(o=(i=t-(f=n[c]).x)*i+(u=e-f.y)*u)<r&&(a=f,r=o);return a},on:function(n,e){return arguments.length>1?(v.on(n,e),t):v.on(n)}}},n.forceX=function(n){var t,e,r,u=i(.1);function o(n){for(var i,u=0,o=t.length;u<o;++u)(i=t[u]).vx+=(r[u]-i.x)*e[u]*n}function f(){if(t){var i,o=t.length;for(e=new Array(o),r=new Array(o),i=0;i<o;++i)e[i]=isNaN(r[i]=+n(t[i],i,t))?0:+u(t[i],i,t)}}return"function"!=typeof n&&(n=i(null==n?0:+n)),o.initialize=function(n){t=n,f()},o.strength=function(n){return arguments.length?(u="function"==typeof n?n:i(+n),f(),o):u},o.x=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),f(),o):n},o},n.forceY=function(n){var t,e,r,u=i(.1);function o(n){for(var i,u=0,o=t.length;u<o;++u)(i=t[u]).vy+=(r[u]-i.y)*e[u]*n}function f(){if(t){var i,o=t.length;for(e=new Array(o),r=new Array(o),i=0;i<o;++i)e[i]=isNaN(r[i]=+n(t[i],i,t))?0:+u(t[i],i,t)}}return"function"!=typeof n&&(n=i(null==n?0:+n)),o.initialize=function(n){t=n,f()},o.strength=function(n){return arguments.length?(u="function"==typeof n?n:i(+n),f(),o):u},o.y=function(t){return arguments.length?(n="function"==typeof t?t:i(+t),f(),o):n},o},Object.defineProperty(n,"__esModule",{value:!0})}))
|
|
10267
|
+
|
|
10268
|
+
class PhysicsWorker {
|
|
10269
|
+
constructor() {
|
|
10270
|
+
this.simulation = null;
|
|
10271
|
+
this.nodes = [];
|
|
10272
|
+
this.links = [];
|
|
10273
|
+
this.isRunning = false;
|
|
10274
|
+
this.tickCount = 0;
|
|
10275
|
+
this.maxTicks = 300;
|
|
10276
|
+
this.intervalId = null; // Track interval for cleanup
|
|
10277
|
+
}
|
|
10278
|
+
|
|
10279
|
+
initializeSimulation(config) {
|
|
10280
|
+
const {
|
|
10281
|
+
nodes,
|
|
10282
|
+
links,
|
|
10283
|
+
width = 400,
|
|
10284
|
+
height = 300,
|
|
10285
|
+
forces = {},
|
|
10286
|
+
maxTicks = 300,
|
|
10287
|
+
alphaDecay = 0.0228,
|
|
10288
|
+
velocityDecay = 0.4
|
|
10289
|
+
} = config;
|
|
10290
|
+
|
|
10291
|
+
this.nodes = nodes.map(node => ({ ...node }));
|
|
10292
|
+
this.links = links.map(link => ({ ...link }));
|
|
10293
|
+
this.maxTicks = maxTicks;
|
|
10294
|
+
this.tickCount = 0;
|
|
10295
|
+
|
|
10296
|
+
this.simulation = d3.forceSimulation(this.nodes)
|
|
10297
|
+
.alphaDecay(alphaDecay)
|
|
10298
|
+
.velocityDecay(velocityDecay);
|
|
10299
|
+
|
|
10300
|
+
if (this.links.length > 0) {
|
|
10301
|
+
this.simulation.force('link',
|
|
10302
|
+
d3.forceLink(this.links)
|
|
10303
|
+
.id(d => d.id)
|
|
10304
|
+
.distance(forces.linkDistance || 30)
|
|
10305
|
+
.strength(forces.linkStrength || 1)
|
|
10306
|
+
);
|
|
10307
|
+
}
|
|
10308
|
+
|
|
10309
|
+
this.simulation.force('charge',
|
|
10310
|
+
d3.forceManyBody()
|
|
10311
|
+
.strength(forces.chargeStrength || -300)
|
|
10312
|
+
);
|
|
10313
|
+
|
|
10314
|
+
this.simulation.force('center',
|
|
10315
|
+
d3.forceCenter(width / 2, height / 2)
|
|
10316
|
+
.strength(forces.centerStrength || 1)
|
|
10317
|
+
);
|
|
10318
|
+
|
|
10319
|
+
if (forces.collisionRadius) {
|
|
10320
|
+
this.simulation.force('collision',
|
|
10321
|
+
d3.forceCollide(forces.collisionRadius)
|
|
10322
|
+
.strength(forces.collisionStrength || 0.7)
|
|
10323
|
+
);
|
|
10324
|
+
}
|
|
10325
|
+
|
|
10326
|
+
return {
|
|
10327
|
+
success: true,
|
|
10328
|
+
message: \`Simulation initialized with \${this.nodes.length} nodes and \${this.links.length} links\`
|
|
10329
|
+
};
|
|
10330
|
+
}
|
|
10331
|
+
|
|
10332
|
+
runSimulation() {
|
|
10333
|
+
if (!this.simulation || this.isRunning) {
|
|
10334
|
+
return { success: false, message: 'Simulation not initialized or already running' };
|
|
10335
|
+
}
|
|
10336
|
+
|
|
10337
|
+
// Clear any existing interval
|
|
10338
|
+
this.clearInterval();
|
|
10339
|
+
|
|
10340
|
+
this.isRunning = true;
|
|
10341
|
+
this.tickCount = 0;
|
|
10342
|
+
|
|
10343
|
+
this.intervalId = setInterval(() => {
|
|
10344
|
+
try {
|
|
10345
|
+
if (this.tickCount >= this.maxTicks || this.simulation.alpha() < 0.01) {
|
|
10346
|
+
this.completeSimulation();
|
|
10347
|
+
return;
|
|
10348
|
+
}
|
|
10349
|
+
|
|
10350
|
+
this.simulation.tick();
|
|
10351
|
+
this.tickCount++;
|
|
10352
|
+
|
|
10353
|
+
if (this.tickCount % 20 === 0) {
|
|
10354
|
+
postMessage({
|
|
10355
|
+
type: 'simulation_progress',
|
|
10356
|
+
data: {
|
|
10357
|
+
tickCount: this.tickCount,
|
|
10358
|
+
alpha: this.simulation.alpha(),
|
|
10359
|
+
progress: this.tickCount / this.maxTicks
|
|
10360
|
+
}
|
|
10361
|
+
});
|
|
10362
|
+
}
|
|
10363
|
+
} catch (error) {
|
|
10364
|
+
this.handleSimulationError(error);
|
|
10365
|
+
}
|
|
10366
|
+
}, 16);
|
|
10367
|
+
|
|
10368
|
+
return { success: true, message: 'Simulation started' };
|
|
10369
|
+
}
|
|
10370
|
+
|
|
10371
|
+
completeSimulation() {
|
|
10372
|
+
this.clearInterval();
|
|
10373
|
+
this.isRunning = false;
|
|
10374
|
+
|
|
10375
|
+
// Create final node data with memory-efficient copying
|
|
10376
|
+
const finalNodes = this.nodes.map(node => ({
|
|
10377
|
+
id: node.id,
|
|
10378
|
+
x: node.x || 0,
|
|
10379
|
+
y: node.y || 0,
|
|
10380
|
+
vx: node.vx || 0,
|
|
10381
|
+
vy: node.vy || 0
|
|
10382
|
+
}));
|
|
10383
|
+
|
|
10384
|
+
postMessage({
|
|
10385
|
+
type: 'simulation_complete',
|
|
10386
|
+
data: {
|
|
10387
|
+
nodes: finalNodes,
|
|
10388
|
+
tickCount: this.tickCount,
|
|
10389
|
+
finalAlpha: this.simulation ? this.simulation.alpha() : 0
|
|
10390
|
+
}
|
|
10391
|
+
});
|
|
10392
|
+
|
|
10393
|
+
// Clean up simulation resources
|
|
10394
|
+
this.cleanupSimulation();
|
|
10395
|
+
}
|
|
10396
|
+
|
|
10397
|
+
handleSimulationError(error) {
|
|
10398
|
+
this.clearInterval();
|
|
10399
|
+
this.isRunning = false;
|
|
10400
|
+
this.cleanupSimulation();
|
|
10401
|
+
|
|
10402
|
+
postMessage({
|
|
10403
|
+
type: 'error',
|
|
10404
|
+
data: {
|
|
10405
|
+
success: false,
|
|
10406
|
+
message: 'Simulation error: ' + error.message,
|
|
10407
|
+
stack: error.stack
|
|
10408
|
+
}
|
|
10409
|
+
});
|
|
10410
|
+
}
|
|
10411
|
+
|
|
10412
|
+
stopSimulation() {
|
|
10413
|
+
this.clearInterval();
|
|
10414
|
+
|
|
10415
|
+
if (this.simulation) {
|
|
10416
|
+
this.simulation.stop();
|
|
10417
|
+
this.isRunning = false;
|
|
10418
|
+
this.cleanupSimulation();
|
|
10419
|
+
return { success: true, message: 'Simulation stopped' };
|
|
10420
|
+
}
|
|
10421
|
+
return { success: false, message: 'No active simulation' };
|
|
10422
|
+
}
|
|
10423
|
+
|
|
10424
|
+
clearInterval() {
|
|
10425
|
+
if (this.intervalId) {
|
|
10426
|
+
clearInterval(this.intervalId);
|
|
10427
|
+
this.intervalId = null;
|
|
10428
|
+
}
|
|
10429
|
+
}
|
|
10430
|
+
|
|
10431
|
+
cleanupSimulation() {
|
|
10432
|
+
// Clear D3 simulation references
|
|
10433
|
+
if (this.simulation) {
|
|
10434
|
+
this.simulation.stop();
|
|
10435
|
+
this.simulation = null;
|
|
10436
|
+
}
|
|
10437
|
+
|
|
10438
|
+
// Clear node and link arrays to free memory
|
|
10439
|
+
this.nodes.length = 0;
|
|
10440
|
+
this.links.length = 0;
|
|
10441
|
+
|
|
10442
|
+
// Reset state
|
|
10443
|
+
this.tickCount = 0;
|
|
10444
|
+
this.isRunning = false;
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10447
|
+
|
|
10448
|
+
const physicsWorker = new PhysicsWorker();
|
|
10449
|
+
|
|
10450
|
+
self.onmessage = function(event) {
|
|
10451
|
+
const { type, data } = event.data;
|
|
10452
|
+
|
|
10453
|
+
try {
|
|
10454
|
+
let result;
|
|
10455
|
+
|
|
10456
|
+
switch (type) {
|
|
10457
|
+
case 'initialize':
|
|
10458
|
+
result = physicsWorker.initializeSimulation(data);
|
|
10459
|
+
break;
|
|
10460
|
+
case 'run':
|
|
10461
|
+
result = physicsWorker.runSimulation();
|
|
10462
|
+
break;
|
|
10463
|
+
case 'stop':
|
|
10464
|
+
result = physicsWorker.stopSimulation();
|
|
10465
|
+
break;
|
|
10466
|
+
default:
|
|
10467
|
+
result = { success: false, message: \`Unknown command: \${type}\` };
|
|
10468
|
+
}
|
|
10469
|
+
|
|
10470
|
+
postMessage({
|
|
10471
|
+
type: 'response',
|
|
10472
|
+
data: result
|
|
10473
|
+
});
|
|
10474
|
+
|
|
10475
|
+
} catch (error) {
|
|
10476
|
+
postMessage({
|
|
10477
|
+
type: 'error',
|
|
10478
|
+
data: {
|
|
10479
|
+
success: false,
|
|
10480
|
+
message: error.message,
|
|
10481
|
+
stack: error.stack
|
|
10482
|
+
}
|
|
10483
|
+
});
|
|
10484
|
+
}
|
|
10485
|
+
};
|
|
10486
|
+
`;
|
|
10487
|
+
}
|
|
10488
|
+
/**
|
|
10489
|
+
* Set up worker event listeners
|
|
10490
|
+
*/
|
|
10491
|
+
setupWorkerEventListeners() {
|
|
10492
|
+
if (!this.worker) return;
|
|
10493
|
+
this.worker.onmessage = (event) => {
|
|
10494
|
+
const { type, data } = event.data;
|
|
10495
|
+
switch (type) {
|
|
10496
|
+
case "response":
|
|
10497
|
+
case "simulation_complete":
|
|
10498
|
+
this.resolvePromise(data);
|
|
10499
|
+
break;
|
|
10500
|
+
case "simulation_progress":
|
|
10501
|
+
break;
|
|
10502
|
+
case "error":
|
|
10503
|
+
this.rejectPromise(new Error(data.message));
|
|
10504
|
+
break;
|
|
10505
|
+
}
|
|
10506
|
+
};
|
|
10507
|
+
this.worker.onerror = (error) => {
|
|
10508
|
+
this.rejectPromise(new Error(`Worker error: ${error.message}`));
|
|
10509
|
+
};
|
|
10510
|
+
}
|
|
10511
|
+
/**
|
|
10512
|
+
* Send message to worker with promise handling
|
|
10513
|
+
*/
|
|
10514
|
+
sendMessage(message) {
|
|
10515
|
+
return new Promise((resolve, reject) => {
|
|
10516
|
+
if (!this.worker) {
|
|
10517
|
+
reject(new Error("Physics Worker not available"));
|
|
10518
|
+
return;
|
|
10519
|
+
}
|
|
10520
|
+
const id2 = ++this.messageId;
|
|
10521
|
+
this.pendingPromises.set(id2, {
|
|
10522
|
+
resolve,
|
|
10523
|
+
reject
|
|
10524
|
+
});
|
|
10525
|
+
const timeoutId = window.setTimeout(() => {
|
|
10526
|
+
if (this.pendingPromises.has(id2)) {
|
|
10527
|
+
this.pendingPromises.delete(id2);
|
|
10528
|
+
this.promiseTimeouts.delete(id2);
|
|
10529
|
+
reject(new Error("Physics Worker timeout"));
|
|
10530
|
+
}
|
|
10531
|
+
}, 3e4);
|
|
10532
|
+
this.promiseTimeouts.set(id2, timeoutId);
|
|
10533
|
+
this.worker.postMessage({ ...message, id: id2 });
|
|
10534
|
+
});
|
|
10535
|
+
}
|
|
10536
|
+
/**
|
|
10537
|
+
* Resolve pending promise
|
|
10538
|
+
*/
|
|
10539
|
+
resolvePromise(data) {
|
|
10540
|
+
const promises = Array.from(this.pendingPromises.values());
|
|
10541
|
+
this.clearAllTimeouts();
|
|
10542
|
+
this.pendingPromises.clear();
|
|
10543
|
+
promises.forEach(({ resolve }) => resolve(data));
|
|
10544
|
+
}
|
|
10545
|
+
/**
|
|
10546
|
+
* Reject pending promise
|
|
10547
|
+
*/
|
|
10548
|
+
rejectPromise(error) {
|
|
10549
|
+
const promises = Array.from(this.pendingPromises.values());
|
|
10550
|
+
this.clearAllTimeouts();
|
|
10551
|
+
this.pendingPromises.clear();
|
|
10552
|
+
promises.forEach(({ reject }) => reject(error));
|
|
10553
|
+
}
|
|
10554
|
+
/**
|
|
10555
|
+
* Clear all promise timeouts
|
|
10556
|
+
*/
|
|
10557
|
+
clearAllTimeouts() {
|
|
10558
|
+
for (const timeoutId of this.promiseTimeouts.values()) {
|
|
10559
|
+
window.clearTimeout(timeoutId);
|
|
10560
|
+
}
|
|
10561
|
+
this.promiseTimeouts.clear();
|
|
10562
|
+
}
|
|
10563
|
+
/**
|
|
10564
|
+
* Initialize physics simulation with given configuration
|
|
10565
|
+
*/
|
|
10566
|
+
async initialize(config) {
|
|
10567
|
+
if (!this.worker) {
|
|
10568
|
+
return false;
|
|
10569
|
+
}
|
|
10570
|
+
try {
|
|
10571
|
+
const response = await this.sendMessage({
|
|
10572
|
+
type: "initialize",
|
|
10573
|
+
data: config
|
|
10574
|
+
});
|
|
10575
|
+
return response.success;
|
|
10576
|
+
} catch {
|
|
10577
|
+
return false;
|
|
10578
|
+
}
|
|
10579
|
+
}
|
|
10580
|
+
/**
|
|
10581
|
+
* Run the physics simulation and return final node positions
|
|
10582
|
+
*/
|
|
10583
|
+
async runSimulation() {
|
|
10584
|
+
if (!this.worker) {
|
|
10585
|
+
throw new Error("Physics Worker not available");
|
|
10586
|
+
}
|
|
10587
|
+
await this.sendMessage({
|
|
10588
|
+
type: "run"
|
|
10589
|
+
});
|
|
10590
|
+
return new Promise((resolve, reject) => {
|
|
10591
|
+
if (!this.worker) {
|
|
10592
|
+
reject(new Error("Physics Worker not available"));
|
|
10593
|
+
return;
|
|
10594
|
+
}
|
|
10595
|
+
const handleMessage = (event) => {
|
|
10596
|
+
const { type, data } = event.data;
|
|
10597
|
+
if (type === "simulation_complete") {
|
|
10598
|
+
this.worker?.removeEventListener("message", handleMessage);
|
|
10599
|
+
resolve(data.nodes);
|
|
10600
|
+
} else if (type === "error") {
|
|
10601
|
+
this.worker?.removeEventListener("message", handleMessage);
|
|
10602
|
+
reject(new Error(data.message));
|
|
10603
|
+
}
|
|
10604
|
+
};
|
|
10605
|
+
this.worker.addEventListener("message", handleMessage);
|
|
10606
|
+
});
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* Stop the current simulation
|
|
10610
|
+
*/
|
|
10611
|
+
stopSimulation() {
|
|
10612
|
+
if (this.worker) {
|
|
10613
|
+
this.worker.postMessage({ type: "stop" });
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
/**
|
|
10617
|
+
* Terminate the worker and cleanup resources
|
|
10618
|
+
*/
|
|
10619
|
+
terminate() {
|
|
10620
|
+
this.clearAllTimeouts();
|
|
10621
|
+
if (this.worker) {
|
|
10622
|
+
this.worker.terminate();
|
|
10623
|
+
this.worker = null;
|
|
10624
|
+
}
|
|
10625
|
+
if (this.workerUrl) {
|
|
10626
|
+
URL.revokeObjectURL(this.workerUrl);
|
|
10627
|
+
this.workerUrl = null;
|
|
10628
|
+
}
|
|
10629
|
+
this.pendingPromises.clear();
|
|
10630
|
+
this.messageId = 0;
|
|
10631
|
+
}
|
|
10632
|
+
/**
|
|
10633
|
+
* Check if web worker is available
|
|
10634
|
+
*/
|
|
10635
|
+
isAvailable() {
|
|
10636
|
+
return this.worker !== null && typeof Worker !== "undefined";
|
|
10637
|
+
}
|
|
10638
|
+
};
|
|
10639
|
+
var physicsWorkerManager = new PhysicsWorkerManagerImpl();
|
|
10640
|
+
|
|
10641
|
+
// src/force-graph-wrapper/utils/performance-monitor.ts
|
|
10642
|
+
var PerformanceMonitor = class {
|
|
10643
|
+
metrics = {
|
|
10644
|
+
creation: 0,
|
|
10645
|
+
dataLoad: 0,
|
|
10646
|
+
firstRender: 0,
|
|
10647
|
+
render: 0,
|
|
10648
|
+
methodCalls: 0,
|
|
10649
|
+
memoryUsage: 0,
|
|
10650
|
+
nodeCount: 0,
|
|
10651
|
+
linkCount: 0,
|
|
10652
|
+
optimizedForNodeCount: 0,
|
|
10653
|
+
timestamp: Date.now()
|
|
10654
|
+
};
|
|
10655
|
+
// Internal detailed tracking
|
|
10656
|
+
methodCallsDetailed = /* @__PURE__ */ new Map();
|
|
10657
|
+
/**
|
|
10658
|
+
* Track execution time of any operation
|
|
10659
|
+
*/
|
|
10660
|
+
track(operation, fn) {
|
|
10661
|
+
const start2 = performance.now();
|
|
10662
|
+
const result = fn();
|
|
10663
|
+
const duration = performance.now() - start2;
|
|
10664
|
+
if (!this.methodCallsDetailed.has(operation)) {
|
|
10665
|
+
this.methodCallsDetailed.set(operation, []);
|
|
10666
|
+
}
|
|
10667
|
+
this.methodCallsDetailed.get(operation).push(duration);
|
|
10668
|
+
this.metrics["methodCalls"]++;
|
|
10669
|
+
return result;
|
|
10670
|
+
}
|
|
10671
|
+
/**
|
|
10672
|
+
* Set specific metric values
|
|
10673
|
+
*/
|
|
10674
|
+
setMetric(key, value) {
|
|
10675
|
+
if (key !== "timestamp") {
|
|
10676
|
+
this.metrics[key] = value;
|
|
10677
|
+
}
|
|
10678
|
+
}
|
|
10679
|
+
/**
|
|
10680
|
+
* Get current metrics snapshot
|
|
10681
|
+
*/
|
|
10682
|
+
getMetrics() {
|
|
10683
|
+
if (typeof performance.memory !== "undefined") {
|
|
10684
|
+
const perfMemory = performance.memory;
|
|
10685
|
+
this.metrics["memoryUsage"] = perfMemory.usedJSHeapSize / 1024 / 1024;
|
|
10686
|
+
}
|
|
10687
|
+
return {
|
|
10688
|
+
...this.metrics,
|
|
10689
|
+
timestamp: Date.now()
|
|
10690
|
+
};
|
|
10691
|
+
}
|
|
10692
|
+
/**
|
|
10693
|
+
* Get performance summary with statistics
|
|
10694
|
+
*/
|
|
10695
|
+
getSummary() {
|
|
10696
|
+
const averages = {};
|
|
10697
|
+
const peaks = {};
|
|
10698
|
+
const warnings = [];
|
|
10699
|
+
this.methodCallsDetailed.forEach((times, operation) => {
|
|
10700
|
+
const avg = times.reduce((sum2, time) => sum2 + time, 0) / times.length;
|
|
10701
|
+
const peak = Math.max(...times);
|
|
10702
|
+
averages[operation] = Math.round(avg * 1e3) / 1e3;
|
|
10703
|
+
peaks[operation] = Math.round(peak * 1e3) / 1e3;
|
|
10704
|
+
if (avg > 100) {
|
|
10705
|
+
warnings.push(`${operation} averaging ${avg.toFixed(1)}ms - consider optimization`);
|
|
10706
|
+
}
|
|
10707
|
+
if (peak > 500) {
|
|
10708
|
+
warnings.push(`${operation} peaked at ${peak.toFixed(1)}ms - investigate bottleneck`);
|
|
10709
|
+
}
|
|
10710
|
+
});
|
|
10711
|
+
averages["creation"] = this.metrics["creation"];
|
|
10712
|
+
averages["dataLoad"] = this.metrics["dataLoad"];
|
|
10713
|
+
averages["firstRender"] = this.metrics["firstRender"];
|
|
10714
|
+
averages["render"] = this.metrics["render"];
|
|
10715
|
+
let nodeScaling = "Linear";
|
|
10716
|
+
if (this.metrics["nodeCount"] > 1e3 && this.metrics["dataLoad"] > 100) {
|
|
10717
|
+
nodeScaling = "Sub-optimal - may be O(n\xB2)";
|
|
10718
|
+
warnings.push("Data loading appears to scale poorly with node count");
|
|
10719
|
+
}
|
|
10720
|
+
return {
|
|
10721
|
+
averages,
|
|
10722
|
+
peaks,
|
|
10723
|
+
warnings,
|
|
10724
|
+
nodeScaling
|
|
10725
|
+
};
|
|
10726
|
+
}
|
|
10727
|
+
/**
|
|
10728
|
+
* Reset all metrics
|
|
10729
|
+
*/
|
|
10730
|
+
reset() {
|
|
10731
|
+
this.metrics = {
|
|
10732
|
+
creation: 0,
|
|
10733
|
+
dataLoad: 0,
|
|
10734
|
+
firstRender: 0,
|
|
10735
|
+
render: 0,
|
|
10736
|
+
methodCalls: 0,
|
|
10737
|
+
memoryUsage: 0,
|
|
10738
|
+
nodeCount: 0,
|
|
10739
|
+
linkCount: 0,
|
|
10740
|
+
optimizedForNodeCount: 0,
|
|
10741
|
+
timestamp: Date.now()
|
|
10742
|
+
};
|
|
10743
|
+
this.methodCallsDetailed.clear();
|
|
10744
|
+
}
|
|
10745
|
+
/**
|
|
10746
|
+
* Log performance summary to console
|
|
10747
|
+
*/
|
|
10748
|
+
logSummary() {
|
|
10749
|
+
}
|
|
10750
|
+
};
|
|
10751
|
+
var globalPerformanceMonitor = new PerformanceMonitor();
|
|
10752
|
+
|
|
10753
|
+
// src/force-graph-wrapper/core/rendering-performance-monitor.ts
|
|
10754
|
+
var RenderingPerformanceMonitor = class {
|
|
10755
|
+
frames = [];
|
|
10756
|
+
frameTimes = [];
|
|
10757
|
+
lastFrameTime = 0;
|
|
10758
|
+
renderCallCount = 0;
|
|
10759
|
+
canvasOpCount = 0;
|
|
10760
|
+
droppedFrameCount = 0;
|
|
10761
|
+
isMonitoring = false;
|
|
10762
|
+
animationFrameId = null;
|
|
10763
|
+
maxSamples = 120;
|
|
10764
|
+
// 2 seconds at 60fps
|
|
10765
|
+
targets = {
|
|
10766
|
+
targetFps: 60,
|
|
10767
|
+
maxFrameTime: 16.67,
|
|
10768
|
+
// 1000ms / 60fps
|
|
10769
|
+
maxDroppedFrames: 5,
|
|
10770
|
+
efficiencyThreshold: 0.8
|
|
10771
|
+
};
|
|
10772
|
+
nodeCount = 0;
|
|
10773
|
+
linkCount = 0;
|
|
10774
|
+
constructor(targets) {
|
|
10775
|
+
if (targets) {
|
|
10776
|
+
this.targets = { ...this.targets, ...targets };
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
/**
|
|
10780
|
+
* Start monitoring rendering performance
|
|
10781
|
+
*/
|
|
10782
|
+
startMonitoring() {
|
|
10783
|
+
if (this.isMonitoring) return;
|
|
10784
|
+
this.isMonitoring = true;
|
|
10785
|
+
this.frames = [];
|
|
10786
|
+
this.frameTimes = [];
|
|
10787
|
+
this.renderCallCount = 0;
|
|
10788
|
+
this.canvasOpCount = 0;
|
|
10789
|
+
this.droppedFrameCount = 0;
|
|
10790
|
+
this.lastFrameTime = performance.now();
|
|
10791
|
+
this.monitorFrame();
|
|
10792
|
+
}
|
|
10793
|
+
/**
|
|
10794
|
+
* Stop monitoring rendering performance
|
|
10795
|
+
*/
|
|
10796
|
+
stopMonitoring() {
|
|
10797
|
+
this.isMonitoring = false;
|
|
10798
|
+
if (this.animationFrameId !== null) {
|
|
10799
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
10800
|
+
this.animationFrameId = null;
|
|
10801
|
+
}
|
|
10802
|
+
}
|
|
10803
|
+
/**
|
|
10804
|
+
* Record a render call (called by wrapper during rendering)
|
|
10805
|
+
*/
|
|
10806
|
+
recordRenderCall() {
|
|
10807
|
+
this.renderCallCount++;
|
|
10808
|
+
}
|
|
10809
|
+
/**
|
|
10810
|
+
* Record canvas operations (drawing calls, transforms, etc.)
|
|
10811
|
+
*/
|
|
10812
|
+
recordCanvasOperation() {
|
|
10813
|
+
this.canvasOpCount++;
|
|
10814
|
+
}
|
|
10815
|
+
/**
|
|
10816
|
+
* Update node and link counts for efficiency calculations
|
|
10817
|
+
*/
|
|
10818
|
+
updateCounts(nodeCount, linkCount) {
|
|
10819
|
+
this.nodeCount = nodeCount;
|
|
10820
|
+
this.linkCount = linkCount;
|
|
10821
|
+
}
|
|
10822
|
+
/**
|
|
10823
|
+
* Get current rendering metrics
|
|
10824
|
+
*/
|
|
10825
|
+
getMetrics() {
|
|
10826
|
+
const now2 = performance.now();
|
|
10827
|
+
const currentFps = this.calculateCurrentFps();
|
|
10828
|
+
const averageFps = this.calculateAverageFps();
|
|
10829
|
+
const currentFrameTime = this.calculateCurrentFrameTime();
|
|
10830
|
+
const averageFrameTime = this.calculateAverageFrameTime();
|
|
10831
|
+
return {
|
|
10832
|
+
fps: currentFps,
|
|
10833
|
+
averageFps,
|
|
10834
|
+
frameTime: currentFrameTime,
|
|
10835
|
+
averageFrameTime,
|
|
10836
|
+
droppedFrames: this.droppedFrameCount,
|
|
10837
|
+
renderCalls: this.renderCallCount,
|
|
10838
|
+
canvasOperations: this.canvasOpCount,
|
|
10839
|
+
nodeCount: this.nodeCount,
|
|
10840
|
+
linkCount: this.linkCount,
|
|
10841
|
+
lastMeasurement: now2,
|
|
10842
|
+
renderingEfficiency: this.calculateEfficiency(averageFps, averageFrameTime),
|
|
10843
|
+
memoryUsage: this.getMemoryUsage()
|
|
10844
|
+
};
|
|
10845
|
+
}
|
|
10846
|
+
/**
|
|
10847
|
+
* Check if performance targets are being met
|
|
10848
|
+
*/
|
|
10849
|
+
isPerformanceTargetMet() {
|
|
10850
|
+
const metrics = this.getMetrics();
|
|
10851
|
+
return metrics.averageFps >= this.targets.targetFps * 0.9 && // Allow 10% tolerance
|
|
10852
|
+
metrics.averageFrameTime <= this.targets.maxFrameTime * 1.1 && metrics.droppedFrames <= this.targets.maxDroppedFrames && metrics.renderingEfficiency >= this.targets.efficiencyThreshold;
|
|
10853
|
+
}
|
|
10854
|
+
/**
|
|
10855
|
+
* Get performance validation results
|
|
10856
|
+
*/
|
|
10857
|
+
validatePerformance() {
|
|
10858
|
+
const metrics = this.getMetrics();
|
|
10859
|
+
const fpsTest = {
|
|
10860
|
+
expected: this.targets.targetFps,
|
|
10861
|
+
actual: metrics.averageFps,
|
|
10862
|
+
passed: metrics.averageFps >= this.targets.targetFps * 0.9
|
|
10863
|
+
};
|
|
10864
|
+
const frameTimeTest = {
|
|
10865
|
+
expected: this.targets.maxFrameTime,
|
|
10866
|
+
actual: metrics.averageFrameTime,
|
|
10867
|
+
passed: metrics.averageFrameTime <= this.targets.maxFrameTime * 1.1
|
|
10868
|
+
};
|
|
10869
|
+
const droppedFramesTest = {
|
|
10870
|
+
expected: this.targets.maxDroppedFrames,
|
|
10871
|
+
actual: metrics.droppedFrames,
|
|
10872
|
+
passed: metrics.droppedFrames <= this.targets.maxDroppedFrames
|
|
10873
|
+
};
|
|
10874
|
+
const efficiencyTest = {
|
|
10875
|
+
expected: this.targets.efficiencyThreshold,
|
|
10876
|
+
actual: metrics.renderingEfficiency,
|
|
10877
|
+
passed: metrics.renderingEfficiency >= this.targets.efficiencyThreshold
|
|
10878
|
+
};
|
|
10879
|
+
const results = {
|
|
10880
|
+
fpsTarget: fpsTest,
|
|
10881
|
+
frameTimeTarget: frameTimeTest,
|
|
10882
|
+
droppedFramesTarget: droppedFramesTest,
|
|
10883
|
+
efficiencyTarget: efficiencyTest
|
|
10884
|
+
};
|
|
10885
|
+
const passedTests = Object.values(results).filter((test) => test.passed).length;
|
|
10886
|
+
const overallScore = passedTests / Object.keys(results).length;
|
|
10887
|
+
const passed = overallScore >= 0.75;
|
|
10888
|
+
return {
|
|
10889
|
+
passed,
|
|
10890
|
+
results,
|
|
10891
|
+
overallScore
|
|
10892
|
+
};
|
|
10893
|
+
}
|
|
10894
|
+
/**
|
|
10895
|
+
* Reset performance counters
|
|
10896
|
+
*/
|
|
10897
|
+
reset() {
|
|
10898
|
+
this.frames = [];
|
|
10899
|
+
this.frameTimes = [];
|
|
10900
|
+
this.renderCallCount = 0;
|
|
10901
|
+
this.canvasOpCount = 0;
|
|
10902
|
+
this.droppedFrameCount = 0;
|
|
10903
|
+
this.lastFrameTime = performance.now();
|
|
10904
|
+
}
|
|
10905
|
+
/**
|
|
10906
|
+
* Get rendering optimization recommendations
|
|
10907
|
+
*/
|
|
10908
|
+
getOptimizationRecommendations() {
|
|
10909
|
+
const metrics = this.getMetrics();
|
|
10910
|
+
const recommendations = [];
|
|
10911
|
+
if (metrics.averageFps < this.targets.targetFps * 0.8) {
|
|
10912
|
+
recommendations.push("FPS is significantly below target - consider reducing node count or simplifying rendering");
|
|
10913
|
+
}
|
|
10914
|
+
if (metrics.averageFrameTime > this.targets.maxFrameTime * 1.5) {
|
|
10915
|
+
recommendations.push("Frame time is too high - optimize canvas operations or reduce complexity");
|
|
10916
|
+
}
|
|
10917
|
+
if (metrics.droppedFrames > this.targets.maxDroppedFrames * 2) {
|
|
10918
|
+
recommendations.push("Too many dropped frames - consider implementing frame skipping or LOD");
|
|
10919
|
+
}
|
|
10920
|
+
if (metrics.renderingEfficiency < 0.5) {
|
|
10921
|
+
recommendations.push("Low rendering efficiency - review canvas drawing operations");
|
|
10922
|
+
}
|
|
10923
|
+
if (metrics.canvasOperations / metrics.renderCalls > 1e3) {
|
|
10924
|
+
recommendations.push("High canvas operations per render - consider batching or caching");
|
|
10925
|
+
}
|
|
10926
|
+
if (recommendations.length === 0) {
|
|
10927
|
+
recommendations.push("Performance targets are being met - no optimizations needed");
|
|
10928
|
+
}
|
|
10929
|
+
return recommendations;
|
|
10930
|
+
}
|
|
10931
|
+
monitorFrame = () => {
|
|
10932
|
+
if (!this.isMonitoring) return;
|
|
10933
|
+
const currentTime = performance.now();
|
|
10934
|
+
const frameTime = currentTime - this.lastFrameTime;
|
|
10935
|
+
this.frameTimes.push(frameTime);
|
|
10936
|
+
if (this.frameTimes.length > this.maxSamples) {
|
|
10937
|
+
this.frameTimes.shift();
|
|
10938
|
+
}
|
|
10939
|
+
const fps = frameTime > 0 ? 1e3 / frameTime : 0;
|
|
10940
|
+
this.frames.push(fps);
|
|
10941
|
+
if (this.frames.length > this.maxSamples) {
|
|
10942
|
+
this.frames.shift();
|
|
10943
|
+
}
|
|
10944
|
+
if (frameTime > this.targets.maxFrameTime * 2) {
|
|
10945
|
+
this.droppedFrameCount++;
|
|
10946
|
+
}
|
|
10947
|
+
this.lastFrameTime = currentTime;
|
|
10948
|
+
this.animationFrameId = requestAnimationFrame(this.monitorFrame);
|
|
10949
|
+
};
|
|
10950
|
+
calculateCurrentFps() {
|
|
10951
|
+
if (this.frames.length === 0) return 0;
|
|
10952
|
+
return this.frames[this.frames.length - 1] || 0;
|
|
10953
|
+
}
|
|
10954
|
+
calculateAverageFps() {
|
|
10955
|
+
if (this.frames.length === 0) return 0;
|
|
10956
|
+
const sum2 = this.frames.reduce((a2, b) => a2 + b, 0);
|
|
10957
|
+
return sum2 / this.frames.length;
|
|
10958
|
+
}
|
|
10959
|
+
calculateCurrentFrameTime() {
|
|
10960
|
+
if (this.frameTimes.length === 0) return 0;
|
|
10961
|
+
return this.frameTimes[this.frameTimes.length - 1] || 0;
|
|
10962
|
+
}
|
|
10963
|
+
calculateAverageFrameTime() {
|
|
10964
|
+
if (this.frameTimes.length === 0) return 0;
|
|
10965
|
+
const sum2 = this.frameTimes.reduce((a2, b) => a2 + b, 0);
|
|
10966
|
+
return sum2 / this.frameTimes.length;
|
|
10967
|
+
}
|
|
10968
|
+
calculateEfficiency(fps, frameTime) {
|
|
10969
|
+
const fpsEfficiency = Math.min(fps / this.targets.targetFps, 1);
|
|
10970
|
+
const frameTimeEfficiency = Math.min(this.targets.maxFrameTime / frameTime, 1);
|
|
10971
|
+
return fpsEfficiency * 0.7 + frameTimeEfficiency * 0.3;
|
|
10972
|
+
}
|
|
10973
|
+
getMemoryUsage() {
|
|
10974
|
+
if (typeof performance.memory !== "undefined") {
|
|
10975
|
+
return performance.memory.usedJSHeapSize / 1024 / 1024;
|
|
10976
|
+
}
|
|
10977
|
+
return 0;
|
|
10978
|
+
}
|
|
10979
|
+
};
|
|
10980
|
+
var rendering_performance_monitor_default = RenderingPerformanceMonitor;
|
|
10981
|
+
|
|
10982
|
+
// src/force-graph-wrapper/core/canvas-optimizer.ts
|
|
10983
|
+
var CanvasOptimizer = class {
|
|
10984
|
+
canvas = null;
|
|
10985
|
+
context = null;
|
|
10986
|
+
imageDataCache = /* @__PURE__ */ new Map();
|
|
10987
|
+
pathCache = /* @__PURE__ */ new Map();
|
|
10988
|
+
renderQueue = [];
|
|
10989
|
+
isOptimizing = false;
|
|
10990
|
+
settings = {
|
|
10991
|
+
enableBatching: true,
|
|
10992
|
+
enableCaching: true,
|
|
10993
|
+
enableLOD: true,
|
|
10994
|
+
lodThreshold: 500,
|
|
10995
|
+
// Start LOD optimizations at 500+ nodes
|
|
10996
|
+
cacheSize: 100,
|
|
10997
|
+
batchSize: 50,
|
|
10998
|
+
cullingEnabled: true,
|
|
10999
|
+
cullingMargin: 100
|
|
11000
|
+
// pixels outside viewport to still render
|
|
11001
|
+
};
|
|
11002
|
+
viewport = {
|
|
11003
|
+
x: 0,
|
|
11004
|
+
y: 0,
|
|
11005
|
+
width: 800,
|
|
11006
|
+
height: 600,
|
|
11007
|
+
scale: 1
|
|
11008
|
+
};
|
|
11009
|
+
constructor(settings) {
|
|
11010
|
+
if (settings) {
|
|
11011
|
+
this.settings = { ...this.settings, ...settings };
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* Initialize optimizer with canvas element
|
|
11016
|
+
*/
|
|
11017
|
+
initialize(canvas) {
|
|
11018
|
+
this.canvas = canvas;
|
|
11019
|
+
this.context = canvas.getContext("2d");
|
|
11020
|
+
this.updateViewport();
|
|
11021
|
+
}
|
|
11022
|
+
/**
|
|
11023
|
+
* Clean up all cached data and references
|
|
11024
|
+
*/
|
|
11025
|
+
destroy() {
|
|
11026
|
+
this.imageDataCache.clear();
|
|
11027
|
+
this.pathCache.clear();
|
|
11028
|
+
this.renderQueue.length = 0;
|
|
11029
|
+
this.canvas = null;
|
|
11030
|
+
this.context = null;
|
|
11031
|
+
this.isOptimizing = false;
|
|
11032
|
+
}
|
|
11033
|
+
/**
|
|
11034
|
+
* Update viewport information for culling calculations
|
|
11035
|
+
*/
|
|
11036
|
+
updateViewport(x3, y3, scale) {
|
|
11037
|
+
if (!this.canvas) return;
|
|
11038
|
+
this.viewport = {
|
|
11039
|
+
x: x3 ?? this.viewport.x,
|
|
11040
|
+
y: y3 ?? this.viewport.y,
|
|
11041
|
+
width: this.canvas.width,
|
|
11042
|
+
height: this.canvas.height,
|
|
11043
|
+
scale: scale ?? this.viewport.scale
|
|
11044
|
+
};
|
|
11045
|
+
}
|
|
11046
|
+
/**
|
|
11047
|
+
* Optimize node rendering based on current settings and viewport
|
|
11048
|
+
*/
|
|
11049
|
+
optimizeNodeRendering(nodes, renderFunction) {
|
|
11050
|
+
if (!this.context || !this.settings.enableLOD) {
|
|
11051
|
+
nodes.forEach((node) => renderFunction(node, 0));
|
|
11052
|
+
return;
|
|
11053
|
+
}
|
|
11054
|
+
const nodeCount = nodes.length;
|
|
11055
|
+
const shouldUseLOD = nodeCount > this.settings.lodThreshold;
|
|
11056
|
+
const lodLevel = shouldUseLOD ? this.calculateLODLevel(nodeCount, this.viewport.scale) : 0;
|
|
11057
|
+
if (this.settings.enableBatching) {
|
|
11058
|
+
this.batchRender(nodes, (node) => renderFunction(node, lodLevel));
|
|
11059
|
+
} else {
|
|
11060
|
+
const visibleNodes = this.settings.cullingEnabled ? this.cullNodes(nodes) : nodes;
|
|
11061
|
+
visibleNodes.forEach((node) => renderFunction(node, lodLevel));
|
|
11062
|
+
}
|
|
11063
|
+
}
|
|
11064
|
+
/**
|
|
11065
|
+
* Optimize link rendering with batching and culling
|
|
11066
|
+
*/
|
|
11067
|
+
optimizeLinkRendering(links, renderFunction) {
|
|
11068
|
+
if (!this.context) {
|
|
11069
|
+
links.forEach((link) => renderFunction(link, 0));
|
|
11070
|
+
return;
|
|
11071
|
+
}
|
|
11072
|
+
const linkCount = links.length;
|
|
11073
|
+
const lodLevel = linkCount > this.settings.lodThreshold ? this.calculateLODLevel(linkCount, this.viewport.scale) : 0;
|
|
11074
|
+
if (this.settings.enableBatching) {
|
|
11075
|
+
this.batchRender(links, (link) => renderFunction(link, lodLevel));
|
|
11076
|
+
} else {
|
|
11077
|
+
const visibleLinks = this.settings.cullingEnabled ? this.cullLinks(links) : links;
|
|
11078
|
+
visibleLinks.forEach((link) => renderFunction(link, lodLevel));
|
|
11079
|
+
}
|
|
11080
|
+
}
|
|
11081
|
+
/**
|
|
11082
|
+
* Cache commonly used drawing operations
|
|
11083
|
+
*/
|
|
11084
|
+
getCachedPath(key, createPath) {
|
|
11085
|
+
if (!this.settings.enableCaching) {
|
|
11086
|
+
return createPath();
|
|
11087
|
+
}
|
|
11088
|
+
if (this.pathCache.has(key)) {
|
|
11089
|
+
return this.pathCache.get(key);
|
|
11090
|
+
}
|
|
11091
|
+
const path = createPath();
|
|
11092
|
+
if (this.pathCache.size >= this.settings.cacheSize) {
|
|
11093
|
+
const firstKey = this.pathCache.keys().next().value;
|
|
11094
|
+
if (firstKey !== void 0) {
|
|
11095
|
+
this.pathCache.delete(firstKey);
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
11098
|
+
this.pathCache.set(key, path);
|
|
11099
|
+
return path;
|
|
11100
|
+
}
|
|
11101
|
+
/**
|
|
11102
|
+
* Cache image data for complex shapes or textures
|
|
11103
|
+
*/
|
|
11104
|
+
getCachedImageData(key, createImageData) {
|
|
11105
|
+
if (!this.settings.enableCaching) {
|
|
11106
|
+
return createImageData();
|
|
11107
|
+
}
|
|
11108
|
+
if (this.imageDataCache.has(key)) {
|
|
11109
|
+
return this.imageDataCache.get(key);
|
|
11110
|
+
}
|
|
11111
|
+
const imageData = createImageData();
|
|
11112
|
+
if (this.imageDataCache.size >= this.settings.cacheSize) {
|
|
11113
|
+
const firstKey = this.imageDataCache.keys().next().value;
|
|
11114
|
+
if (firstKey !== void 0) {
|
|
11115
|
+
this.imageDataCache.delete(firstKey);
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
this.imageDataCache.set(key, imageData);
|
|
11119
|
+
return imageData;
|
|
11120
|
+
}
|
|
11121
|
+
/**
|
|
11122
|
+
* Batch render operations for better performance
|
|
11123
|
+
*/
|
|
11124
|
+
batchRender(items, renderFunction) {
|
|
11125
|
+
if (!this.context) return;
|
|
11126
|
+
for (let i = 0; i < items.length; i += this.settings.batchSize) {
|
|
11127
|
+
const batch = items.slice(i, i + this.settings.batchSize);
|
|
11128
|
+
const visibleItems = this.settings.cullingEnabled ? batch.filter((item) => this.isItemVisible(item)) : batch;
|
|
11129
|
+
this.context.save();
|
|
11130
|
+
visibleItems.forEach(renderFunction);
|
|
11131
|
+
this.context.restore();
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
11134
|
+
/**
|
|
11135
|
+
* Calculate appropriate LOD level based on node count and zoom
|
|
11136
|
+
*/
|
|
11137
|
+
calculateLODLevel(itemCount, scale) {
|
|
11138
|
+
if (itemCount < this.settings.lodThreshold) return 0;
|
|
11139
|
+
if (scale > 2) return 0;
|
|
11140
|
+
if (scale > 1) return 1;
|
|
11141
|
+
if (itemCount < 1e3) return 1;
|
|
11142
|
+
if (itemCount < 2e3) return 2;
|
|
11143
|
+
return 3;
|
|
11144
|
+
}
|
|
11145
|
+
/**
|
|
11146
|
+
* Cull nodes outside the visible viewport
|
|
11147
|
+
*/
|
|
11148
|
+
cullNodes(nodes) {
|
|
11149
|
+
return nodes.filter((node) => this.isNodeVisible(node));
|
|
11150
|
+
}
|
|
11151
|
+
/**
|
|
11152
|
+
* Cull links outside the visible viewport
|
|
11153
|
+
*/
|
|
11154
|
+
cullLinks(links) {
|
|
11155
|
+
return links.filter((link) => this.isLinkVisible(link));
|
|
11156
|
+
}
|
|
11157
|
+
/**
|
|
11158
|
+
* Check if a node is visible in the current viewport
|
|
11159
|
+
*/
|
|
11160
|
+
isNodeVisible(node) {
|
|
11161
|
+
if (!node.x || !node.y) return true;
|
|
11162
|
+
const margin = this.settings.cullingMargin;
|
|
11163
|
+
const { x: x3, y: y3, width, height, scale } = this.viewport;
|
|
11164
|
+
const worldX = (node.x - x3) * scale;
|
|
11165
|
+
const worldY = (node.y - y3) * scale;
|
|
11166
|
+
return worldX >= -margin && worldX <= width + margin && worldY >= -margin && worldY <= height + margin;
|
|
11167
|
+
}
|
|
11168
|
+
/**
|
|
11169
|
+
* Check if a link is visible in the current viewport
|
|
11170
|
+
*/
|
|
11171
|
+
isLinkVisible(link) {
|
|
11172
|
+
const source = link.source;
|
|
11173
|
+
const target = link.target;
|
|
11174
|
+
if (!source || !target) return true;
|
|
11175
|
+
return this.isNodeVisible(source) || this.isNodeVisible(target) || this.lineIntersectsViewport(source, target);
|
|
11176
|
+
}
|
|
11177
|
+
/**
|
|
11178
|
+
* Generic item visibility check
|
|
11179
|
+
*/
|
|
11180
|
+
isItemVisible(item) {
|
|
11181
|
+
const itemWithXY = item;
|
|
11182
|
+
const itemWithSourceTarget = item;
|
|
11183
|
+
if (itemWithXY.x !== void 0 && itemWithXY.y !== void 0) {
|
|
11184
|
+
return this.isNodeVisible(itemWithXY);
|
|
11185
|
+
}
|
|
11186
|
+
if (itemWithSourceTarget.source && itemWithSourceTarget.target) {
|
|
11187
|
+
return this.isLinkVisible(itemWithSourceTarget);
|
|
11188
|
+
}
|
|
11189
|
+
return true;
|
|
11190
|
+
}
|
|
11191
|
+
/**
|
|
11192
|
+
* Check if a line intersects with the viewport
|
|
11193
|
+
*/
|
|
11194
|
+
lineIntersectsViewport(source, target) {
|
|
11195
|
+
const { x: x3, y: y3, width, height } = this.viewport;
|
|
11196
|
+
const margin = this.settings.cullingMargin;
|
|
11197
|
+
if (source.x === void 0 || source.y === void 0 || target.x === void 0 || target.y === void 0) {
|
|
11198
|
+
return true;
|
|
11199
|
+
}
|
|
11200
|
+
const minX = Math.min(source.x, target.x);
|
|
11201
|
+
const maxX = Math.max(source.x, target.x);
|
|
11202
|
+
const minY = Math.min(source.y, target.y);
|
|
11203
|
+
const maxY = Math.max(source.y, target.y);
|
|
11204
|
+
return !(maxX < x3 - margin || minX > x3 + width + margin || maxY < y3 - margin || minY > y3 + height + margin);
|
|
11205
|
+
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Clear all caches
|
|
11208
|
+
*/
|
|
11209
|
+
clearCaches() {
|
|
11210
|
+
this.imageDataCache.clear();
|
|
11211
|
+
this.pathCache.clear();
|
|
11212
|
+
}
|
|
11213
|
+
/**
|
|
11214
|
+
* Get cache statistics
|
|
11215
|
+
*/
|
|
11216
|
+
getCacheStats() {
|
|
11217
|
+
return {
|
|
11218
|
+
pathCacheSize: this.pathCache.size,
|
|
11219
|
+
imageCacheSize: this.imageDataCache.size,
|
|
11220
|
+
pathCacheHitRate: 0,
|
|
11221
|
+
// Would need hit/miss tracking
|
|
11222
|
+
imageCacheHitRate: 0
|
|
11223
|
+
// Would need hit/miss tracking
|
|
11224
|
+
};
|
|
11225
|
+
}
|
|
11226
|
+
/**
|
|
11227
|
+
* Update optimization settings
|
|
11228
|
+
*/
|
|
11229
|
+
updateSettings(newSettings) {
|
|
11230
|
+
this.settings = { ...this.settings, ...newSettings };
|
|
11231
|
+
}
|
|
11232
|
+
/**
|
|
11233
|
+
* Get current optimization settings
|
|
11234
|
+
*/
|
|
11235
|
+
getSettings() {
|
|
11236
|
+
return { ...this.settings };
|
|
11237
|
+
}
|
|
11238
|
+
};
|
|
11239
|
+
var canvas_optimizer_default = CanvasOptimizer;
|
|
11240
|
+
|
|
11241
|
+
// src/force-graph-wrapper/core/force-graph-wrapper.ts
|
|
11242
|
+
var ForceGraphWrapper = class {
|
|
11243
|
+
// Core force-graph instance (strategic use of 'unknown' for library integration)
|
|
11244
|
+
forceGraph = null;
|
|
11245
|
+
// Container element
|
|
11246
|
+
container;
|
|
11247
|
+
// Performance monitor
|
|
11248
|
+
performanceMonitor;
|
|
11249
|
+
// Step 4: Advanced rendering performance monitoring
|
|
11250
|
+
renderingMonitor = null;
|
|
11251
|
+
canvasOptimizer = null;
|
|
11252
|
+
// Configuration
|
|
11253
|
+
config;
|
|
11254
|
+
// State tracking
|
|
11255
|
+
isInitialized = false;
|
|
11256
|
+
isDestroyed = false;
|
|
11257
|
+
isRenderingOptimized = false;
|
|
11258
|
+
enableVerboseLogging = false;
|
|
11259
|
+
// Disable by default for performance
|
|
11260
|
+
// Page visibility handling for deterministic layouts
|
|
11261
|
+
wasInitializedHidden = false;
|
|
11262
|
+
visibilityChangeHandler = null;
|
|
11263
|
+
// Element viewport visibility handling for deterministic physics
|
|
11264
|
+
wasInitializedOutOfView = false;
|
|
11265
|
+
intersectionObserver = null;
|
|
11266
|
+
// Web Worker physics for all graphs (deterministic layouts)
|
|
11267
|
+
webWorkerPhysicsCompleted = false;
|
|
11268
|
+
physicsTimeoutId = void 0;
|
|
11269
|
+
// Cached container dimensions when visible
|
|
11270
|
+
lastKnownDimensions = null;
|
|
11271
|
+
// Deferred fitView parameters for library-level handling
|
|
11272
|
+
deferredFitViewParams = null;
|
|
11273
|
+
pendingFitView = false;
|
|
11274
|
+
// Performance testing interval tracking for cleanup
|
|
11275
|
+
performanceTestInterval = null;
|
|
11276
|
+
// Canvas event listener for cleanup
|
|
11277
|
+
canvasClickHandler = null;
|
|
11278
|
+
// Export animation timeout for cleanup
|
|
11279
|
+
exportTimeoutId = null;
|
|
11280
|
+
// Visibility change timeouts for cleanup
|
|
11281
|
+
visibilityTimeoutId = null;
|
|
11282
|
+
intersectionTimeoutId = null;
|
|
11283
|
+
kapsuleTimeoutId = null;
|
|
11284
|
+
// Graph controls UI
|
|
11285
|
+
controlsInstance = null;
|
|
11286
|
+
// Graph legends UI
|
|
11287
|
+
legendsInstance = null;
|
|
11288
|
+
/**
|
|
11289
|
+
* Helper method to safely cast and call methods on the force graph instance
|
|
11290
|
+
*/
|
|
11291
|
+
getGraphInstance() {
|
|
11292
|
+
return this.forceGraph;
|
|
11293
|
+
}
|
|
11294
|
+
/**
|
|
11295
|
+
* Constructor - Step 1 Performance Test
|
|
11296
|
+
* Target: < 1ms creation time
|
|
11297
|
+
*/
|
|
11298
|
+
constructor(container, config) {
|
|
11299
|
+
const creationStart = performance.now();
|
|
11300
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
11301
|
+
throw new Error("Invalid container: must be an HTMLElement");
|
|
11302
|
+
}
|
|
11303
|
+
this.container = container;
|
|
11304
|
+
this.performanceMonitor = new PerformanceMonitor();
|
|
11305
|
+
if (config?.enablePerformanceMonitoring) {
|
|
11306
|
+
this.renderingMonitor = new rendering_performance_monitor_default({
|
|
11307
|
+
targetFps: 60,
|
|
11308
|
+
maxFrameTime: 16.67,
|
|
11309
|
+
maxDroppedFrames: 5,
|
|
11310
|
+
efficiencyThreshold: 0.8
|
|
11311
|
+
});
|
|
11312
|
+
this.canvasOptimizer = new canvas_optimizer_default({
|
|
11313
|
+
enableBatching: false,
|
|
11314
|
+
// Keep it simple like react-force-graph
|
|
11315
|
+
enableCaching: false,
|
|
11316
|
+
enableLOD: false,
|
|
11317
|
+
lodThreshold: 500,
|
|
11318
|
+
cullingEnabled: false
|
|
11319
|
+
});
|
|
11320
|
+
}
|
|
11321
|
+
this.config = {
|
|
11322
|
+
container,
|
|
11323
|
+
width: container.clientWidth || 400,
|
|
11324
|
+
height: container.clientHeight || 300,
|
|
11325
|
+
backgroundColor: "#ffffff",
|
|
11326
|
+
enablePerformanceMonitoring: false,
|
|
11327
|
+
// Disable by default for better performance
|
|
11328
|
+
controls: {
|
|
11329
|
+
enabled: true,
|
|
11330
|
+
position: "bottom-left",
|
|
11331
|
+
orientation: "vertical"
|
|
11332
|
+
},
|
|
11333
|
+
legends: {
|
|
11334
|
+
enabled: true,
|
|
11335
|
+
position: "top-right",
|
|
11336
|
+
maxItems: 10
|
|
11337
|
+
},
|
|
11338
|
+
...config
|
|
11339
|
+
};
|
|
11340
|
+
this.enableVerboseLogging = config?.enablePerformanceMonitoring === true && config?.performanceTargets?.methodCall !== void 0;
|
|
11341
|
+
this.setupPageVisibilityHandling();
|
|
11342
|
+
this.setupDeterministicPhysics();
|
|
11343
|
+
const creationTime = performance.now() - creationStart;
|
|
11344
|
+
this.performanceMonitor.setMetric("creation", creationTime);
|
|
11345
|
+
if (this.enableVerboseLogging) {
|
|
11346
|
+
this.validateCreationPerformance(creationTime);
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
/**
|
|
11350
|
+
* Step 1 Performance Validation
|
|
11351
|
+
*/
|
|
11352
|
+
validateCreationPerformance(_creationTime) {
|
|
11353
|
+
}
|
|
11354
|
+
/**
|
|
11355
|
+
* Setup automatic page visibility handling for deterministic layouts
|
|
11356
|
+
* Ensures consistent graph layouts regardless of tab visibility during initialization
|
|
11357
|
+
*/
|
|
11358
|
+
setupPageVisibilityHandling() {
|
|
11359
|
+
if (this.config.handlePageVisibility === false) {
|
|
11360
|
+
return;
|
|
11361
|
+
}
|
|
11362
|
+
this.wasInitializedHidden = document.hidden;
|
|
11363
|
+
this.visibilityChangeHandler = () => {
|
|
11364
|
+
if (!document.hidden && this.isInitialized) {
|
|
11365
|
+
if (this.wasInitializedHidden) {
|
|
11366
|
+
this.getGraphInstance().d3ReheatSimulation();
|
|
11367
|
+
this.wasInitializedHidden = false;
|
|
11368
|
+
}
|
|
11369
|
+
if (this.pendingFitView) {
|
|
11370
|
+
if (this.visibilityTimeoutId) {
|
|
11371
|
+
clearTimeout(this.visibilityTimeoutId);
|
|
11372
|
+
}
|
|
11373
|
+
this.visibilityTimeoutId = setTimeout(() => {
|
|
11374
|
+
this.zoomToFit(40, 300);
|
|
11375
|
+
this.visibilityTimeoutId = null;
|
|
11376
|
+
}, 100);
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11379
|
+
};
|
|
11380
|
+
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
11381
|
+
}
|
|
11382
|
+
/**
|
|
11383
|
+
* Cleanup page visibility handling
|
|
11384
|
+
*/
|
|
11385
|
+
cleanupPageVisibilityHandling() {
|
|
11386
|
+
if (this.visibilityChangeHandler) {
|
|
11387
|
+
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
11388
|
+
this.visibilityChangeHandler = null;
|
|
11389
|
+
}
|
|
11390
|
+
}
|
|
11391
|
+
/**
|
|
11392
|
+
* Setup deterministic physics handling for viewport visibility
|
|
11393
|
+
* Ensures consistent physics timing regardless of element visibility
|
|
11394
|
+
*/
|
|
11395
|
+
setupDeterministicPhysics() {
|
|
11396
|
+
if (this.config.deterministicLayout === false) {
|
|
11397
|
+
return;
|
|
11398
|
+
}
|
|
11399
|
+
const isInViewport = this.isElementInViewport(this.container);
|
|
11400
|
+
this.wasInitializedOutOfView = !isInViewport;
|
|
11401
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
11402
|
+
const scrollableParent = this.findScrollableParent(this.container);
|
|
11403
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
11404
|
+
entries.forEach((entry) => {
|
|
11405
|
+
if (entry.isIntersecting && this.isInitialized) {
|
|
11406
|
+
if (this.wasInitializedOutOfView) {
|
|
11407
|
+
if (this.physicsTimeoutId) {
|
|
11408
|
+
window.clearTimeout(this.physicsTimeoutId);
|
|
11409
|
+
this.physicsTimeoutId = void 0;
|
|
11410
|
+
}
|
|
11411
|
+
if (!this.webWorkerPhysicsCompleted) {
|
|
11412
|
+
this.runWebWorkerPhysics();
|
|
11413
|
+
}
|
|
11414
|
+
this.wasInitializedOutOfView = false;
|
|
11415
|
+
}
|
|
11416
|
+
if (this.pendingFitView) {
|
|
11417
|
+
if (this.intersectionTimeoutId) {
|
|
11418
|
+
clearTimeout(this.intersectionTimeoutId);
|
|
11419
|
+
}
|
|
11420
|
+
this.intersectionTimeoutId = setTimeout(() => {
|
|
11421
|
+
if (this.pendingFitView) {
|
|
11422
|
+
this.zoomToFit(40, 300);
|
|
11423
|
+
}
|
|
11424
|
+
this.intersectionTimeoutId = null;
|
|
11425
|
+
}, 100);
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
});
|
|
11429
|
+
}, {
|
|
11430
|
+
root: scrollableParent,
|
|
11431
|
+
// Use found scrollable parent as root
|
|
11432
|
+
threshold: 0.1,
|
|
11433
|
+
// Trigger when 10% visible
|
|
11434
|
+
rootMargin: "100px"
|
|
11435
|
+
// Larger margin like Angular component
|
|
11436
|
+
});
|
|
11437
|
+
this.intersectionObserver.observe(this.container);
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
/**
|
|
11441
|
+
* Find the scrollable parent container for intersection observer
|
|
11442
|
+
* Similar to Angular component approach but more generic
|
|
11443
|
+
*/
|
|
11444
|
+
findScrollableParent(element) {
|
|
11445
|
+
let current = element.parentElement;
|
|
11446
|
+
while (current && current !== document.body) {
|
|
11447
|
+
const style = window.getComputedStyle(current);
|
|
11448
|
+
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
11449
|
+
if (/(auto|scroll)/.test(overflow) && (current.scrollHeight > current.clientHeight || current.scrollWidth > current.clientWidth)) {
|
|
11450
|
+
return current;
|
|
11451
|
+
}
|
|
11452
|
+
current = current.parentElement;
|
|
11453
|
+
}
|
|
11454
|
+
return null;
|
|
11455
|
+
}
|
|
11456
|
+
/**
|
|
11457
|
+
* Setup deferred fitView mechanism using IntersectionObserver
|
|
11458
|
+
* This handles library-level viewport-aware fitView
|
|
11459
|
+
*/
|
|
11460
|
+
setupDeferredFitView() {
|
|
11461
|
+
if (this.intersectionObserver || !this.container || !this.deferredFitViewParams) {
|
|
11462
|
+
return;
|
|
11463
|
+
}
|
|
11464
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
11465
|
+
entries.forEach((entry) => {
|
|
11466
|
+
if (entry.isIntersecting && this.pendingFitView && this.deferredFitViewParams) {
|
|
11467
|
+
const { padding, duration } = this.deferredFitViewParams;
|
|
11468
|
+
this.pendingFitView = false;
|
|
11469
|
+
this.deferredFitViewParams = null;
|
|
11470
|
+
if (this.intersectionObserver) {
|
|
11471
|
+
this.intersectionObserver.disconnect();
|
|
11472
|
+
this.intersectionObserver = null;
|
|
11473
|
+
}
|
|
11474
|
+
requestAnimationFrame(() => {
|
|
11475
|
+
this.executeFitViewNow(padding, duration);
|
|
11476
|
+
});
|
|
11477
|
+
}
|
|
11478
|
+
});
|
|
11479
|
+
}, {
|
|
11480
|
+
threshold: 0.1,
|
|
11481
|
+
rootMargin: "50px"
|
|
11482
|
+
// Start checking slightly before fully visible
|
|
11483
|
+
});
|
|
11484
|
+
this.intersectionObserver.observe(this.container);
|
|
11485
|
+
}
|
|
11486
|
+
/**
|
|
11487
|
+
* Execute fitView immediately (bypass visibility checks)
|
|
11488
|
+
*/
|
|
11489
|
+
executeFitViewNow(padding, duration) {
|
|
11490
|
+
if (!this.forceGraph || !this.container) return;
|
|
11491
|
+
const containerWidth = this.container.clientWidth;
|
|
11492
|
+
const containerHeight = this.container.clientHeight;
|
|
11493
|
+
if (containerWidth === 0 || containerHeight === 0) {
|
|
11494
|
+
return;
|
|
11495
|
+
}
|
|
11496
|
+
this.cacheContainerDimensions();
|
|
11497
|
+
const bbox = this.getGraphInstance().getGraphBbox();
|
|
11498
|
+
const width = bbox.x[1] - bbox.x[0];
|
|
11499
|
+
const height = bbox.y[1] - bbox.y[0];
|
|
11500
|
+
const centerX = (bbox.x[0] + bbox.x[1]) / 2;
|
|
11501
|
+
const centerY = (bbox.y[0] + bbox.y[1]) / 2;
|
|
11502
|
+
const viewportWidth = containerWidth - padding * 2;
|
|
11503
|
+
const viewportHeight = containerHeight - padding * 2;
|
|
11504
|
+
if (width > 0 && height > 0) {
|
|
11505
|
+
const scale = Math.min(viewportWidth / width, viewportHeight / height);
|
|
11506
|
+
this.getGraphInstance().centerAt(centerX, centerY, duration);
|
|
11507
|
+
this.getGraphInstance().zoom(scale, duration);
|
|
11508
|
+
}
|
|
11509
|
+
}
|
|
11510
|
+
/**
|
|
11511
|
+
* Cleanup deferred fitView intersection observer
|
|
11512
|
+
*/
|
|
11513
|
+
cleanupDeferredFitView() {
|
|
11514
|
+
if (this.intersectionObserver) {
|
|
11515
|
+
this.intersectionObserver.disconnect();
|
|
11516
|
+
this.intersectionObserver = null;
|
|
11517
|
+
}
|
|
11518
|
+
this.deferredFitViewParams = null;
|
|
11519
|
+
this.pendingFitView = false;
|
|
11520
|
+
}
|
|
11521
|
+
/**
|
|
11522
|
+
* Cleanup performance test interval
|
|
11523
|
+
*/
|
|
11524
|
+
cleanupPerformanceTest() {
|
|
11525
|
+
if (this.performanceTestInterval) {
|
|
11526
|
+
clearInterval(this.performanceTestInterval);
|
|
11527
|
+
this.performanceTestInterval = null;
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
/**
|
|
11531
|
+
* Cleanup export animation timeout
|
|
11532
|
+
*/
|
|
11533
|
+
cleanupExportTimeout() {
|
|
11534
|
+
if (this.exportTimeoutId) {
|
|
11535
|
+
clearTimeout(this.exportTimeoutId);
|
|
11536
|
+
this.exportTimeoutId = null;
|
|
11537
|
+
}
|
|
11538
|
+
}
|
|
11539
|
+
/**
|
|
11540
|
+
* Cleanup visibility-related timeouts
|
|
11541
|
+
*/
|
|
11542
|
+
cleanupVisibilityTimeouts() {
|
|
11543
|
+
if (this.visibilityTimeoutId) {
|
|
11544
|
+
clearTimeout(this.visibilityTimeoutId);
|
|
11545
|
+
this.visibilityTimeoutId = null;
|
|
11546
|
+
}
|
|
11547
|
+
if (this.intersectionTimeoutId) {
|
|
11548
|
+
clearTimeout(this.intersectionTimeoutId);
|
|
11549
|
+
this.intersectionTimeoutId = null;
|
|
11550
|
+
}
|
|
11551
|
+
if (this.kapsuleTimeoutId) {
|
|
11552
|
+
clearTimeout(this.kapsuleTimeoutId);
|
|
11553
|
+
this.kapsuleTimeoutId = null;
|
|
11554
|
+
}
|
|
11555
|
+
}
|
|
11556
|
+
/**
|
|
11557
|
+
* Cleanup canvas event listeners
|
|
11558
|
+
*/
|
|
11559
|
+
cleanupCanvasEventListeners() {
|
|
11560
|
+
if (this.canvasClickHandler && this.container) {
|
|
11561
|
+
const canvas = this.container.querySelector("canvas");
|
|
11562
|
+
if (canvas) {
|
|
11563
|
+
canvas.removeEventListener("click", this.canvasClickHandler);
|
|
11564
|
+
}
|
|
11565
|
+
this.canvasClickHandler = null;
|
|
11566
|
+
}
|
|
11567
|
+
}
|
|
11568
|
+
/**
|
|
11569
|
+
* Check if element is currently visible (even partially) in viewport
|
|
11570
|
+
*/
|
|
11571
|
+
isElementInViewport(element) {
|
|
11572
|
+
const rect = element.getBoundingClientRect();
|
|
11573
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
11574
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
11575
|
+
return rect.bottom > 0 && // Not above viewport
|
|
11576
|
+
rect.right > 0 && // Not to the left of viewport
|
|
11577
|
+
rect.top < windowHeight && // Not below viewport
|
|
11578
|
+
rect.left < windowWidth;
|
|
11579
|
+
}
|
|
11580
|
+
/**
|
|
11581
|
+
* Cache container dimensions when they're valid (non-zero)
|
|
11582
|
+
*/
|
|
11583
|
+
cacheContainerDimensions() {
|
|
11584
|
+
const width = this.container.clientWidth;
|
|
11585
|
+
const height = this.container.clientHeight;
|
|
11586
|
+
if (width > 0 && height > 0) {
|
|
11587
|
+
this.lastKnownDimensions = { width, height };
|
|
11588
|
+
}
|
|
11589
|
+
}
|
|
11590
|
+
/**
|
|
11591
|
+
* Check if container has valid dimensions and cache them
|
|
11592
|
+
*/
|
|
11593
|
+
hasValidDimensions() {
|
|
11594
|
+
const width = this.container.clientWidth;
|
|
11595
|
+
const height = this.container.clientHeight;
|
|
11596
|
+
if (width > 0 && height > 0) {
|
|
11597
|
+
this.cacheContainerDimensions();
|
|
11598
|
+
return true;
|
|
11599
|
+
}
|
|
11600
|
+
return false;
|
|
11601
|
+
}
|
|
11602
|
+
/**
|
|
11603
|
+
* Run deterministic physics using Web Worker for off-screen graphs only
|
|
11604
|
+
*/
|
|
11605
|
+
async runWebWorkerPhysics() {
|
|
11606
|
+
if (!this.config.graphData || !this.container || this.webWorkerPhysicsCompleted) return;
|
|
11607
|
+
const { nodes, links } = this.config.graphData;
|
|
11608
|
+
if (!nodes || nodes.length === 0) return;
|
|
11609
|
+
if (this.physicsTimeoutId) {
|
|
11610
|
+
window.clearTimeout(this.physicsTimeoutId);
|
|
11611
|
+
this.physicsTimeoutId = void 0;
|
|
11612
|
+
}
|
|
11613
|
+
try {
|
|
11614
|
+
if (!physicsWorkerManager.isAvailable()) {
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
const containerWidth = this.container.clientWidth || this.config.width || 800;
|
|
11618
|
+
const containerHeight = this.container.clientHeight || this.config.height || 600;
|
|
11619
|
+
const config = {
|
|
11620
|
+
nodes: nodes.map((node) => ({ ...node })),
|
|
11621
|
+
links: links ? links.map((link) => ({ ...link })) : [],
|
|
11622
|
+
width: containerWidth,
|
|
11623
|
+
height: containerHeight,
|
|
11624
|
+
maxTicks: 300,
|
|
11625
|
+
// Deterministic tick count for off-screen graphs
|
|
11626
|
+
alphaDecay: this.config.d3AlphaDecay || 0.0228,
|
|
11627
|
+
// Use wrapper config or default
|
|
11628
|
+
velocityDecay: this.config.d3VelocityDecay || 0.4,
|
|
11629
|
+
// Use wrapper config or default
|
|
11630
|
+
forces: {}
|
|
11631
|
+
// Let worker use force-graph defaults
|
|
11632
|
+
};
|
|
11633
|
+
const initialized = await physicsWorkerManager.initialize(config);
|
|
11634
|
+
if (!initialized) {
|
|
11635
|
+
throw new Error("Failed to initialize Web Worker physics");
|
|
11636
|
+
}
|
|
11637
|
+
const finalNodes = await physicsWorkerManager.runSimulation();
|
|
11638
|
+
this.applyWebWorkerResults(finalNodes);
|
|
11639
|
+
this.webWorkerPhysicsCompleted = true;
|
|
11640
|
+
} catch {
|
|
11641
|
+
}
|
|
11642
|
+
}
|
|
11643
|
+
/**
|
|
11644
|
+
* Apply Web Worker results to force-graph instance WITHOUT disrupting interactions
|
|
11645
|
+
*/
|
|
11646
|
+
applyWebWorkerResults(physicsNodes) {
|
|
11647
|
+
if (!this.forceGraph || !this.config.graphData?.nodes) return;
|
|
11648
|
+
const nodeMap = new Map(physicsNodes.map((node) => [node.id, node]));
|
|
11649
|
+
this.config.graphData.nodes.forEach((node) => {
|
|
11650
|
+
const physicsNode = nodeMap.get(node.id);
|
|
11651
|
+
if (physicsNode) {
|
|
11652
|
+
if (typeof node.fx === "undefined") {
|
|
11653
|
+
node.x = physicsNode.x;
|
|
11654
|
+
node.y = physicsNode.y;
|
|
11655
|
+
}
|
|
11656
|
+
}
|
|
11657
|
+
});
|
|
11658
|
+
if (this.forceGraph) {
|
|
11659
|
+
const graphInstance = this.getGraphInstance();
|
|
11660
|
+
graphInstance.graphData(this.config.graphData);
|
|
11661
|
+
if (!document.hidden && this.isElementInViewport(this.container)) {
|
|
11662
|
+
graphInstance.d3ReheatSimulation();
|
|
11663
|
+
}
|
|
11664
|
+
}
|
|
11665
|
+
this.triggerKapsuleUpdate();
|
|
11666
|
+
}
|
|
11667
|
+
/**
|
|
11668
|
+
* Cleanup deterministic physics handling
|
|
11669
|
+
*/
|
|
11670
|
+
cleanupDeterministicPhysics() {
|
|
11671
|
+
if (this.physicsTimeoutId) {
|
|
11672
|
+
window.clearTimeout(this.physicsTimeoutId);
|
|
11673
|
+
this.physicsTimeoutId = void 0;
|
|
11674
|
+
}
|
|
11675
|
+
if (this.intersectionObserver) {
|
|
11676
|
+
this.intersectionObserver.disconnect();
|
|
11677
|
+
this.intersectionObserver = null;
|
|
11678
|
+
}
|
|
11679
|
+
}
|
|
11680
|
+
/**
|
|
11681
|
+
* Initialize the underlying force-graph instance
|
|
11682
|
+
* Step 2: Data loading with performance validation
|
|
11683
|
+
* Target: Linear scaling, < 10ms per 1000 nodes
|
|
11684
|
+
*/
|
|
11685
|
+
initializeForceGraph() {
|
|
11686
|
+
if (this.isInitialized || this.isDestroyed) {
|
|
11687
|
+
return;
|
|
11688
|
+
}
|
|
11689
|
+
const initStart = performance.now();
|
|
11690
|
+
try {
|
|
11691
|
+
this.forceGraph = ForceGraph()(this.container);
|
|
11692
|
+
if (!this.forceGraph) {
|
|
11693
|
+
throw new Error("Failed to create force-graph instance");
|
|
11694
|
+
}
|
|
11695
|
+
this.applyConfiguration();
|
|
11696
|
+
this.isInitialized = true;
|
|
11697
|
+
if (this.config.enablePerformanceMonitoring) {
|
|
11698
|
+
this.setupRenderingMonitoring();
|
|
11699
|
+
}
|
|
11700
|
+
const initTime = performance.now() - initStart;
|
|
11701
|
+
this.performanceMonitor.setMetric("firstRender", initTime);
|
|
11702
|
+
if (this.config.enablePerformanceMonitoring) {
|
|
11703
|
+
this.validateInitializationPerformance(initTime);
|
|
11704
|
+
}
|
|
11705
|
+
} catch (error) {
|
|
11706
|
+
this.isInitialized = false;
|
|
11707
|
+
throw new Error(`Failed to initialize force-graph: ${error}`);
|
|
11708
|
+
}
|
|
11709
|
+
}
|
|
11710
|
+
/**
|
|
11711
|
+
* Apply configuration to force-graph instance
|
|
11712
|
+
* Step 2: Performance-optimized configuration application
|
|
11713
|
+
*/
|
|
11714
|
+
applyConfiguration() {
|
|
11715
|
+
if (!this.forceGraph || !this.config) return;
|
|
11716
|
+
const graph = this.getGraphInstance();
|
|
11717
|
+
graph.width(this.config.width || 400).height(this.config.height || 300).backgroundColor(this.config.backgroundColor || "#ffffff").autoPauseRedraw(this.config.autoPauseRedraw ?? true);
|
|
11718
|
+
if (this.config.d3AlphaDecay !== void 0) {
|
|
11719
|
+
graph.d3AlphaDecay(this.config.d3AlphaDecay);
|
|
11720
|
+
}
|
|
11721
|
+
if (this.config.d3VelocityDecay !== void 0) {
|
|
11722
|
+
graph.d3VelocityDecay(this.config.d3VelocityDecay);
|
|
11723
|
+
}
|
|
11724
|
+
if (this.config.d3AlphaMin !== void 0) {
|
|
11725
|
+
graph.d3AlphaMin(this.config.d3AlphaMin);
|
|
11726
|
+
}
|
|
11727
|
+
if (this.config.cooldownTime !== void 0) {
|
|
11728
|
+
graph.cooldownTime(this.config.cooldownTime);
|
|
11729
|
+
}
|
|
11730
|
+
if (this.config.cooldownTicks !== void 0) {
|
|
11731
|
+
graph.cooldownTicks(this.config.cooldownTicks);
|
|
11732
|
+
}
|
|
11733
|
+
if (this.config.nodeColor) {
|
|
11734
|
+
graph.nodeColor(this.config.nodeColor);
|
|
11735
|
+
}
|
|
11736
|
+
if (this.config.nodeVal) {
|
|
11737
|
+
graph.nodeVal(this.config.nodeVal);
|
|
11738
|
+
}
|
|
11739
|
+
if (this.config.nodeRelSize !== void 0) {
|
|
11740
|
+
graph.nodeRelSize(this.config.nodeRelSize);
|
|
11741
|
+
}
|
|
11742
|
+
if (this.config.nodeLabel) {
|
|
11743
|
+
graph.nodeLabel(this.config.nodeLabel);
|
|
11744
|
+
}
|
|
11745
|
+
if (this.config.nodeVisibility) {
|
|
11746
|
+
graph.nodeVisibility(this.config.nodeVisibility);
|
|
11747
|
+
}
|
|
11748
|
+
if (this.config.nodeCanvasObjectMode) {
|
|
11749
|
+
graph.nodeCanvasObjectMode(this.config.nodeCanvasObjectMode);
|
|
11750
|
+
}
|
|
11751
|
+
if (this.config.linkColor) {
|
|
11752
|
+
graph.linkColor(this.config.linkColor);
|
|
11753
|
+
}
|
|
11754
|
+
if (this.config.linkWidth) {
|
|
11755
|
+
graph.linkWidth(this.config.linkWidth);
|
|
11756
|
+
}
|
|
11757
|
+
if (this.config.linkLabel) {
|
|
11758
|
+
graph.linkLabel(this.config.linkLabel);
|
|
11759
|
+
}
|
|
11760
|
+
if (this.config.linkVisibility) {
|
|
11761
|
+
graph.linkVisibility(this.config.linkVisibility);
|
|
11762
|
+
}
|
|
11763
|
+
if (this.config.linkDirectionalArrowLength !== void 0) {
|
|
11764
|
+
graph.linkDirectionalArrowLength(this.config.linkDirectionalArrowLength);
|
|
11765
|
+
}
|
|
11766
|
+
if (this.config.linkDirectionalArrowColor) {
|
|
11767
|
+
graph.linkDirectionalArrowColor(this.config.linkDirectionalArrowColor);
|
|
11768
|
+
}
|
|
11769
|
+
if (this.config.linkDirectionalArrowRelPos !== void 0) {
|
|
11770
|
+
graph.linkDirectionalArrowRelPos(this.config.linkDirectionalArrowRelPos);
|
|
11771
|
+
}
|
|
11772
|
+
if (this.config.linkDirectionalParticles !== void 0) {
|
|
11773
|
+
graph.linkDirectionalParticles(this.config.linkDirectionalParticles);
|
|
11774
|
+
}
|
|
11775
|
+
if (this.config.linkDirectionalParticleSpeed !== void 0) {
|
|
11776
|
+
graph.linkDirectionalParticleSpeed(this.config.linkDirectionalParticleSpeed);
|
|
11777
|
+
}
|
|
11778
|
+
if (this.config.linkDirectionalParticleWidth !== void 0) {
|
|
11779
|
+
graph.linkDirectionalParticleWidth(this.config.linkDirectionalParticleWidth);
|
|
11780
|
+
}
|
|
11781
|
+
if (this.config.linkDirectionalParticleColor) {
|
|
11782
|
+
graph.linkDirectionalParticleColor(this.config.linkDirectionalParticleColor);
|
|
11783
|
+
}
|
|
11784
|
+
if (this.config.linkCurvature !== void 0) {
|
|
11785
|
+
graph.linkCurvature(this.config.linkCurvature);
|
|
11786
|
+
}
|
|
11787
|
+
if (this.config.linkCanvasObjectMode) {
|
|
11788
|
+
graph.linkCanvasObjectMode(this.config.linkCanvasObjectMode);
|
|
11789
|
+
}
|
|
11790
|
+
if (this.config.enableNodeDrag !== void 0) {
|
|
11791
|
+
graph.enableNodeDrag(this.config.enableNodeDrag);
|
|
11792
|
+
}
|
|
11793
|
+
if (this.config.enableZoomInteraction !== void 0) {
|
|
11794
|
+
graph.enableZoomInteraction(this.config.enableZoomInteraction);
|
|
11795
|
+
}
|
|
11796
|
+
if (this.config.enablePanInteraction !== void 0) {
|
|
11797
|
+
graph.enablePanInteraction(this.config.enablePanInteraction);
|
|
11798
|
+
}
|
|
11799
|
+
if (this.config.enablePerformanceMonitoring) {
|
|
11800
|
+
this.setupRenderingMonitoring();
|
|
11801
|
+
}
|
|
11802
|
+
if (this.config.onNodeClick) {
|
|
11803
|
+
graph.onNodeClick(this.config.onNodeClick);
|
|
11804
|
+
}
|
|
11805
|
+
if (this.config.onNodeHover) {
|
|
11806
|
+
graph.onNodeHover(this.config.onNodeHover);
|
|
11807
|
+
}
|
|
11808
|
+
if (this.config.onLinkClick) {
|
|
11809
|
+
graph.onLinkClick(this.config.onLinkClick);
|
|
11810
|
+
}
|
|
11811
|
+
if (this.config.onLinkHover) {
|
|
11812
|
+
graph.onLinkHover(this.config.onLinkHover);
|
|
11813
|
+
}
|
|
11814
|
+
if (this.config.onRenderFramePre) {
|
|
11815
|
+
graph.onRenderFramePre(this.config.onRenderFramePre);
|
|
11816
|
+
}
|
|
11817
|
+
if (this.config.onRenderFramePost) {
|
|
11818
|
+
graph.onRenderFramePost(this.config.onRenderFramePost);
|
|
11819
|
+
}
|
|
11820
|
+
if (this.config.onEngineTick) {
|
|
11821
|
+
graph.onEngineTick(this.config.onEngineTick);
|
|
11822
|
+
}
|
|
11823
|
+
if (this.config.onEngineStop) {
|
|
11824
|
+
graph.onEngineStop(this.config.onEngineStop);
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
11827
|
+
/**
|
|
11828
|
+
* Step 2 Performance Validation for Initialization
|
|
11829
|
+
*/
|
|
11830
|
+
validateInitializationPerformance(_initTime) {
|
|
11831
|
+
}
|
|
11832
|
+
/**
|
|
11833
|
+
* Validate data loading performance based on node/link count
|
|
11834
|
+
* Step 2: Linear scaling validation
|
|
11835
|
+
*/
|
|
11836
|
+
validateDataLoadingPerformance(_loadTime, nodeCount, linkCount) {
|
|
11837
|
+
this.performanceMonitor.setMetric("nodeCount", nodeCount);
|
|
11838
|
+
this.performanceMonitor.setMetric("linkCount", linkCount);
|
|
11839
|
+
}
|
|
11840
|
+
// =============================================================================
|
|
11841
|
+
// PUBLIC API - ForceGraphMethods
|
|
11842
|
+
// =============================================================================
|
|
11843
|
+
d3ReheatSimulation() {
|
|
11844
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11845
|
+
if (this.forceGraph) {
|
|
11846
|
+
this.getGraphInstance().d3ReheatSimulation();
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
stopAnimation() {
|
|
11850
|
+
if (!this.isInitialized) return;
|
|
11851
|
+
if (this.forceGraph) {
|
|
11852
|
+
this.getGraphInstance().pauseAnimation();
|
|
11853
|
+
}
|
|
11854
|
+
}
|
|
11855
|
+
pauseAnimation() {
|
|
11856
|
+
if (!this.isInitialized) return;
|
|
11857
|
+
if (this.forceGraph) {
|
|
11858
|
+
this.getGraphInstance().pauseAnimation();
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
resumeAnimation() {
|
|
11862
|
+
if (!this.isInitialized) return;
|
|
11863
|
+
if (this.forceGraph) {
|
|
11864
|
+
this.getGraphInstance().resumeAnimation();
|
|
11865
|
+
}
|
|
11866
|
+
}
|
|
11867
|
+
centerAt(x3, y3, duration) {
|
|
11868
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11869
|
+
if (this.forceGraph) {
|
|
11870
|
+
this.getGraphInstance().centerAt(x3, y3, duration);
|
|
11871
|
+
}
|
|
11872
|
+
}
|
|
11873
|
+
zoom(scale, duration) {
|
|
11874
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11875
|
+
let result = void 0;
|
|
11876
|
+
if (this.forceGraph) {
|
|
11877
|
+
if (scale === void 0) {
|
|
11878
|
+
result = this.getGraphInstance().zoom();
|
|
11879
|
+
} else {
|
|
11880
|
+
this.getGraphInstance().zoom(scale, duration);
|
|
11881
|
+
result = void 0;
|
|
11882
|
+
}
|
|
11883
|
+
} else if (scale === void 0) {
|
|
11884
|
+
result = 1;
|
|
11885
|
+
}
|
|
11886
|
+
return result;
|
|
11887
|
+
}
|
|
11888
|
+
zoomToFit(padding, duration) {
|
|
11889
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11890
|
+
const containerWidth = this.container.clientWidth;
|
|
11891
|
+
const containerHeight = this.container.clientHeight;
|
|
11892
|
+
const isInViewport = this.container ? this.isElementInViewport(this.container) : false;
|
|
11893
|
+
const isVisible = !document.hidden && isInViewport;
|
|
11894
|
+
const hasValidDimensions = containerWidth > 0 && containerHeight > 0;
|
|
11895
|
+
let bboxValid = false;
|
|
11896
|
+
if (this.forceGraph) {
|
|
11897
|
+
const bbox = this.getGraphInstance().getGraphBbox();
|
|
11898
|
+
bboxValid = bbox && bbox.x[1] - bbox.x[0] > 0 && bbox.y[1] - bbox.y[0] > 0;
|
|
11899
|
+
}
|
|
11900
|
+
if (!isVisible || !hasValidDimensions || !bboxValid) {
|
|
11901
|
+
this.deferredFitViewParams = { padding: padding || 40, duration: duration || 300 };
|
|
11902
|
+
this.pendingFitView = true;
|
|
11903
|
+
this.setupDeferredFitView();
|
|
11904
|
+
return;
|
|
11905
|
+
}
|
|
11906
|
+
if (this.forceGraph && this.container) {
|
|
11907
|
+
this.cacheContainerDimensions();
|
|
11908
|
+
const bbox = this.getGraphInstance().getGraphBbox();
|
|
11909
|
+
const width = bbox.x[1] - bbox.x[0];
|
|
11910
|
+
const height = bbox.y[1] - bbox.y[0];
|
|
11911
|
+
const centerX = (bbox.x[0] + bbox.x[1]) / 2;
|
|
11912
|
+
const centerY = (bbox.y[0] + bbox.y[1]) / 2;
|
|
11913
|
+
const paddingValue = padding || 40;
|
|
11914
|
+
const viewportWidth = containerWidth - paddingValue;
|
|
11915
|
+
const viewportHeight = containerHeight - paddingValue;
|
|
11916
|
+
const scale = Math.min(viewportWidth / width, viewportHeight / height);
|
|
11917
|
+
if (width > 0 && height > 0) {
|
|
11918
|
+
this.getGraphInstance().centerAt(centerX, centerY);
|
|
11919
|
+
this.getGraphInstance().zoom(scale, duration || 300);
|
|
11920
|
+
this.pendingFitView = false;
|
|
11921
|
+
}
|
|
11922
|
+
}
|
|
11923
|
+
}
|
|
11924
|
+
screen2GraphCoords(screenX, screenY) {
|
|
11925
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11926
|
+
let result = { x: 0, y: 0 };
|
|
11927
|
+
if (this.forceGraph) {
|
|
11928
|
+
result = this.getGraphInstance().screen2GraphCoords(screenX, screenY) || { x: 0, y: 0 };
|
|
11929
|
+
}
|
|
11930
|
+
return result;
|
|
11931
|
+
}
|
|
11932
|
+
graph2ScreenCoords(graphX, graphY) {
|
|
11933
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11934
|
+
let result = { x: 0, y: 0 };
|
|
11935
|
+
if (this.forceGraph) {
|
|
11936
|
+
result = this.getGraphInstance().graph2ScreenCoords(graphX, graphY) || { x: 0, y: 0 };
|
|
11937
|
+
}
|
|
11938
|
+
return result;
|
|
11939
|
+
}
|
|
11940
|
+
getGraphBbox(nodes) {
|
|
11941
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11942
|
+
let result = { x: [0, 0], y: [0, 0] };
|
|
11943
|
+
if (this.forceGraph) {
|
|
11944
|
+
result = this.getGraphInstance().getGraphBbox(nodes) || { x: [0, 0], y: [0, 0] };
|
|
11945
|
+
}
|
|
11946
|
+
return result;
|
|
11947
|
+
}
|
|
11948
|
+
emitParticle(link) {
|
|
11949
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11950
|
+
if (this.forceGraph) {
|
|
11951
|
+
this.getGraphInstance().emitParticle(link);
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
d3Force(forceName, forceImpl) {
|
|
11955
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
11956
|
+
if (this.forceGraph) {
|
|
11957
|
+
if (forceImpl !== void 0) {
|
|
11958
|
+
this.getGraphInstance().d3Force(forceName, forceImpl);
|
|
11959
|
+
return this;
|
|
11960
|
+
} else {
|
|
11961
|
+
const result = this.getGraphInstance().d3Force(forceName);
|
|
11962
|
+
return result;
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
return forceImpl !== void 0 ? this : null;
|
|
11966
|
+
}
|
|
11967
|
+
graphData(data) {
|
|
11968
|
+
if (data === void 0) {
|
|
11969
|
+
return this.config.graphData ?? { nodes: [], links: [] };
|
|
11970
|
+
}
|
|
11971
|
+
this.config.graphData = data;
|
|
11972
|
+
const nodeCount = data.nodes.length;
|
|
11973
|
+
const linkCount = data.links.length;
|
|
11974
|
+
this.pendingFitView = false;
|
|
11975
|
+
this.performanceMonitor.setMetric("nodeCount", nodeCount);
|
|
11976
|
+
this.performanceMonitor.setMetric("linkCount", linkCount);
|
|
11977
|
+
this.webWorkerPhysicsCompleted = false;
|
|
11978
|
+
if (!this.isInitialized) {
|
|
11979
|
+
this.initializeForceGraph();
|
|
11980
|
+
}
|
|
11981
|
+
if (this.forceGraph) {
|
|
11982
|
+
const loadStart = performance.now();
|
|
11983
|
+
this.getGraphInstance().graphData(data);
|
|
11984
|
+
const loadTime = performance.now() - loadStart;
|
|
11985
|
+
this.performanceMonitor.setMetric("dataLoad", loadTime);
|
|
11986
|
+
if (this.config.enablePerformanceMonitoring) {
|
|
11987
|
+
this.validateDataLoadingPerformance(loadTime, nodeCount, linkCount);
|
|
11988
|
+
}
|
|
11989
|
+
this.updateLegends();
|
|
11990
|
+
}
|
|
11991
|
+
if (this.config.deterministicLayout !== false && this.wasInitializedOutOfView && data?.nodes?.length > 0 && !this.webWorkerPhysicsCompleted) {
|
|
11992
|
+
if (this.physicsTimeoutId) {
|
|
11993
|
+
window.clearTimeout(this.physicsTimeoutId);
|
|
11994
|
+
}
|
|
11995
|
+
this.physicsTimeoutId = window.setTimeout(() => {
|
|
11996
|
+
this.runWebWorkerPhysics().catch(() => {
|
|
11997
|
+
});
|
|
11998
|
+
}, 200);
|
|
11999
|
+
}
|
|
12000
|
+
return this;
|
|
12001
|
+
}
|
|
12002
|
+
width(width) {
|
|
12003
|
+
if (width === void 0) {
|
|
12004
|
+
return this.config.width ?? 400;
|
|
12005
|
+
}
|
|
12006
|
+
this.config.width = width;
|
|
12007
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12008
|
+
this.getGraphInstance().width(width);
|
|
12009
|
+
}
|
|
12010
|
+
return this;
|
|
12011
|
+
}
|
|
12012
|
+
height(height) {
|
|
12013
|
+
if (height === void 0) {
|
|
12014
|
+
return this.config.height ?? 300;
|
|
12015
|
+
}
|
|
12016
|
+
this.config.height = height;
|
|
12017
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12018
|
+
this.getGraphInstance().height(height);
|
|
12019
|
+
}
|
|
12020
|
+
return this;
|
|
12021
|
+
}
|
|
12022
|
+
backgroundColor(color2) {
|
|
12023
|
+
if (color2 === void 0) {
|
|
12024
|
+
return this.config.backgroundColor ?? "#ffffff";
|
|
12025
|
+
}
|
|
12026
|
+
this.config.backgroundColor = color2;
|
|
12027
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12028
|
+
this.getGraphInstance().backgroundColor(color2);
|
|
12029
|
+
}
|
|
12030
|
+
return this;
|
|
12031
|
+
}
|
|
12032
|
+
nodeColor(color2) {
|
|
12033
|
+
if (color2 === void 0) return this.config.nodeColor ?? "#999999";
|
|
12034
|
+
this.config.nodeColor = color2;
|
|
12035
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12036
|
+
this.getGraphInstance().nodeColor(color2);
|
|
12037
|
+
}
|
|
12038
|
+
return this;
|
|
12039
|
+
}
|
|
12040
|
+
nodeVal(val) {
|
|
12041
|
+
if (val === void 0) return this.config.nodeVal ?? 1;
|
|
12042
|
+
this.config.nodeVal = val;
|
|
12043
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12044
|
+
this.getGraphInstance().nodeVal(val);
|
|
12045
|
+
}
|
|
12046
|
+
return this;
|
|
12047
|
+
}
|
|
12048
|
+
nodeRelSize(size) {
|
|
12049
|
+
if (size === void 0) return this.config.nodeRelSize ?? 4;
|
|
12050
|
+
this.config.nodeRelSize = size;
|
|
12051
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12052
|
+
this.getGraphInstance().nodeRelSize(size);
|
|
12053
|
+
}
|
|
12054
|
+
return this;
|
|
12055
|
+
}
|
|
12056
|
+
nodeLabel(label) {
|
|
12057
|
+
if (label === void 0) return this.config.nodeLabel ?? "";
|
|
12058
|
+
this.config.nodeLabel = label;
|
|
12059
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12060
|
+
this.getGraphInstance().nodeLabel(label);
|
|
12061
|
+
}
|
|
12062
|
+
return this;
|
|
12063
|
+
}
|
|
12064
|
+
nodeVisibility(visibility) {
|
|
12065
|
+
if (visibility === void 0) return this.config.nodeVisibility ?? true;
|
|
12066
|
+
this.config.nodeVisibility = visibility;
|
|
12067
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12068
|
+
this.getGraphInstance().nodeVisibility(visibility);
|
|
12069
|
+
}
|
|
12070
|
+
return this;
|
|
12071
|
+
}
|
|
12072
|
+
nodeCanvasObjectMode(mode) {
|
|
12073
|
+
if (mode === void 0) return this.config.nodeCanvasObjectMode ?? "replace";
|
|
12074
|
+
this.config.nodeCanvasObjectMode = mode;
|
|
12075
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12076
|
+
this.getGraphInstance().nodeCanvasObjectMode(mode);
|
|
12077
|
+
}
|
|
12078
|
+
return this;
|
|
12079
|
+
}
|
|
12080
|
+
linkColor(color2) {
|
|
12081
|
+
if (color2 === void 0) return this.config.linkColor ?? "#999999";
|
|
12082
|
+
this.config.linkColor = color2;
|
|
12083
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12084
|
+
this.getGraphInstance().linkColor(color2);
|
|
12085
|
+
}
|
|
12086
|
+
return this;
|
|
12087
|
+
}
|
|
12088
|
+
linkWidth(width) {
|
|
12089
|
+
if (width === void 0) return this.config.linkWidth ?? 1;
|
|
12090
|
+
this.config.linkWidth = width;
|
|
12091
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12092
|
+
this.getGraphInstance().linkWidth(width);
|
|
12093
|
+
}
|
|
12094
|
+
return this;
|
|
12095
|
+
}
|
|
12096
|
+
linkLabel(label) {
|
|
12097
|
+
if (label === void 0) return this.config.linkLabel ?? "";
|
|
12098
|
+
this.config.linkLabel = label;
|
|
12099
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12100
|
+
this.getGraphInstance().linkLabel(label);
|
|
12101
|
+
}
|
|
12102
|
+
return this;
|
|
12103
|
+
}
|
|
12104
|
+
linkVisibility(visibility) {
|
|
12105
|
+
if (visibility === void 0) return this.config.linkVisibility ?? true;
|
|
12106
|
+
this.config.linkVisibility = visibility;
|
|
12107
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12108
|
+
this.getGraphInstance().linkVisibility(visibility);
|
|
12109
|
+
}
|
|
12110
|
+
return this;
|
|
12111
|
+
}
|
|
12112
|
+
linkDirectionalArrowLength(length) {
|
|
12113
|
+
if (length === void 0) return this.config.linkDirectionalArrowLength ?? 0;
|
|
12114
|
+
this.config.linkDirectionalArrowLength = length;
|
|
12115
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12116
|
+
this.getGraphInstance().linkDirectionalArrowLength(length);
|
|
12117
|
+
}
|
|
12118
|
+
return this;
|
|
12119
|
+
}
|
|
12120
|
+
linkDirectionalParticles(particles) {
|
|
12121
|
+
if (particles === void 0) return this.config.linkDirectionalParticles ?? 0;
|
|
12122
|
+
this.config.linkDirectionalParticles = particles;
|
|
12123
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12124
|
+
this.getGraphInstance().linkDirectionalParticles(particles);
|
|
12125
|
+
}
|
|
12126
|
+
return this;
|
|
12127
|
+
}
|
|
12128
|
+
linkDirectionalArrowColor(color2) {
|
|
12129
|
+
if (color2 === void 0) return this.config.linkDirectionalArrowColor ?? "#999999";
|
|
12130
|
+
this.config.linkDirectionalArrowColor = color2;
|
|
12131
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12132
|
+
this.getGraphInstance().linkDirectionalArrowColor(color2);
|
|
12133
|
+
}
|
|
12134
|
+
return this;
|
|
12135
|
+
}
|
|
12136
|
+
linkDirectionalArrowRelPos(position) {
|
|
12137
|
+
if (position === void 0) return this.config.linkDirectionalArrowRelPos ?? 0.5;
|
|
12138
|
+
this.config.linkDirectionalArrowRelPos = position;
|
|
12139
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12140
|
+
this.getGraphInstance().linkDirectionalArrowRelPos(position);
|
|
12141
|
+
}
|
|
12142
|
+
return this;
|
|
12143
|
+
}
|
|
12144
|
+
linkDirectionalParticleSpeed(speed) {
|
|
12145
|
+
if (speed === void 0) return this.config.linkDirectionalParticleSpeed ?? 1;
|
|
12146
|
+
this.config.linkDirectionalParticleSpeed = speed;
|
|
12147
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12148
|
+
this.getGraphInstance().linkDirectionalParticleSpeed(speed);
|
|
12149
|
+
}
|
|
12150
|
+
return this;
|
|
12151
|
+
}
|
|
12152
|
+
linkDirectionalParticleWidth(width) {
|
|
12153
|
+
if (width === void 0) return this.config.linkDirectionalParticleWidth ?? 4;
|
|
12154
|
+
this.config.linkDirectionalParticleWidth = width;
|
|
12155
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12156
|
+
this.getGraphInstance().linkDirectionalParticleWidth(width);
|
|
12157
|
+
}
|
|
12158
|
+
return this;
|
|
12159
|
+
}
|
|
12160
|
+
linkDirectionalParticleColor(color2) {
|
|
12161
|
+
if (color2 === void 0) return this.config.linkDirectionalParticleColor ?? "#999999";
|
|
12162
|
+
this.config.linkDirectionalParticleColor = color2;
|
|
12163
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12164
|
+
this.getGraphInstance().linkDirectionalParticleColor(color2);
|
|
12165
|
+
}
|
|
12166
|
+
return this;
|
|
12167
|
+
}
|
|
12168
|
+
linkCurvature(curvature) {
|
|
12169
|
+
if (curvature === void 0) return this.config.linkCurvature ?? 0;
|
|
12170
|
+
this.config.linkCurvature = curvature;
|
|
12171
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12172
|
+
this.getGraphInstance().linkCurvature(curvature);
|
|
12173
|
+
}
|
|
12174
|
+
return this;
|
|
12175
|
+
}
|
|
12176
|
+
linkCanvasObjectMode(mode) {
|
|
12177
|
+
if (mode === void 0) return this.config.linkCanvasObjectMode ?? "replace";
|
|
12178
|
+
this.config.linkCanvasObjectMode = mode;
|
|
12179
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12180
|
+
this.getGraphInstance().linkCanvasObjectMode(mode);
|
|
12181
|
+
}
|
|
12182
|
+
return this;
|
|
12183
|
+
}
|
|
12184
|
+
onNodeClick(handler) {
|
|
12185
|
+
if (handler === void 0) return this.config.onNodeClick ?? null;
|
|
12186
|
+
this.config.onNodeClick = handler || void 0;
|
|
12187
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12188
|
+
this.getGraphInstance().onNodeClick(handler);
|
|
12189
|
+
}
|
|
12190
|
+
return this;
|
|
12191
|
+
}
|
|
12192
|
+
onNodeDoubleClick(handler) {
|
|
12193
|
+
if (handler === void 0) return this.config.onNodeDoubleClick ?? null;
|
|
12194
|
+
this.config.onNodeDoubleClick = handler || void 0;
|
|
12195
|
+
return this;
|
|
12196
|
+
}
|
|
12197
|
+
onNodeHover(handler) {
|
|
12198
|
+
if (handler === void 0) return this.config.onNodeHover ?? null;
|
|
12199
|
+
this.config.onNodeHover = handler || void 0;
|
|
12200
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12201
|
+
this.getGraphInstance().onNodeHover(handler);
|
|
12202
|
+
}
|
|
12203
|
+
return this;
|
|
12204
|
+
}
|
|
12205
|
+
onLinkClick(handler) {
|
|
12206
|
+
if (handler === void 0) return this.config.onLinkClick ?? null;
|
|
12207
|
+
this.config.onLinkClick = handler || void 0;
|
|
12208
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12209
|
+
this.getGraphInstance().onLinkClick(handler);
|
|
12210
|
+
}
|
|
12211
|
+
return this;
|
|
12212
|
+
}
|
|
12213
|
+
onLinkHover(handler) {
|
|
12214
|
+
if (handler === void 0) return this.config.onLinkHover ?? null;
|
|
12215
|
+
this.config.onLinkHover = handler || void 0;
|
|
12216
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12217
|
+
this.getGraphInstance().onLinkHover(handler);
|
|
12218
|
+
}
|
|
12219
|
+
return this;
|
|
12220
|
+
}
|
|
12221
|
+
onRenderFramePre(handler) {
|
|
12222
|
+
if (handler === void 0) return this.config.onRenderFramePre ?? null;
|
|
12223
|
+
this.config.onRenderFramePre = handler || void 0;
|
|
12224
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12225
|
+
this.getGraphInstance().onRenderFramePre(handler);
|
|
12226
|
+
}
|
|
12227
|
+
return this;
|
|
12228
|
+
}
|
|
12229
|
+
onRenderFramePost(handler) {
|
|
12230
|
+
if (handler === void 0) return this.config.onRenderFramePost ?? null;
|
|
12231
|
+
this.config.onRenderFramePost = handler || void 0;
|
|
12232
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12233
|
+
this.getGraphInstance().onRenderFramePost(handler);
|
|
12234
|
+
}
|
|
12235
|
+
return this;
|
|
12236
|
+
}
|
|
12237
|
+
cooldownTime(time) {
|
|
12238
|
+
if (time === void 0) return this.config.cooldownTime ?? 15e3;
|
|
12239
|
+
this.config.cooldownTime = time;
|
|
12240
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12241
|
+
this.getGraphInstance().cooldownTime(time);
|
|
12242
|
+
}
|
|
12243
|
+
return this;
|
|
12244
|
+
}
|
|
12245
|
+
d3AlphaDecay(decay) {
|
|
12246
|
+
if (decay === void 0) return this.config.d3AlphaDecay ?? 0.0228;
|
|
12247
|
+
this.config.d3AlphaDecay = decay;
|
|
12248
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12249
|
+
this.getGraphInstance().d3AlphaDecay(decay);
|
|
12250
|
+
}
|
|
12251
|
+
return this;
|
|
12252
|
+
}
|
|
12253
|
+
d3VelocityDecay(decay) {
|
|
12254
|
+
if (decay === void 0) return this.config.d3VelocityDecay ?? 0.4;
|
|
12255
|
+
this.config.d3VelocityDecay = decay;
|
|
12256
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12257
|
+
this.getGraphInstance().d3VelocityDecay(decay);
|
|
12258
|
+
}
|
|
12259
|
+
return this;
|
|
12260
|
+
}
|
|
12261
|
+
onEngineStop(handler) {
|
|
12262
|
+
if (handler === void 0) return this.config.onEngineStop;
|
|
12263
|
+
this.config.onEngineStop = handler;
|
|
12264
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12265
|
+
this.getGraphInstance().onEngineStop(handler);
|
|
12266
|
+
}
|
|
12267
|
+
return this;
|
|
12268
|
+
}
|
|
12269
|
+
onEngineTick(handler) {
|
|
12270
|
+
if (handler === void 0) return this.config.onEngineTick;
|
|
12271
|
+
this.config.onEngineTick = handler;
|
|
12272
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12273
|
+
this.getGraphInstance().onEngineTick(handler);
|
|
12274
|
+
}
|
|
12275
|
+
return this;
|
|
12276
|
+
}
|
|
12277
|
+
// =============================================================================
|
|
12278
|
+
// PUBLIC API - ForceGraphPerformanceMethods
|
|
12279
|
+
// =============================================================================
|
|
12280
|
+
getPerformanceMetrics() {
|
|
12281
|
+
return this.performanceMonitor.getMetrics();
|
|
12282
|
+
}
|
|
12283
|
+
resetPerformanceMetrics() {
|
|
12284
|
+
this.performanceMonitor.reset();
|
|
12285
|
+
}
|
|
12286
|
+
logPerformanceSummary() {
|
|
12287
|
+
this.performanceMonitor.logSummary();
|
|
12288
|
+
}
|
|
12289
|
+
validatePerformance() {
|
|
12290
|
+
const metrics = this.performanceMonitor.getMetrics();
|
|
12291
|
+
const targets = this.config.performanceTargets ?? {};
|
|
12292
|
+
const warnings = [];
|
|
12293
|
+
const recommendations = [];
|
|
12294
|
+
if (targets.creation && metrics.creation > targets.creation) {
|
|
12295
|
+
warnings.push(`Creation time ${metrics.creation.toFixed(2)}ms exceeds target ${targets.creation}ms`);
|
|
12296
|
+
recommendations.push("Consider reducing initialization complexity");
|
|
12297
|
+
}
|
|
12298
|
+
if (targets.dataLoad && metrics.dataLoad > targets.dataLoad) {
|
|
12299
|
+
warnings.push(`Data load time ${metrics.dataLoad.toFixed(2)}ms exceeds target ${targets.dataLoad}ms`);
|
|
12300
|
+
recommendations.push("Consider data preprocessing or pagination for large datasets");
|
|
12301
|
+
}
|
|
12302
|
+
return {
|
|
12303
|
+
passed: warnings.length === 0,
|
|
12304
|
+
warnings,
|
|
12305
|
+
recommendations
|
|
12306
|
+
};
|
|
12307
|
+
}
|
|
12308
|
+
// =============================================================================
|
|
12309
|
+
// STEP 4: RENDERING PERFORMANCE OPTIMIZATION
|
|
12310
|
+
// =============================================================================
|
|
12311
|
+
/**
|
|
12312
|
+
* Setup advanced rendering performance monitoring
|
|
12313
|
+
* Target: 60fps @ 1000 nodes with optimizations
|
|
12314
|
+
*/
|
|
12315
|
+
setupRenderingMonitoring() {
|
|
12316
|
+
if (!this.forceGraph || !this.renderingMonitor || !this.canvasOptimizer) return;
|
|
12317
|
+
const canvas = this.container.querySelector("canvas");
|
|
12318
|
+
if (canvas) {
|
|
12319
|
+
this.canvasOptimizer.initialize(canvas);
|
|
12320
|
+
}
|
|
12321
|
+
if (this.config.enablePerformanceMonitoring && this.config.performanceTargets?.render) {
|
|
12322
|
+
let frameCount = 0;
|
|
12323
|
+
this.getGraphInstance().onRenderFramePost(() => {
|
|
12324
|
+
frameCount++;
|
|
12325
|
+
if (frameCount % 10 === 0) {
|
|
12326
|
+
this.renderingMonitor?.recordRenderCall();
|
|
12327
|
+
}
|
|
12328
|
+
});
|
|
12329
|
+
this.startRenderingMonitoring();
|
|
12330
|
+
}
|
|
12331
|
+
}
|
|
12332
|
+
/**
|
|
12333
|
+
* Start advanced rendering performance monitoring
|
|
12334
|
+
*/
|
|
12335
|
+
startRenderingMonitoring() {
|
|
12336
|
+
if (!this.renderingMonitor) return;
|
|
12337
|
+
this.renderingMonitor.startMonitoring();
|
|
12338
|
+
this.isRenderingOptimized = true;
|
|
12339
|
+
}
|
|
12340
|
+
/**
|
|
12341
|
+
* Stop rendering performance monitoring
|
|
12342
|
+
*/
|
|
12343
|
+
stopRenderingMonitoring() {
|
|
12344
|
+
if (!this.renderingMonitor) return;
|
|
12345
|
+
this.renderingMonitor.stopMonitoring();
|
|
12346
|
+
this.isRenderingOptimized = false;
|
|
12347
|
+
}
|
|
12348
|
+
/**
|
|
12349
|
+
* Get current rendering performance metrics
|
|
12350
|
+
*/
|
|
12351
|
+
getRenderingMetrics() {
|
|
12352
|
+
if (!this.isRenderingOptimized || !this.renderingMonitor) {
|
|
12353
|
+
return null;
|
|
12354
|
+
}
|
|
12355
|
+
const currentData = this.forceGraph?.graphData?.();
|
|
12356
|
+
if (currentData) {
|
|
12357
|
+
this.renderingMonitor.updateCounts(
|
|
12358
|
+
currentData.nodes?.length ?? 0,
|
|
12359
|
+
currentData.links?.length ?? 0
|
|
12360
|
+
);
|
|
12361
|
+
}
|
|
12362
|
+
return this.renderingMonitor.getMetrics();
|
|
12363
|
+
}
|
|
12364
|
+
/**
|
|
12365
|
+
* Validate rendering performance against Step 4 targets
|
|
12366
|
+
*/
|
|
12367
|
+
validateRenderingPerformance() {
|
|
12368
|
+
const metrics = this.getRenderingMetrics();
|
|
12369
|
+
if (!metrics) {
|
|
12370
|
+
return {
|
|
12371
|
+
passed: false,
|
|
12372
|
+
metrics: {},
|
|
12373
|
+
validation: {
|
|
12374
|
+
passed: false,
|
|
12375
|
+
results: {
|
|
12376
|
+
fpsTarget: { expected: 60, actual: 0, passed: false },
|
|
12377
|
+
frameTimeTarget: { expected: 16.67, actual: 0, passed: false },
|
|
12378
|
+
droppedFramesTarget: { expected: 0, actual: 0, passed: false },
|
|
12379
|
+
efficiencyTarget: { expected: 0.9, actual: 0, passed: false }
|
|
12380
|
+
},
|
|
12381
|
+
overallScore: 0
|
|
12382
|
+
},
|
|
12383
|
+
recommendations: ["Rendering monitoring not started"]
|
|
12384
|
+
};
|
|
12385
|
+
}
|
|
12386
|
+
const validation = this.renderingMonitor?.validatePerformance() ?? {
|
|
12387
|
+
passed: false,
|
|
12388
|
+
results: {
|
|
12389
|
+
fpsTarget: { expected: 60, actual: 0, passed: false },
|
|
12390
|
+
frameTimeTarget: { expected: 16.67, actual: 0, passed: false },
|
|
12391
|
+
droppedFramesTarget: { expected: 0, actual: 0, passed: false },
|
|
12392
|
+
efficiencyTarget: { expected: 0.9, actual: 0, passed: false }
|
|
12393
|
+
},
|
|
12394
|
+
overallScore: 0
|
|
12395
|
+
};
|
|
12396
|
+
const recommendations = this.renderingMonitor?.getOptimizationRecommendations() ?? ["Rendering monitoring not available"];
|
|
12397
|
+
return {
|
|
12398
|
+
passed: validation.passed,
|
|
12399
|
+
metrics,
|
|
12400
|
+
validation,
|
|
12401
|
+
recommendations
|
|
12402
|
+
};
|
|
12403
|
+
}
|
|
12404
|
+
/**
|
|
12405
|
+
* Optimize rendering for large datasets
|
|
12406
|
+
* Implements performance optimizations based on node count
|
|
12407
|
+
*/
|
|
12408
|
+
optimizeForDataset(nodeCount) {
|
|
12409
|
+
if (!this.forceGraph) {
|
|
12410
|
+
return;
|
|
12411
|
+
}
|
|
12412
|
+
if (nodeCount > 5e3) {
|
|
12413
|
+
this.forceGraph.nodeCanvasObject(null).linkCanvasObject(null).nodeLabel("").linkLabel("").cooldownTicks(50).d3AlphaDecay(0.05);
|
|
12414
|
+
} else if (nodeCount > 1e3) {
|
|
12415
|
+
this.forceGraph.nodeCanvasObject(null).cooldownTicks(100).d3AlphaDecay(0.04);
|
|
12416
|
+
} else if (nodeCount > 500) {
|
|
12417
|
+
this.getGraphInstance().cooldownTicks(200);
|
|
12418
|
+
}
|
|
12419
|
+
this.performanceMonitor.setMetric("optimizedForNodeCount", nodeCount);
|
|
12420
|
+
}
|
|
12421
|
+
/**
|
|
12422
|
+
* Force a rendering performance test
|
|
12423
|
+
* Useful for validation during data loading
|
|
12424
|
+
*/
|
|
12425
|
+
async testRenderingPerformance(durationMs = 5e3) {
|
|
12426
|
+
return new Promise((resolve) => {
|
|
12427
|
+
const fpsReadings = [];
|
|
12428
|
+
const target = this.config.performanceTargets?.render ?? 60;
|
|
12429
|
+
const tempTracker = {
|
|
12430
|
+
startTime: performance.now(),
|
|
12431
|
+
frames: 0,
|
|
12432
|
+
lastTime: performance.now()
|
|
12433
|
+
};
|
|
12434
|
+
if (this.performanceTestInterval) {
|
|
12435
|
+
clearInterval(this.performanceTestInterval);
|
|
12436
|
+
}
|
|
12437
|
+
this.performanceTestInterval = setInterval(() => {
|
|
12438
|
+
const now2 = performance.now();
|
|
12439
|
+
tempTracker.frames++;
|
|
12440
|
+
if (now2 - tempTracker.lastTime >= 1e3) {
|
|
12441
|
+
const fps = tempTracker.frames * 1e3 / (now2 - tempTracker.lastTime);
|
|
12442
|
+
fpsReadings.push(fps);
|
|
12443
|
+
tempTracker.frames = 0;
|
|
12444
|
+
tempTracker.lastTime = now2;
|
|
12445
|
+
}
|
|
12446
|
+
if (now2 - tempTracker.startTime >= durationMs) {
|
|
12447
|
+
if (this.performanceTestInterval) {
|
|
12448
|
+
clearInterval(this.performanceTestInterval);
|
|
12449
|
+
this.performanceTestInterval = null;
|
|
12450
|
+
}
|
|
12451
|
+
const averageFPS = fpsReadings.reduce((a2, b) => a2 + b, 0) / fpsReadings.length;
|
|
12452
|
+
const minFPS = Math.min(...fpsReadings);
|
|
12453
|
+
const maxFPS = Math.max(...fpsReadings);
|
|
12454
|
+
const passed = averageFPS >= target * 0.8;
|
|
12455
|
+
resolve({
|
|
12456
|
+
averageFPS,
|
|
12457
|
+
minFPS,
|
|
12458
|
+
maxFPS,
|
|
12459
|
+
passed,
|
|
12460
|
+
targetFPS: target
|
|
12461
|
+
});
|
|
12462
|
+
}
|
|
12463
|
+
}, 16);
|
|
12464
|
+
});
|
|
12465
|
+
}
|
|
12466
|
+
// =============================================================================
|
|
12467
|
+
// PUBLIC API - Core Methods
|
|
12468
|
+
// =============================================================================
|
|
12469
|
+
render() {
|
|
12470
|
+
if (!this.isInitialized) {
|
|
12471
|
+
this.initializeForceGraph();
|
|
12472
|
+
}
|
|
12473
|
+
const renderStart = performance.now();
|
|
12474
|
+
if (this.forceGraph) {
|
|
12475
|
+
const graphInstance = this.getGraphInstance();
|
|
12476
|
+
if (graphInstance.refresh) {
|
|
12477
|
+
graphInstance.refresh();
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12480
|
+
const renderTime = performance.now() - renderStart;
|
|
12481
|
+
this.performanceMonitor.setMetric("firstRender", renderTime);
|
|
12482
|
+
this.initializeControls();
|
|
12483
|
+
this.initializeLegends();
|
|
12484
|
+
this.setupDoubleClickHandling();
|
|
12485
|
+
}
|
|
12486
|
+
cooldownTicks(ticks) {
|
|
12487
|
+
if (ticks === void 0) return this.config.cooldownTicks ?? 100;
|
|
12488
|
+
this.config.cooldownTicks = ticks;
|
|
12489
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12490
|
+
this.getGraphInstance().cooldownTicks(ticks);
|
|
12491
|
+
}
|
|
12492
|
+
return this;
|
|
12493
|
+
}
|
|
12494
|
+
d3AlphaMin(min) {
|
|
12495
|
+
if (min === void 0) return this.config.d3AlphaMin ?? 1e-3;
|
|
12496
|
+
this.config.d3AlphaMin = min;
|
|
12497
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12498
|
+
this.getGraphInstance().d3AlphaMin(min);
|
|
12499
|
+
}
|
|
12500
|
+
return this;
|
|
12501
|
+
}
|
|
12502
|
+
enableNodeDrag(enable) {
|
|
12503
|
+
if (enable === void 0) return this.config.enableNodeDrag ?? true;
|
|
12504
|
+
this.config.enableNodeDrag = enable;
|
|
12505
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12506
|
+
this.getGraphInstance().enableNodeDrag(enable);
|
|
12507
|
+
}
|
|
12508
|
+
return this;
|
|
12509
|
+
}
|
|
12510
|
+
nodeCanvasObject(paintFunction) {
|
|
12511
|
+
if (paintFunction === void 0) return this.config.nodeCanvasObject ?? null;
|
|
12512
|
+
this.config.nodeCanvasObject = paintFunction || void 0;
|
|
12513
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12514
|
+
this.getGraphInstance().nodeCanvasObject(paintFunction);
|
|
12515
|
+
}
|
|
12516
|
+
return this;
|
|
12517
|
+
}
|
|
12518
|
+
linkCanvasObject(paintFunction) {
|
|
12519
|
+
if (paintFunction === void 0) return this.config.linkCanvasObject ?? null;
|
|
12520
|
+
this.config.linkCanvasObject = paintFunction || void 0;
|
|
12521
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12522
|
+
this.getGraphInstance().linkCanvasObject(paintFunction);
|
|
12523
|
+
}
|
|
12524
|
+
return this;
|
|
12525
|
+
}
|
|
12526
|
+
autoPauseRedraw(enable) {
|
|
12527
|
+
if (enable === void 0) return this.config.autoPauseRedraw ?? true;
|
|
12528
|
+
this.config.autoPauseRedraw = enable;
|
|
12529
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12530
|
+
this.getGraphInstance().autoPauseRedraw(enable);
|
|
12531
|
+
}
|
|
12532
|
+
return this;
|
|
12533
|
+
}
|
|
12534
|
+
enableZoomInteraction(enable) {
|
|
12535
|
+
if (enable === void 0) {
|
|
12536
|
+
const configValue = this.config.enableZoomInteraction ?? true;
|
|
12537
|
+
return typeof configValue === "function" ? true : configValue;
|
|
12538
|
+
}
|
|
12539
|
+
this.config.enableZoomInteraction = enable;
|
|
12540
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12541
|
+
this.getGraphInstance().enableZoomInteraction(enable);
|
|
12542
|
+
}
|
|
12543
|
+
return this;
|
|
12544
|
+
}
|
|
12545
|
+
enablePanInteraction(enable) {
|
|
12546
|
+
if (enable === void 0) {
|
|
12547
|
+
const configValue = this.config.enablePanInteraction ?? true;
|
|
12548
|
+
return typeof configValue === "function" ? true : configValue;
|
|
12549
|
+
}
|
|
12550
|
+
this.config.enablePanInteraction = enable;
|
|
12551
|
+
if (this.isInitialized && this.forceGraph) {
|
|
12552
|
+
this.getGraphInstance().enablePanInteraction(enable);
|
|
12553
|
+
}
|
|
12554
|
+
return this;
|
|
12555
|
+
}
|
|
12556
|
+
// =============================================================================
|
|
12557
|
+
// GRAPH CONTROLS METHODS
|
|
12558
|
+
// =============================================================================
|
|
12559
|
+
/**
|
|
12560
|
+
* Initialize graph controls if enabled
|
|
12561
|
+
*/
|
|
12562
|
+
initializeControls() {
|
|
12563
|
+
if (!this.config.controls?.enabled) return;
|
|
12564
|
+
const controlsConfig = {
|
|
12565
|
+
...this.config.controls,
|
|
12566
|
+
enabled: true
|
|
12567
|
+
};
|
|
12568
|
+
if (!controlsConfig.position) {
|
|
12569
|
+
controlsConfig.position = "bottom-left";
|
|
12570
|
+
}
|
|
12571
|
+
const controlActions = {
|
|
12572
|
+
zoomIn: () => this.zoomIn(),
|
|
12573
|
+
zoomOut: () => this.zoomOut(),
|
|
12574
|
+
fitView: () => this.fitView(),
|
|
12575
|
+
resetView: () => this.resetView()
|
|
12576
|
+
};
|
|
12577
|
+
try {
|
|
12578
|
+
this.controlsInstance = createGraphControls(this.container, controlActions, controlsConfig);
|
|
12579
|
+
this.controlsInstance.mount();
|
|
12580
|
+
} catch {
|
|
12581
|
+
}
|
|
12582
|
+
}
|
|
12583
|
+
/**
|
|
12584
|
+
* Zoom in by a factor of 1.5
|
|
12585
|
+
*/
|
|
12586
|
+
zoomIn() {
|
|
12587
|
+
if (!this.isInitialized) return;
|
|
12588
|
+
const currentZoom = this.zoom();
|
|
12589
|
+
this.zoom(currentZoom * 1.5, 200);
|
|
12590
|
+
}
|
|
12591
|
+
/**
|
|
12592
|
+
* Zoom out by a factor of 0.67
|
|
12593
|
+
*/
|
|
12594
|
+
zoomOut() {
|
|
12595
|
+
if (!this.isInitialized) return;
|
|
12596
|
+
const currentZoom = this.zoom();
|
|
12597
|
+
this.zoom(currentZoom * 0.67, 200);
|
|
12598
|
+
}
|
|
12599
|
+
/**
|
|
12600
|
+
* Fit the graph to the viewport
|
|
12601
|
+
*/
|
|
12602
|
+
fitView() {
|
|
12603
|
+
this.zoomToFit(20, 300);
|
|
12604
|
+
}
|
|
12605
|
+
/**
|
|
12606
|
+
* Reset the view to initial state
|
|
12607
|
+
*/
|
|
12608
|
+
resetView() {
|
|
12609
|
+
this.centerAt(0, 0, 300);
|
|
12610
|
+
this.zoom(1, 300);
|
|
12611
|
+
}
|
|
12612
|
+
/**
|
|
12613
|
+
* Cleanup graph controls
|
|
12614
|
+
*/
|
|
12615
|
+
cleanupControls() {
|
|
12616
|
+
if (this.controlsInstance) {
|
|
12617
|
+
this.controlsInstance.destroy();
|
|
12618
|
+
this.controlsInstance = null;
|
|
12619
|
+
}
|
|
12620
|
+
}
|
|
12621
|
+
/**
|
|
12622
|
+
* Initialize graph legends if enabled
|
|
12623
|
+
*/
|
|
12624
|
+
initializeLegends() {
|
|
12625
|
+
if (!this.config.legends?.enabled) return;
|
|
12626
|
+
const legendsConfig = {
|
|
12627
|
+
...this.config.legends,
|
|
12628
|
+
enabled: true
|
|
12629
|
+
};
|
|
12630
|
+
if (!legendsConfig.position) {
|
|
12631
|
+
legendsConfig.position = "top-right";
|
|
12632
|
+
}
|
|
12633
|
+
try {
|
|
12634
|
+
this.legendsInstance = createGraphLegends(this.container, legendsConfig);
|
|
12635
|
+
this.legendsInstance.mount();
|
|
12636
|
+
this.updateLegends();
|
|
12637
|
+
} catch {
|
|
12638
|
+
}
|
|
12639
|
+
}
|
|
12640
|
+
/**
|
|
12641
|
+
* Update legends with current graph data
|
|
12642
|
+
*/
|
|
12643
|
+
updateLegends() {
|
|
12644
|
+
if (!this.legendsInstance || !this.config.legends?.enabled) return;
|
|
12645
|
+
const graphData = this.config.graphData;
|
|
12646
|
+
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) return;
|
|
12647
|
+
const nodeTypes = Array.from(
|
|
12648
|
+
new Set(
|
|
12649
|
+
graphData.nodes.map((node) => node.data?.type).filter((type) => Boolean(type && typeof type === "string"))
|
|
12650
|
+
)
|
|
12651
|
+
);
|
|
12652
|
+
const colorMap = {};
|
|
12653
|
+
const nodeColorAccessor = this.config.nodeColor;
|
|
12654
|
+
if (nodeColorAccessor && typeof nodeColorAccessor === "function") {
|
|
12655
|
+
graphData.nodes.forEach((node) => {
|
|
12656
|
+
const type = node.data?.type;
|
|
12657
|
+
if (type) {
|
|
12658
|
+
const color2 = nodeColorAccessor(node);
|
|
12659
|
+
if (color2) {
|
|
12660
|
+
colorMap[type] = color2;
|
|
12661
|
+
}
|
|
12662
|
+
}
|
|
12663
|
+
});
|
|
12664
|
+
} else if (nodeColorAccessor && typeof nodeColorAccessor === "string") {
|
|
12665
|
+
nodeTypes.forEach((type) => {
|
|
12666
|
+
if (type) {
|
|
12667
|
+
colorMap[type] = nodeColorAccessor;
|
|
12668
|
+
}
|
|
12669
|
+
});
|
|
12670
|
+
} else {
|
|
12671
|
+
const defaultColors = [
|
|
12672
|
+
"#3b82f6",
|
|
12673
|
+
"#ef4444",
|
|
12674
|
+
"#10b981",
|
|
12675
|
+
"#f59e0b",
|
|
12676
|
+
"#8b5cf6",
|
|
12677
|
+
"#06b6d4",
|
|
12678
|
+
"#84cc16",
|
|
12679
|
+
"#f97316",
|
|
12680
|
+
"#ec4899",
|
|
12681
|
+
"#6b7280"
|
|
12682
|
+
];
|
|
12683
|
+
nodeTypes.forEach((type, index2) => {
|
|
12684
|
+
const color2 = defaultColors[index2 % defaultColors.length];
|
|
12685
|
+
if (type && color2) {
|
|
12686
|
+
colorMap[type] = color2;
|
|
12687
|
+
}
|
|
12688
|
+
});
|
|
12689
|
+
}
|
|
12690
|
+
this.legendsInstance.update(nodeTypes, colorMap);
|
|
12691
|
+
}
|
|
12692
|
+
/**
|
|
12693
|
+
* Cleanup graph legends
|
|
12694
|
+
*/
|
|
12695
|
+
cleanupLegends() {
|
|
12696
|
+
if (this.legendsInstance) {
|
|
12697
|
+
this.legendsInstance.destroy();
|
|
12698
|
+
this.legendsInstance = null;
|
|
12699
|
+
}
|
|
12700
|
+
}
|
|
12701
|
+
/**
|
|
12702
|
+
* Draw legends on the export canvas (similar to React implementation)
|
|
12703
|
+
*/
|
|
12704
|
+
drawLegendsOnExportCanvas(ctx, canvasWidth, canvasHeight) {
|
|
12705
|
+
if (!this.config.legends?.enabled || !this.legendsInstance) return;
|
|
12706
|
+
const graphData = this.config.graphData;
|
|
12707
|
+
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) return;
|
|
12708
|
+
const nodeTypes = Array.from(
|
|
12709
|
+
new Set(
|
|
12710
|
+
graphData.nodes.map((node) => node.data?.type).filter((type) => Boolean(type && typeof type === "string"))
|
|
12711
|
+
)
|
|
12712
|
+
).sort();
|
|
12713
|
+
if (nodeTypes.length === 0) return;
|
|
12714
|
+
const colorMap = {};
|
|
12715
|
+
const nodeColorAccessor = this.config.nodeColor;
|
|
12716
|
+
if (nodeColorAccessor && typeof nodeColorAccessor === "function") {
|
|
12717
|
+
graphData.nodes.forEach((node) => {
|
|
12718
|
+
const type = node.data?.type;
|
|
12719
|
+
if (type) {
|
|
12720
|
+
const color2 = nodeColorAccessor(node);
|
|
12721
|
+
if (color2) {
|
|
12722
|
+
colorMap[type] = color2;
|
|
12723
|
+
}
|
|
12724
|
+
}
|
|
12725
|
+
});
|
|
12726
|
+
} else {
|
|
12727
|
+
const defaultColors = [
|
|
12728
|
+
"#3b82f6",
|
|
12729
|
+
"#ef4444",
|
|
12730
|
+
"#10b981",
|
|
12731
|
+
"#f59e0b",
|
|
12732
|
+
"#8b5cf6",
|
|
12733
|
+
"#06b6d4",
|
|
12734
|
+
"#84cc16",
|
|
12735
|
+
"#f97316",
|
|
12736
|
+
"#ec4899",
|
|
12737
|
+
"#6b7280"
|
|
12738
|
+
];
|
|
12739
|
+
nodeTypes.forEach((type, index2) => {
|
|
12740
|
+
const color2 = defaultColors[index2 % defaultColors.length];
|
|
12741
|
+
if (type && color2) {
|
|
12742
|
+
colorMap[type] = color2;
|
|
12743
|
+
}
|
|
12744
|
+
});
|
|
12745
|
+
}
|
|
12746
|
+
const LEGEND_CONFIG = {
|
|
12747
|
+
padding: 20,
|
|
12748
|
+
circleRadius: 10,
|
|
12749
|
+
lineHeight: 40,
|
|
12750
|
+
circleTextGap: 12,
|
|
12751
|
+
containerPadding: 16,
|
|
12752
|
+
borderRadius: 16
|
|
12753
|
+
};
|
|
12754
|
+
ctx.save();
|
|
12755
|
+
ctx.font = "20px sans-serif";
|
|
12756
|
+
ctx.textBaseline = "middle";
|
|
12757
|
+
let maxTextWidth = 0;
|
|
12758
|
+
nodeTypes.forEach((type) => {
|
|
12759
|
+
if (type) {
|
|
12760
|
+
const textWidth = ctx.measureText(type).width;
|
|
12761
|
+
maxTextWidth = Math.max(maxTextWidth, textWidth);
|
|
12762
|
+
}
|
|
12763
|
+
});
|
|
12764
|
+
const legendContentWidth = LEGEND_CONFIG.circleRadius * 2 + LEGEND_CONFIG.circleTextGap + maxTextWidth;
|
|
12765
|
+
const legendContentHeight = nodeTypes.length * LEGEND_CONFIG.lineHeight - (LEGEND_CONFIG.lineHeight - LEGEND_CONFIG.circleRadius * 2);
|
|
12766
|
+
const containerWidth = legendContentWidth + LEGEND_CONFIG.containerPadding * 2;
|
|
12767
|
+
const containerHeight = legendContentHeight + LEGEND_CONFIG.containerPadding * 2;
|
|
12768
|
+
const position = this.config.legends.position || "top-right";
|
|
12769
|
+
let containerX, containerY;
|
|
12770
|
+
switch (position) {
|
|
12771
|
+
case "top-left":
|
|
12772
|
+
containerX = LEGEND_CONFIG.padding;
|
|
12773
|
+
containerY = LEGEND_CONFIG.padding;
|
|
12774
|
+
break;
|
|
12775
|
+
case "top-right":
|
|
12776
|
+
default:
|
|
12777
|
+
containerX = canvasWidth - containerWidth - LEGEND_CONFIG.padding;
|
|
12778
|
+
containerY = LEGEND_CONFIG.padding;
|
|
12779
|
+
break;
|
|
12780
|
+
case "bottom-left":
|
|
12781
|
+
containerX = LEGEND_CONFIG.padding;
|
|
12782
|
+
containerY = canvasHeight - containerHeight - LEGEND_CONFIG.padding;
|
|
12783
|
+
break;
|
|
12784
|
+
case "bottom-right":
|
|
12785
|
+
containerX = canvasWidth - containerWidth - LEGEND_CONFIG.padding;
|
|
12786
|
+
containerY = canvasHeight - containerHeight - LEGEND_CONFIG.padding;
|
|
12787
|
+
break;
|
|
12788
|
+
}
|
|
12789
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.1)";
|
|
12790
|
+
ctx.shadowBlur = 10;
|
|
12791
|
+
ctx.shadowOffsetX = 0;
|
|
12792
|
+
ctx.shadowOffsetY = 2;
|
|
12793
|
+
ctx.fillStyle = "#ffffff";
|
|
12794
|
+
this.drawRoundedRect(ctx, containerX, containerY, containerWidth, containerHeight, LEGEND_CONFIG.borderRadius);
|
|
12795
|
+
ctx.fill();
|
|
12796
|
+
ctx.restore();
|
|
12797
|
+
const startX = containerX + LEGEND_CONFIG.containerPadding;
|
|
12798
|
+
const startY = containerY + LEGEND_CONFIG.containerPadding;
|
|
12799
|
+
nodeTypes.forEach((type, index2) => {
|
|
12800
|
+
if (!type) return;
|
|
12801
|
+
const y3 = startY + index2 * LEGEND_CONFIG.lineHeight;
|
|
12802
|
+
const color2 = colorMap[type] || "#ccc";
|
|
12803
|
+
ctx.beginPath();
|
|
12804
|
+
ctx.arc(startX + LEGEND_CONFIG.circleRadius, y3 + LEGEND_CONFIG.circleRadius, LEGEND_CONFIG.circleRadius, 0, 2 * Math.PI);
|
|
12805
|
+
ctx.fillStyle = color2;
|
|
12806
|
+
ctx.fill();
|
|
12807
|
+
ctx.fillStyle = "#374151";
|
|
12808
|
+
ctx.fillText(type, startX + LEGEND_CONFIG.circleRadius * 2 + LEGEND_CONFIG.circleTextGap, y3 + LEGEND_CONFIG.circleRadius);
|
|
12809
|
+
});
|
|
12810
|
+
}
|
|
12811
|
+
/**
|
|
12812
|
+
* Draw rounded rectangle helper method
|
|
12813
|
+
*/
|
|
12814
|
+
drawRoundedRect(ctx, x3, y3, width, height, radius) {
|
|
12815
|
+
ctx.beginPath();
|
|
12816
|
+
ctx.moveTo(x3 + radius, y3);
|
|
12817
|
+
ctx.lineTo(x3 + width - radius, y3);
|
|
12818
|
+
ctx.quadraticCurveTo(x3 + width, y3, x3 + width, y3 + radius);
|
|
12819
|
+
ctx.lineTo(x3 + width, y3 + height - radius);
|
|
12820
|
+
ctx.quadraticCurveTo(x3 + width, y3 + height, x3 + width - radius, y3 + height);
|
|
12821
|
+
ctx.lineTo(x3 + radius, y3 + height);
|
|
12822
|
+
ctx.quadraticCurveTo(x3, y3 + height, x3, y3 + height - radius);
|
|
12823
|
+
ctx.lineTo(x3, y3 + radius);
|
|
12824
|
+
ctx.quadraticCurveTo(x3, y3, x3 + radius, y3);
|
|
12825
|
+
ctx.closePath();
|
|
12826
|
+
}
|
|
12827
|
+
/**
|
|
12828
|
+
* Export graph as PNG image
|
|
12829
|
+
*/
|
|
12830
|
+
async exportGraph(fileName) {
|
|
12831
|
+
if (!this.isInitialized) {
|
|
12832
|
+
throw new Error("Graph not initialized");
|
|
12833
|
+
}
|
|
12834
|
+
try {
|
|
12835
|
+
const defaultFileName = `polly-graph-export-${Date.now()}.png`;
|
|
12836
|
+
const finalFileName = fileName || defaultFileName;
|
|
12837
|
+
this.zoomToFit(40, 300);
|
|
12838
|
+
await new Promise((resolve) => {
|
|
12839
|
+
this.exportTimeoutId = setTimeout(() => {
|
|
12840
|
+
this.exportTimeoutId = null;
|
|
12841
|
+
resolve(void 0);
|
|
12842
|
+
}, 350);
|
|
12843
|
+
});
|
|
12844
|
+
const canvas = this.container.querySelector("canvas");
|
|
12845
|
+
if (!canvas) {
|
|
12846
|
+
throw new Error("Canvas not found");
|
|
12847
|
+
}
|
|
12848
|
+
const elementsToHide = [];
|
|
12849
|
+
const controls = this.container.querySelectorAll(".fg-controls");
|
|
12850
|
+
controls.forEach((control) => {
|
|
12851
|
+
const element = control;
|
|
12852
|
+
elementsToHide.push({ element, originalDisplay: element.style.display });
|
|
12853
|
+
element.style.display = "none";
|
|
12854
|
+
});
|
|
12855
|
+
const legends = this.container.querySelectorAll(".fg-legends");
|
|
12856
|
+
legends.forEach((legend) => {
|
|
12857
|
+
const element = legend;
|
|
12858
|
+
elementsToHide.push({ element, originalDisplay: element.style.display });
|
|
12859
|
+
element.style.display = "none";
|
|
12860
|
+
});
|
|
12861
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
12862
|
+
const exportCanvas = document.createElement("canvas");
|
|
12863
|
+
const ctx = exportCanvas.getContext("2d");
|
|
12864
|
+
if (!ctx) {
|
|
12865
|
+
throw new Error("Could not get canvas context");
|
|
12866
|
+
}
|
|
12867
|
+
exportCanvas.width = canvas.width;
|
|
12868
|
+
exportCanvas.height = canvas.height;
|
|
12869
|
+
ctx.fillStyle = this.config.backgroundColor || "#ffffff";
|
|
12870
|
+
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
|
12871
|
+
ctx.drawImage(canvas, 0, 0);
|
|
12872
|
+
this.drawLegendsOnExportCanvas(ctx, exportCanvas.width, exportCanvas.height);
|
|
12873
|
+
exportCanvas.toBlob((blob) => {
|
|
12874
|
+
if (!blob) {
|
|
12875
|
+
throw new Error("Failed to create image blob");
|
|
12876
|
+
}
|
|
12877
|
+
const url = URL.createObjectURL(blob);
|
|
12878
|
+
const link = document.createElement("a");
|
|
12879
|
+
link.href = url;
|
|
12880
|
+
link.download = finalFileName;
|
|
12881
|
+
document.body.appendChild(link);
|
|
12882
|
+
link.click();
|
|
12883
|
+
document.body.removeChild(link);
|
|
12884
|
+
URL.revokeObjectURL(url);
|
|
12885
|
+
}, "image/png");
|
|
12886
|
+
elementsToHide.forEach(({ element, originalDisplay }) => {
|
|
12887
|
+
element.style.display = originalDisplay;
|
|
12888
|
+
});
|
|
12889
|
+
} catch (error) {
|
|
12890
|
+
const controls = this.container.querySelectorAll(".fg-controls");
|
|
12891
|
+
controls.forEach((control) => {
|
|
12892
|
+
const element = control;
|
|
12893
|
+
element.style.display = "";
|
|
12894
|
+
});
|
|
12895
|
+
const legends = this.container.querySelectorAll(".fg-legends");
|
|
12896
|
+
legends.forEach((legend) => {
|
|
12897
|
+
const element = legend;
|
|
12898
|
+
element.style.display = "";
|
|
12899
|
+
});
|
|
12900
|
+
throw error;
|
|
12901
|
+
}
|
|
12902
|
+
}
|
|
12903
|
+
/**
|
|
12904
|
+
* Setup double-click functionality
|
|
12905
|
+
* - Double-click on node: center and zoom to node
|
|
12906
|
+
* - Double-click on canvas: fit view
|
|
12907
|
+
*/
|
|
12908
|
+
setupDoubleClickHandling() {
|
|
12909
|
+
if (!this.isInitialized) return;
|
|
12910
|
+
const canvas = this.container.querySelector("canvas");
|
|
12911
|
+
if (!canvas) return;
|
|
12912
|
+
let clickTimeout = null;
|
|
12913
|
+
let clickCount = 0;
|
|
12914
|
+
let lastClickTime = 0;
|
|
12915
|
+
const DOUBLE_CLICK_DELAY = 300;
|
|
12916
|
+
const originalOnNodeClick = this.config.onNodeClick;
|
|
12917
|
+
if (this.canvasClickHandler) {
|
|
12918
|
+
canvas.removeEventListener("click", this.canvasClickHandler);
|
|
12919
|
+
}
|
|
12920
|
+
this.canvasClickHandler = (event) => {
|
|
12921
|
+
const currentTime = Date.now();
|
|
12922
|
+
clickCount++;
|
|
12923
|
+
if (clickCount === 1) {
|
|
12924
|
+
clickTimeout = window.setTimeout(() => {
|
|
12925
|
+
if (clickCount === 1) {
|
|
12926
|
+
const rect = canvas.getBoundingClientRect();
|
|
12927
|
+
const x3 = event.clientX - rect.left;
|
|
12928
|
+
const y3 = event.clientY - rect.top;
|
|
12929
|
+
const graphCoords = this.getGraphInstance().screen2GraphCoords(x3, y3);
|
|
12930
|
+
const clickedNode = this.findNodeAtPosition(graphCoords.x, graphCoords.y);
|
|
12931
|
+
if (clickedNode && originalOnNodeClick) {
|
|
12932
|
+
originalOnNodeClick(clickedNode, event);
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
clickCount = 0;
|
|
12936
|
+
}, DOUBLE_CLICK_DELAY);
|
|
12937
|
+
} else if (clickCount === 2 && currentTime - lastClickTime < DOUBLE_CLICK_DELAY) {
|
|
12938
|
+
if (clickTimeout) {
|
|
12939
|
+
window.clearTimeout(clickTimeout);
|
|
12940
|
+
clickTimeout = null;
|
|
12941
|
+
}
|
|
12942
|
+
const rect = canvas.getBoundingClientRect();
|
|
12943
|
+
const x3 = event.clientX - rect.left;
|
|
12944
|
+
const y3 = event.clientY - rect.top;
|
|
12945
|
+
const graphCoords = this.getGraphInstance().screen2GraphCoords(x3, y3);
|
|
12946
|
+
const clickedNode = this.findNodeAtPosition(graphCoords.x, graphCoords.y);
|
|
12947
|
+
if (clickedNode) {
|
|
12948
|
+
if (this.config.onNodeDoubleClick) {
|
|
12949
|
+
this.config.onNodeDoubleClick(clickedNode, event);
|
|
12950
|
+
} else {
|
|
12951
|
+
this.centerAt(clickedNode.x || 0, clickedNode.y || 0, 400);
|
|
12952
|
+
this.zoom(8, 400);
|
|
12953
|
+
}
|
|
12954
|
+
} else {
|
|
12955
|
+
this.smoothZoomToFit(500);
|
|
12956
|
+
}
|
|
12957
|
+
clickCount = 0;
|
|
12958
|
+
} else {
|
|
12959
|
+
clickCount = 1;
|
|
12960
|
+
if (clickTimeout) {
|
|
12961
|
+
window.clearTimeout(clickTimeout);
|
|
12962
|
+
}
|
|
12963
|
+
clickTimeout = window.setTimeout(() => {
|
|
12964
|
+
clickCount = 0;
|
|
12965
|
+
}, DOUBLE_CLICK_DELAY);
|
|
12966
|
+
}
|
|
12967
|
+
lastClickTime = currentTime;
|
|
12968
|
+
};
|
|
12969
|
+
canvas.addEventListener("click", this.canvasClickHandler);
|
|
12970
|
+
if (this.forceGraph && originalOnNodeClick) {
|
|
12971
|
+
this.getGraphInstance().onNodeClick(null);
|
|
12972
|
+
}
|
|
12973
|
+
}
|
|
12974
|
+
/**
|
|
12975
|
+
* Smooth zoom to fit that animates from current position
|
|
12976
|
+
*/
|
|
12977
|
+
smoothZoomToFit(duration = 500) {
|
|
12978
|
+
if (!this.isInitialized) this.initializeForceGraph();
|
|
12979
|
+
if (this.forceGraph && this.container) {
|
|
12980
|
+
const bbox = this.getGraphInstance().getGraphBbox();
|
|
12981
|
+
const width = bbox.x[1] - bbox.x[0];
|
|
12982
|
+
const height = bbox.y[1] - bbox.y[0];
|
|
12983
|
+
const centerX = (bbox.x[0] + bbox.x[1]) / 2;
|
|
12984
|
+
const centerY = (bbox.y[0] + bbox.y[1]) / 2;
|
|
12985
|
+
const paddingValue = 50;
|
|
12986
|
+
const viewportWidth = this.container.clientWidth - paddingValue;
|
|
12987
|
+
const viewportHeight = this.container.clientHeight - paddingValue;
|
|
12988
|
+
const targetZoom = Math.min(viewportWidth / width, viewportHeight / height);
|
|
12989
|
+
this.getGraphInstance().centerAt(centerX, centerY, duration / 2);
|
|
12990
|
+
if (this.kapsuleTimeoutId) {
|
|
12991
|
+
clearTimeout(this.kapsuleTimeoutId);
|
|
12992
|
+
}
|
|
12993
|
+
this.kapsuleTimeoutId = setTimeout(() => {
|
|
12994
|
+
this.zoom(targetZoom, duration / 2);
|
|
12995
|
+
this.kapsuleTimeoutId = null;
|
|
12996
|
+
}, duration / 2);
|
|
12997
|
+
}
|
|
12998
|
+
}
|
|
12999
|
+
/**
|
|
13000
|
+
* Trigger Kapsule update to ensure render callbacks continue working
|
|
13001
|
+
* This is essential for onRenderFramePost callbacks after physics simulation stops
|
|
13002
|
+
*/
|
|
13003
|
+
triggerKapsuleUpdate() {
|
|
13004
|
+
if (!this.isInitialized || !this.forceGraph) return;
|
|
13005
|
+
try {
|
|
13006
|
+
const kapsuleInstance = this._getForceGraphInstance();
|
|
13007
|
+
if (kapsuleInstance) {
|
|
13008
|
+
const state = kapsuleInstance["__state"];
|
|
13009
|
+
if (state && typeof state === "object") {
|
|
13010
|
+
state["needsRedraw"] = true;
|
|
13011
|
+
return;
|
|
13012
|
+
}
|
|
13013
|
+
const zoomFn = kapsuleInstance["zoom"];
|
|
13014
|
+
if (typeof zoomFn === "function") {
|
|
13015
|
+
const currentZoom = zoomFn();
|
|
13016
|
+
if (typeof currentZoom === "number") {
|
|
13017
|
+
zoomFn(currentZoom);
|
|
13018
|
+
}
|
|
13019
|
+
}
|
|
13020
|
+
}
|
|
13021
|
+
} catch {
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
/**
|
|
13025
|
+
* Find node at given graph coordinates
|
|
13026
|
+
*/
|
|
13027
|
+
findNodeAtPosition(graphX, graphY) {
|
|
13028
|
+
if (!this.config.graphData?.nodes) return null;
|
|
13029
|
+
const nodeRadius = 4;
|
|
13030
|
+
const tolerance = nodeRadius + 2;
|
|
13031
|
+
for (const node of this.config.graphData.nodes) {
|
|
13032
|
+
if (typeof node.x === "number" && typeof node.y === "number") {
|
|
13033
|
+
const distance = Math.sqrt(
|
|
13034
|
+
Math.pow(node.x - graphX, 2) + Math.pow(node.y - graphY, 2)
|
|
13035
|
+
);
|
|
13036
|
+
if (distance <= tolerance) {
|
|
13037
|
+
return node;
|
|
13038
|
+
}
|
|
13039
|
+
}
|
|
13040
|
+
}
|
|
13041
|
+
return null;
|
|
13042
|
+
}
|
|
13043
|
+
destroy() {
|
|
13044
|
+
if (this.isDestroyed) return;
|
|
13045
|
+
this.cleanupControls();
|
|
13046
|
+
this.cleanupLegends();
|
|
13047
|
+
this.cleanupPageVisibilityHandling();
|
|
13048
|
+
this.cleanupDeterministicPhysics();
|
|
13049
|
+
this.cleanupDeferredFitView();
|
|
13050
|
+
this.cleanupPerformanceTest();
|
|
13051
|
+
this.cleanupExportTimeout();
|
|
13052
|
+
this.cleanupVisibilityTimeouts();
|
|
13053
|
+
this.cleanupCanvasEventListeners();
|
|
13054
|
+
physicsWorkerManager.terminate();
|
|
13055
|
+
this.stopRenderingMonitoring();
|
|
13056
|
+
if (this.canvasOptimizer) {
|
|
13057
|
+
this.canvasOptimizer.destroy?.();
|
|
13058
|
+
}
|
|
13059
|
+
if (this.forceGraph) {
|
|
13060
|
+
this.getGraphInstance().onRenderFramePre(void 0);
|
|
13061
|
+
this.getGraphInstance().onRenderFramePost(void 0);
|
|
13062
|
+
this.getGraphInstance().onNodeClick(void 0);
|
|
13063
|
+
this.getGraphInstance().onNodeHover(void 0);
|
|
13064
|
+
this.getGraphInstance().onLinkClick(void 0);
|
|
13065
|
+
this.getGraphInstance().onLinkHover(void 0);
|
|
13066
|
+
this.getGraphInstance().onEngineTick(void 0);
|
|
13067
|
+
this.getGraphInstance().onEngineStop(void 0);
|
|
13068
|
+
const graphInstance = this.getGraphInstance();
|
|
13069
|
+
if (typeof graphInstance.destroy === "function") {
|
|
13070
|
+
graphInstance.destroy();
|
|
13071
|
+
}
|
|
13072
|
+
}
|
|
13073
|
+
if (this.container) {
|
|
13074
|
+
this.container.innerHTML = "";
|
|
13075
|
+
}
|
|
13076
|
+
this.performanceMonitor.logSummary();
|
|
13077
|
+
this.forceGraph = null;
|
|
13078
|
+
this.renderingMonitor = null;
|
|
13079
|
+
this.canvasOptimizer = null;
|
|
13080
|
+
this.isDestroyed = true;
|
|
13081
|
+
this.isInitialized = false;
|
|
13082
|
+
}
|
|
13083
|
+
_getForceGraphInstance() {
|
|
13084
|
+
return this.forceGraph;
|
|
13085
|
+
}
|
|
13086
|
+
};
|
|
13087
|
+
function createForceGraph(container, config) {
|
|
13088
|
+
return new ForceGraphWrapper(container, config);
|
|
13089
|
+
}
|
|
13090
|
+
|
|
13091
|
+
// src/force-graph-wrapper/types/config.types.ts
|
|
13092
|
+
var ConfigPresets = {
|
|
13093
|
+
/**
|
|
13094
|
+
* High-performance preset for large graphs (5000+ nodes)
|
|
13095
|
+
*/
|
|
13096
|
+
highPerformance: () => ({
|
|
13097
|
+
autoPauseRedraw: true,
|
|
13098
|
+
warmupTicks: 100,
|
|
13099
|
+
cooldownTime: 1e3,
|
|
13100
|
+
d3AlphaDecay: 0.05,
|
|
13101
|
+
d3VelocityDecay: 0.4,
|
|
13102
|
+
enablePerformanceMonitoring: true,
|
|
13103
|
+
performanceTargets: {
|
|
13104
|
+
creation: 5,
|
|
13105
|
+
// 5ms
|
|
13106
|
+
dataLoad: 100,
|
|
13107
|
+
// 100ms
|
|
13108
|
+
firstRender: 500,
|
|
13109
|
+
// 500ms
|
|
13110
|
+
methodCall: 1,
|
|
13111
|
+
// 1ms
|
|
13112
|
+
memoryGrowth: 10
|
|
13113
|
+
// 10MB per 1000 nodes
|
|
13114
|
+
}
|
|
13115
|
+
}),
|
|
13116
|
+
/**
|
|
13117
|
+
* Interactive preset for medium graphs (50-1000 nodes)
|
|
13118
|
+
*/
|
|
13119
|
+
interactive: () => ({
|
|
13120
|
+
enablePointerInteraction: true,
|
|
13121
|
+
enableNodeDrag: true,
|
|
13122
|
+
enableZoomInteraction: true,
|
|
13123
|
+
enablePanInteraction: true,
|
|
13124
|
+
linkHoverPrecision: 2,
|
|
13125
|
+
cooldownTime: 2e3,
|
|
13126
|
+
controls: {
|
|
13127
|
+
enabled: true,
|
|
13128
|
+
position: "bottom-left",
|
|
13129
|
+
orientation: "vertical"
|
|
13130
|
+
},
|
|
13131
|
+
legends: {
|
|
13132
|
+
enabled: true,
|
|
13133
|
+
position: "top-right",
|
|
13134
|
+
maxItems: 10
|
|
13135
|
+
},
|
|
13136
|
+
performanceTargets: {
|
|
13137
|
+
creation: 1,
|
|
13138
|
+
dataLoad: 20,
|
|
13139
|
+
firstRender: 50,
|
|
13140
|
+
methodCall: 0.5,
|
|
13141
|
+
memoryGrowth: 5
|
|
13142
|
+
}
|
|
13143
|
+
}),
|
|
13144
|
+
/**
|
|
13145
|
+
* Minimal preset for small graphs (5-50 nodes)
|
|
13146
|
+
*/
|
|
13147
|
+
minimal: () => ({
|
|
13148
|
+
width: 400,
|
|
13149
|
+
height: 300,
|
|
13150
|
+
backgroundColor: "#ffffff",
|
|
13151
|
+
cooldownTime: 1e3,
|
|
13152
|
+
enablePerformanceMonitoring: false
|
|
13153
|
+
})
|
|
13154
|
+
};
|
|
13155
|
+
var ForceGraphConfigBuilder = class {
|
|
13156
|
+
config = {};
|
|
13157
|
+
constructor(container) {
|
|
13158
|
+
this.config.container = container;
|
|
13159
|
+
}
|
|
13160
|
+
data(graphData) {
|
|
13161
|
+
this.config.graphData = graphData;
|
|
13162
|
+
return this;
|
|
13163
|
+
}
|
|
13164
|
+
size(width, height) {
|
|
13165
|
+
this.config.width = width;
|
|
13166
|
+
this.config.height = height;
|
|
13167
|
+
return this;
|
|
13168
|
+
}
|
|
13169
|
+
performance(preset) {
|
|
13170
|
+
Object.assign(this.config, ConfigPresets[preset]());
|
|
13171
|
+
return this;
|
|
13172
|
+
}
|
|
13173
|
+
nodeColor(accessor) {
|
|
13174
|
+
this.config.nodeColor = accessor;
|
|
13175
|
+
return this;
|
|
13176
|
+
}
|
|
13177
|
+
onNodeClick(handler) {
|
|
13178
|
+
this.config.onNodeClick = handler;
|
|
13179
|
+
return this;
|
|
13180
|
+
}
|
|
13181
|
+
build() {
|
|
13182
|
+
if (!this.config.container || !this.config.graphData) {
|
|
13183
|
+
throw new Error("Container and graphData are required");
|
|
13184
|
+
}
|
|
13185
|
+
return this.config;
|
|
13186
|
+
}
|
|
13187
|
+
};
|
|
13188
|
+
|
|
13189
|
+
// src/force-graph-wrapper/index.ts
|
|
13190
|
+
var VERSION = "1.0.0-alpha.1";
|
|
13191
|
+
var LIBRARY_INFO = {
|
|
13192
|
+
name: "force-graph-wrapper",
|
|
13193
|
+
version: VERSION,
|
|
13194
|
+
description: "Framework-independent TypeScript wrapper for force-graph library",
|
|
13195
|
+
performance: "Built-in performance monitoring and optimization",
|
|
13196
|
+
compatibility: "Vanilla JS, React, Angular, Vue.js"
|
|
13197
|
+
};
|
|
10072
13198
|
export {
|
|
10073
13199
|
CanvasManager,
|
|
13200
|
+
ConfigPresets,
|
|
10074
13201
|
DEFAULT_COLORS,
|
|
10075
13202
|
DEFAULT_HOVER_STYLES,
|
|
10076
13203
|
DEFAULT_LINK_LABEL_STYLE,
|
|
10077
13204
|
DEFAULT_LINK_STYLE,
|
|
10078
13205
|
DragManager,
|
|
10079
13206
|
ErrorHandler,
|
|
13207
|
+
ForceGraphConfigBuilder,
|
|
13208
|
+
ForceGraphWrapper,
|
|
10080
13209
|
HoverManager,
|
|
13210
|
+
LIBRARY_INFO,
|
|
10081
13211
|
NeutralColor,
|
|
13212
|
+
PerformanceMonitor,
|
|
10082
13213
|
PhysicsManager,
|
|
10083
13214
|
PointerManager,
|
|
10084
13215
|
PrimaryColor,
|
|
@@ -10087,8 +13218,10 @@ export {
|
|
|
10087
13218
|
SelectionManager,
|
|
10088
13219
|
StandardColor,
|
|
10089
13220
|
V2Graph,
|
|
13221
|
+
VERSION,
|
|
10090
13222
|
ValidationError,
|
|
10091
13223
|
ZoomManager,
|
|
13224
|
+
createForceGraph,
|
|
10092
13225
|
createV2Graph,
|
|
10093
13226
|
getIcon,
|
|
10094
13227
|
getIconSvg,
|