@logixode/force-graph-lib 0.1.8 → 0.1.10

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.
@@ -9,6 +9,7 @@ class C {
9
9
  options;
10
10
  worker = null;
11
11
  groupBounds = /* @__PURE__ */ new Map();
12
+ isFirstRender = !0;
12
13
  constructor(t, i = { nodes: [], links: [] }, o = {}) {
13
14
  this.container = t, this.data = i, this.graph = new f(this.container), i.nodes.forEach((r) => {
14
15
  this.nodesMap.set(r.id.toString(), r);
@@ -34,7 +35,9 @@ class C {
34
35
  }, this.initGraph();
35
36
  }
36
37
  initGraph() {
37
- this.applyOptions(), this.render(), this.graphData(this.data), this.graph.onEngineStop(() => this.graph.zoomToFit(400));
38
+ this.applyOptions(), this.render(), this.graphData(this.data), this.graph.onEngineStop(() => {
39
+ this.isFirstRender ? (this.options.onRenderComplete ? this.options.onRenderComplete() : this.graph.zoomToFit(400), this.isFirstRender = !1) : this.options.onGraphUpdated && this.options.onGraphUpdated();
40
+ });
38
41
  }
39
42
  renderer() {
40
43
  return this.graph;
@@ -71,10 +74,10 @@ class C {
71
74
  }), this.applyLinkOptions();
72
75
  }
73
76
  getNodeSize(t) {
74
- return typeof this.options.nodeSize == "function" ? this.options.nodeSize(t) || t.marker?.radius : this.options.nodeSize || t.marker?.radius || 1;
77
+ return typeof this.options.nodeSize == "function" ? this.options.nodeSize(t) || t?.marker?.radius : this.options.nodeSize || t?.marker?.radius || 1;
75
78
  }
76
79
  getNodeLabel(t) {
77
- return typeof this.options.nodeLabel == "function" ? this.options.nodeLabel(t) : t.label || t.id;
80
+ return typeof this.options.nodeLabel == "function" ? this.options.nodeLabel(t) : t?.label || t.id;
78
81
  }
79
82
  /**
80
83
  * Check if label should be shown based on threshold logic
@@ -104,13 +107,15 @@ class C {
104
107
  const i = this.options.nodeColor(t);
105
108
  if (i) return i;
106
109
  }
107
- return t.color ?? "";
110
+ return t?.color ?? "";
108
111
  }
109
112
  getNodeBorderColor(t) {
110
113
  return typeof this.options.nodeBorderColor == "function" ? this.options.nodeBorderColor(t) : this.options.nodeBorderColor || "#333";
111
114
  }
112
115
  applyLinkOptions() {
113
- this.options.linkWidth !== void 0 && this.graph.linkWidth((t) => this.getLinkProperty(this.options.linkWidth, t) ?? 1), this.options.linkCurvature !== void 0 && this.graph.linkCurvature(this.getLinkCurvature.bind(this)), this.options.linkDirectionalParticles && this.graph.linkDirectionalParticles(
116
+ this.options.linkWidth !== void 0 && this.graph.linkWidth(
117
+ (t) => this.getLinkProperty(this.options.linkWidth, t) ?? 1
118
+ ), this.options.linkCurvature !== void 0 && this.graph.linkCurvature(this.getLinkCurvature.bind(this)), this.options.linkDirectionalParticles && this.graph.linkDirectionalParticles(
114
119
  (t) => this.getLinkProperty(this.options.linkDirectionalParticles, t) ?? 0
115
120
  ), this.options.linkDirectionalParticleSpeed !== void 0 && this.graph.linkDirectionalParticleSpeed(
116
121
  (t) => this.getLinkProperty(this.options.linkDirectionalParticleSpeed, t) ?? 0
@@ -239,7 +244,7 @@ class C {
239
244
  const i = t.toString();
240
245
  return this.nodesMap.has(i) ? (this.nodesMap.delete(i), this.data.nodes = this.data.nodes.filter((o) => o.id.toString() !== i), this.data.links = this.data.links.filter((o) => {
241
246
  const e = typeof o.source == "object" ? o.source.id : o.source, s = typeof o.target == "object" ? o.target.id : o.target;
242
- return e.toString() !== i && s.toString() !== i;
247
+ return e?.toString() !== i && s?.toString() !== i;
243
248
  }), this.graph.cooldownTime(this.getCooldownTime()), this.refreshGraph(), !0) : !1;
244
249
  }
245
250
  async addData(t) {
@@ -286,7 +291,7 @@ class C {
286
291
  return this.data.links;
287
292
  }
288
293
  createLinkKey(t, i) {
289
- const o = typeof t == "object" ? t.id : t, e = typeof i == "object" ? i.id : i;
294
+ const o = t === void 0 ? "undefined" : typeof t == "object" ? t.id : t, e = i === void 0 ? "undefined" : typeof i == "object" ? i.id : i;
290
295
  return `${o}-${e}`;
291
296
  }
292
297
  /**
@@ -1 +1 @@
1
- (function(a,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],l):(a=typeof globalThis<"u"?globalThis:a||self,l(a.ForceGraphLib={},a.ForceGraph,a.d3))})(this,(function(a,l,b){"use strict";function k(d){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(d){for(const i in d)if(i!=="default"){const o=Object.getOwnPropertyDescriptor(d,i);Object.defineProperty(t,i,o.get?o:{enumerable:!0,get:()=>d[i]})}}return t.default=d,Object.freeze(t)}const m=k(b);class C{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupBounds=new Map;constructor(t,i={nodes:[],links:[]},o={}){this.container=t,this.data=i,this.graph=new l(this.container),i.nodes.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e={labelThreshold:1.5,showGroups:!1,...o},s=e.showGroups?{groupBy:"topic",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,groupPadding:20}:{};this.options={...e,...s},this.initGraph()}initGraph(){this.applyOptions(),this.render(),this.graphData(this.data),this.graph.onEngineStop(()=>this.graph.zoomToFit(400))}renderer(){return this.graph}focusPosition(t={}){if(Object.values(t).length){if(t.id){const{x:i,y:o}=this.nodesMap.get(t.id)??{x:0,y:0};i&&o&&(t={x:i,y:o})}t&&(this.graph.centerAt(t.x,t.y,1e3),this.graph.zoom(8,2e3))}}render(){this.graph.width(this.options.width??800).height(this.options.height??400).cooldownTime(this.getCooldownTime()).d3Force("charge",m.forceManyBody().strength(this.options.nodeGap??-50)),typeof this.options.nodeClickHandler=="function"&&this.graph.onNodeClick(t=>{this.options.nodeClickHandler(t)})}force(t,i){return this.graph.d3Force(t,i)}applyOptions(){this.options.keepDragPosition&&this.graph.onNodeDragEnd(t=>{t.fx=t.x,t.fy=t.y}),this.options.pointerInteraction||this.graph.enablePointerInteraction(!1),this.graph.nodeCanvasObject((t,i,o)=>{t===this.data.nodes[0]&&this.renderGroups(i,o);const e=this.getNodeSize(t)*2,s=typeof this.options.nodeBorderWidth=="function"?this.options.nodeBorderWidth(t):this.options.nodeBorderWidth;i.beginPath(),i.arc(t.x||0,t.y||0,e,0,2*Math.PI),i.fillStyle=this.getNodeColor(t),i.fill(),s&&s>0&&(i.beginPath(),i.arc(t.x||0,t.y||0,e,0,2*Math.PI),i.strokeStyle=this.getNodeBorderColor(t),i.lineWidth=s,i.stroke());const r=this.getNodeLabel(t);if(r&&this.shouldShowLabel(t,o)){const p=typeof this.options.nodeLabelColor=="function"?this.options.nodeLabelColor(t):this.options.nodeLabelColor??"#555",n=(this.options.labelFontSize||14)/o;i.font=`${n}px Arial`,i.fillStyle=p,i.textAlign="center",i.textBaseline="middle",i.fillText(r,t.x||0,(t.y||0)+e+n/2+2)}}),this.applyLinkOptions()}getNodeSize(t){return typeof this.options.nodeSize=="function"?this.options.nodeSize(t)||t.marker?.radius:this.options.nodeSize||t.marker?.radius||1}getNodeLabel(t){return typeof this.options.nodeLabel=="function"?this.options.nodeLabel(t):t.label||t.id}shouldShowLabel(t,i){const o=this.getNodeSize(t)*2,s=(this.options.labelFontSize||14)/i,r=this.options.labelThreshold||1.5;return s<=o*r}updateData(t){const i=new Set(this.data.nodes.map(r=>r.id.toString())),o=t.nodes.filter(r=>!i.has(r.id.toString()));o.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e=new Set(this.data.links.map(r=>this.createLinkKey(r.source,r.target))),s=t.links.filter(r=>!e.has(this.createLinkKey(r.source,r.target)));this.data={nodes:[...this.data.nodes,...o],links:[...this.data.links,...s]},this.refreshGraph()}getNodeColor(t){if(typeof this.options.nodeColor=="function"){const i=this.options.nodeColor(t);if(i)return i}return t.color??""}getNodeBorderColor(t){return typeof this.options.nodeBorderColor=="function"?this.options.nodeBorderColor(t):this.options.nodeBorderColor||"#333"}applyLinkOptions(){this.options.linkWidth!==void 0&&this.graph.linkWidth(t=>this.getLinkProperty(this.options.linkWidth,t)??1),this.options.linkCurvature!==void 0&&this.graph.linkCurvature(this.getLinkCurvature.bind(this)),this.options.linkDirectionalParticles&&this.graph.linkDirectionalParticles(t=>this.getLinkProperty(this.options.linkDirectionalParticles,t)??0),this.options.linkDirectionalParticleSpeed!==void 0&&this.graph.linkDirectionalParticleSpeed(t=>this.getLinkProperty(this.options.linkDirectionalParticleSpeed,t)??0),this.options.linkDirectionalParticleWidth!==void 0&&this.graph.linkDirectionalParticleWidth(t=>this.getLinkProperty(this.options.linkDirectionalParticleWidth,t)??0),this.options.linkDirectionalParticleColor!==void 0&&this.graph.linkDirectionalParticleColor(t=>this.getLinkProperty(this.options.linkDirectionalParticleColor,t)??"#aaa")}getLinkCurvature(t){return typeof this.options.linkCurvature=="function"?this.options.linkCurvature(t):typeof this.options.linkCurvature=="string"?t[this.options.linkCurvature]||0:typeof this.options.linkCurvature=="number"?this.options.linkCurvature:t.curvature||0}getLinkProperty(t,i){return typeof t=="function"?t(i):t}calculateGroupBounds(){if(!this.options.showGroups)return;this.groupBounds.clear();const t=this.options.groupPadding||20,i=new Map;this.data.nodes.forEach(o=>{const e=this.getNodeGroupId(o);e&&(i.has(e)||i.set(e,[]),i.get(e).push(o))}),i.forEach((o,e)=>{if(o.length===0)return;let s=1/0,r=1/0,p=-1/0,u=-1/0;o.forEach(n=>{if(n.x!==void 0&&n.y!==void 0){const h=this.getNodeSize(n);s=Math.min(s,n.x-h),r=Math.min(r,n.y-h),p=Math.max(p,n.x+h),u=Math.max(u,n.y+h)}}),s!==1/0&&this.groupBounds.set(e,{minX:s-t,minY:r-t,maxX:p+t,maxY:u+t,nodes:o})})}getNodeGroupId(t){if(this.options.groupBy){if(typeof this.options.groupBy=="function")return this.options.groupBy(t);if(typeof this.options.groupBy=="string")return t[this.options.groupBy]}}renderGroups(t,i){this.options.showGroups&&(this.calculateGroupBounds(),this.groupBounds.forEach((o,e)=>{const s=this.getGroupBorderColor(e),r=this.options.groupBorderWidth||2,p=this.options.groupBorderOpacity||.3;t.save(),t.globalAlpha=p,t.strokeStyle=s,t.lineWidth=r/i,t.setLineDash([10/i,5/i]),t.strokeRect(o.minX,o.minY,o.maxX-o.minX,o.maxY-o.minY),t.restore();const u=this.options.groupLabelThreshold||.8;if(i<=u){const n=this.getGroupLabelColor(e),h=(this.options.groupLabelSize||16)/i;t.save(),t.font=`bold ${h}px Arial`,t.fillStyle=n,t.textAlign="center",t.textBaseline="middle";const g=(o.minX+o.maxX)/2,f=o.minY-h/2,c=t.measureText(e).width,y=h;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(g-c/2-4,f-y/2-2,c+8,y+4),t.globalAlpha=1,t.fillStyle=n,t.fillText(e,g,f),t.restore()}}))}getGroupBorderColor(t){return typeof this.options.groupBorderColor=="function"?this.options.groupBorderColor(t):this.options.groupBorderColor||"#666"}getGroupLabelColor(t){return typeof this.options.groupLabelColor=="function"?this.options.groupLabelColor(t):this.options.groupLabelColor||"#333"}getNodeById(t){return this.nodesMap.get(t.toString())}hasNode(t){return this.nodesMap.has(t.toString())}getCooldownTime(){const i=this.data.nodes.length*(125/100);return Math.max(4e3,i)}getDataSize(){const{nodes:t,links:i}=this.graph.graphData();return{nodes:t.length,links:i.length}}getAllNodeIds(){return Array.from(this.nodesMap.keys())}updateNode(t,i){const o=this.nodesMap.get(t.toString());if(o){Object.assign(o,i);const e=this.data.nodes.findIndex(s=>s.id.toString()===t.toString());return e!==-1&&(this.data.nodes[e]=o),this.refreshGraph(),!0}return!1}removeNode(t){const i=t.toString();return this.nodesMap.has(i)?(this.nodesMap.delete(i),this.data.nodes=this.data.nodes.filter(o=>o.id.toString()!==i),this.data.links=this.data.links.filter(o=>{const e=typeof o.source=="object"?o.source.id:o.source,s=typeof o.target=="object"?o.target.id:o.target;return e.toString()!==i&&s.toString()!==i}),this.graph.cooldownTime(this.getCooldownTime()),this.refreshGraph(),!0):!1}async addData(t){t.nodes.forEach(i=>{this.nodesMap.has(i.id.toString())||this.nodesMap.set(i.id.toString(),i)}),t.links.forEach(i=>{const o=this.createLinkKey(i.source,i.target);this.linkMap.has(o)?console.log("link already exists",i):this.linkMap.set(o,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.graph.cooldownTime(this.getCooldownTime()),this.graph.graphData(this.data)}setLabelThreshold(t){this.options.labelThreshold=t,this.render()}setOptions(t){this.options={...this.options,...t},this.applyOptions()}refreshGraph(){this.graphData(this.data)}reinitialize(){this.initGraph()}reset(){this.graph.pauseAnimation();const t=this.graph.d3Force("simulation");t&&t.stop(),this.data={nodes:[],links:[]},this.nodesMap.clear(),this.graph=new l(this.container),this.initGraph()}getData(){return this.data}getNodesData(){return this.data.nodes}getLinksData(){return this.data.links}createLinkKey(t,i){const o=typeof t=="object"?t.id:t,e=typeof i=="object"?i.id:i;return`${o}-${e}`}graphData(t){return this.nodesMap.clear(),this.linkMap.clear(),t.nodes.forEach(i=>{this.nodesMap.has(i.id.toString())||this.nodesMap.set(i.id.toString(),i)}),t.links.forEach(i=>{const o=this.createLinkKey(i.source,i.target);this.linkMap.has(o)||this.linkMap.set(o,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.graph.cooldownTime(this.getCooldownTime()),this.graph.graphData(this.data),this}showGroups(t){return this.options.showGroups=t,t&&Object.entries({groupBy:"topic",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,groupPadding:20}).forEach(([o,e])=>{this.options[o]===void 0&&(this.options[o]=e)}),this.applyOptions(),this.refreshGraph(),this}setGroupBy(t){return this.options.groupBy=t,this.applyOptions(),this.refreshGraph(),this}setGroupOptions(t){return t.borderColor!==void 0&&(this.options.groupBorderColor=t.borderColor),t.borderWidth!==void 0&&(this.options.groupBorderWidth=t.borderWidth),t.borderOpacity!==void 0&&(this.options.groupBorderOpacity=t.borderOpacity),t.labelColor!==void 0&&(this.options.groupLabelColor=t.labelColor),t.labelSize!==void 0&&(this.options.groupLabelSize=t.labelSize),t.labelThreshold!==void 0&&(this.options.groupLabelThreshold=t.labelThreshold),t.padding!==void 0&&(this.options.groupPadding=t.padding),this.applyOptions(),this.refreshGraph(),this}getGroups(){const t=new Set;return this.data.nodes.forEach(i=>{const o=this.getNodeGroupId(i);o&&t.add(o)}),Array.from(t)}getNodesInGroup(t){return this.data.nodes.filter(i=>this.getNodeGroupId(i)===t)}getOptions(){return{...this.options}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.graph._destructor()}}a.ForceGraph=C,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(h,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],l):(h=typeof globalThis<"u"?globalThis:h||self,l(h.ForceGraphLib={},h.ForceGraph,h.d3))})(this,(function(h,l,b){"use strict";function k(d){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(d){for(const i in d)if(i!=="default"){const o=Object.getOwnPropertyDescriptor(d,i);Object.defineProperty(t,i,o.get?o:{enumerable:!0,get:()=>d[i]})}}return t.default=d,Object.freeze(t)}const m=k(b);class C{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupBounds=new Map;isFirstRender=!0;constructor(t,i={nodes:[],links:[]},o={}){this.container=t,this.data=i,this.graph=new l(this.container),i.nodes.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e={labelThreshold:1.5,showGroups:!1,...o},s=e.showGroups?{groupBy:"topic",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,groupPadding:20}:{};this.options={...e,...s},this.initGraph()}initGraph(){this.applyOptions(),this.render(),this.graphData(this.data),this.graph.onEngineStop(()=>{this.isFirstRender?(this.options.onRenderComplete?this.options.onRenderComplete():this.graph.zoomToFit(400),this.isFirstRender=!1):this.options.onGraphUpdated&&this.options.onGraphUpdated()})}renderer(){return this.graph}focusPosition(t={}){if(Object.values(t).length){if(t.id){const{x:i,y:o}=this.nodesMap.get(t.id)??{x:0,y:0};i&&o&&(t={x:i,y:o})}t&&(this.graph.centerAt(t.x,t.y,1e3),this.graph.zoom(8,2e3))}}render(){this.graph.width(this.options.width??800).height(this.options.height??400).cooldownTime(this.getCooldownTime()).d3Force("charge",m.forceManyBody().strength(this.options.nodeGap??-50)),typeof this.options.nodeClickHandler=="function"&&this.graph.onNodeClick(t=>{this.options.nodeClickHandler(t)})}force(t,i){return this.graph.d3Force(t,i)}applyOptions(){this.options.keepDragPosition&&this.graph.onNodeDragEnd(t=>{t.fx=t.x,t.fy=t.y}),this.options.pointerInteraction||this.graph.enablePointerInteraction(!1),this.graph.nodeCanvasObject((t,i,o)=>{t===this.data.nodes[0]&&this.renderGroups(i,o);const e=this.getNodeSize(t)*2,s=typeof this.options.nodeBorderWidth=="function"?this.options.nodeBorderWidth(t):this.options.nodeBorderWidth;i.beginPath(),i.arc(t.x||0,t.y||0,e,0,2*Math.PI),i.fillStyle=this.getNodeColor(t),i.fill(),s&&s>0&&(i.beginPath(),i.arc(t.x||0,t.y||0,e,0,2*Math.PI),i.strokeStyle=this.getNodeBorderColor(t),i.lineWidth=s,i.stroke());const r=this.getNodeLabel(t);if(r&&this.shouldShowLabel(t,o)){const p=typeof this.options.nodeLabelColor=="function"?this.options.nodeLabelColor(t):this.options.nodeLabelColor??"#555",n=(this.options.labelFontSize||14)/o;i.font=`${n}px Arial`,i.fillStyle=p,i.textAlign="center",i.textBaseline="middle",i.fillText(r,t.x||0,(t.y||0)+e+n/2+2)}}),this.applyLinkOptions()}getNodeSize(t){return typeof this.options.nodeSize=="function"?this.options.nodeSize(t)||t?.marker?.radius:this.options.nodeSize||t?.marker?.radius||1}getNodeLabel(t){return typeof this.options.nodeLabel=="function"?this.options.nodeLabel(t):t?.label||t.id}shouldShowLabel(t,i){const o=this.getNodeSize(t)*2,s=(this.options.labelFontSize||14)/i,r=this.options.labelThreshold||1.5;return s<=o*r}updateData(t){const i=new Set(this.data.nodes.map(r=>r.id.toString())),o=t.nodes.filter(r=>!i.has(r.id.toString()));o.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e=new Set(this.data.links.map(r=>this.createLinkKey(r.source,r.target))),s=t.links.filter(r=>!e.has(this.createLinkKey(r.source,r.target)));this.data={nodes:[...this.data.nodes,...o],links:[...this.data.links,...s]},this.refreshGraph()}getNodeColor(t){if(typeof this.options.nodeColor=="function"){const i=this.options.nodeColor(t);if(i)return i}return t?.color??""}getNodeBorderColor(t){return typeof this.options.nodeBorderColor=="function"?this.options.nodeBorderColor(t):this.options.nodeBorderColor||"#333"}applyLinkOptions(){this.options.linkWidth!==void 0&&this.graph.linkWidth(t=>this.getLinkProperty(this.options.linkWidth,t)??1),this.options.linkCurvature!==void 0&&this.graph.linkCurvature(this.getLinkCurvature.bind(this)),this.options.linkDirectionalParticles&&this.graph.linkDirectionalParticles(t=>this.getLinkProperty(this.options.linkDirectionalParticles,t)??0),this.options.linkDirectionalParticleSpeed!==void 0&&this.graph.linkDirectionalParticleSpeed(t=>this.getLinkProperty(this.options.linkDirectionalParticleSpeed,t)??0),this.options.linkDirectionalParticleWidth!==void 0&&this.graph.linkDirectionalParticleWidth(t=>this.getLinkProperty(this.options.linkDirectionalParticleWidth,t)??0),this.options.linkDirectionalParticleColor!==void 0&&this.graph.linkDirectionalParticleColor(t=>this.getLinkProperty(this.options.linkDirectionalParticleColor,t)??"#aaa")}getLinkCurvature(t){return typeof this.options.linkCurvature=="function"?this.options.linkCurvature(t):typeof this.options.linkCurvature=="string"?t[this.options.linkCurvature]||0:typeof this.options.linkCurvature=="number"?this.options.linkCurvature:t.curvature||0}getLinkProperty(t,i){return typeof t=="function"?t(i):t}calculateGroupBounds(){if(!this.options.showGroups)return;this.groupBounds.clear();const t=this.options.groupPadding||20,i=new Map;this.data.nodes.forEach(o=>{const e=this.getNodeGroupId(o);e&&(i.has(e)||i.set(e,[]),i.get(e).push(o))}),i.forEach((o,e)=>{if(o.length===0)return;let s=1/0,r=1/0,p=-1/0,u=-1/0;o.forEach(n=>{if(n.x!==void 0&&n.y!==void 0){const a=this.getNodeSize(n);s=Math.min(s,n.x-a),r=Math.min(r,n.y-a),p=Math.max(p,n.x+a),u=Math.max(u,n.y+a)}}),s!==1/0&&this.groupBounds.set(e,{minX:s-t,minY:r-t,maxX:p+t,maxY:u+t,nodes:o})})}getNodeGroupId(t){if(this.options.groupBy){if(typeof this.options.groupBy=="function")return this.options.groupBy(t);if(typeof this.options.groupBy=="string")return t[this.options.groupBy]}}renderGroups(t,i){this.options.showGroups&&(this.calculateGroupBounds(),this.groupBounds.forEach((o,e)=>{const s=this.getGroupBorderColor(e),r=this.options.groupBorderWidth||2,p=this.options.groupBorderOpacity||.3;t.save(),t.globalAlpha=p,t.strokeStyle=s,t.lineWidth=r/i,t.setLineDash([10/i,5/i]),t.strokeRect(o.minX,o.minY,o.maxX-o.minX,o.maxY-o.minY),t.restore();const u=this.options.groupLabelThreshold||.8;if(i<=u){const n=this.getGroupLabelColor(e),a=(this.options.groupLabelSize||16)/i;t.save(),t.font=`bold ${a}px Arial`,t.fillStyle=n,t.textAlign="center",t.textBaseline="middle";const g=(o.minX+o.maxX)/2,f=o.minY-a/2,c=t.measureText(e).width,y=a;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(g-c/2-4,f-y/2-2,c+8,y+4),t.globalAlpha=1,t.fillStyle=n,t.fillText(e,g,f),t.restore()}}))}getGroupBorderColor(t){return typeof this.options.groupBorderColor=="function"?this.options.groupBorderColor(t):this.options.groupBorderColor||"#666"}getGroupLabelColor(t){return typeof this.options.groupLabelColor=="function"?this.options.groupLabelColor(t):this.options.groupLabelColor||"#333"}getNodeById(t){return this.nodesMap.get(t.toString())}hasNode(t){return this.nodesMap.has(t.toString())}getCooldownTime(){const i=this.data.nodes.length*(125/100);return Math.max(4e3,i)}getDataSize(){const{nodes:t,links:i}=this.graph.graphData();return{nodes:t.length,links:i.length}}getAllNodeIds(){return Array.from(this.nodesMap.keys())}updateNode(t,i){const o=this.nodesMap.get(t.toString());if(o){Object.assign(o,i);const e=this.data.nodes.findIndex(s=>s.id.toString()===t.toString());return e!==-1&&(this.data.nodes[e]=o),this.refreshGraph(),!0}return!1}removeNode(t){const i=t.toString();return this.nodesMap.has(i)?(this.nodesMap.delete(i),this.data.nodes=this.data.nodes.filter(o=>o.id.toString()!==i),this.data.links=this.data.links.filter(o=>{const e=typeof o.source=="object"?o.source.id:o.source,s=typeof o.target=="object"?o.target.id:o.target;return e?.toString()!==i&&s?.toString()!==i}),this.graph.cooldownTime(this.getCooldownTime()),this.refreshGraph(),!0):!1}async addData(t){t.nodes.forEach(i=>{this.nodesMap.has(i.id.toString())||this.nodesMap.set(i.id.toString(),i)}),t.links.forEach(i=>{const o=this.createLinkKey(i.source,i.target);this.linkMap.has(o)?console.log("link already exists",i):this.linkMap.set(o,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.graph.cooldownTime(this.getCooldownTime()),this.graph.graphData(this.data)}setLabelThreshold(t){this.options.labelThreshold=t,this.render()}setOptions(t){this.options={...this.options,...t},this.applyOptions()}refreshGraph(){this.graphData(this.data)}reinitialize(){this.initGraph()}reset(){this.graph.pauseAnimation();const t=this.graph.d3Force("simulation");t&&t.stop(),this.data={nodes:[],links:[]},this.nodesMap.clear(),this.graph=new l(this.container),this.initGraph()}getData(){return this.data}getNodesData(){return this.data.nodes}getLinksData(){return this.data.links}createLinkKey(t,i){const o=t===void 0?"undefined":typeof t=="object"?t.id:t,e=i===void 0?"undefined":typeof i=="object"?i.id:i;return`${o}-${e}`}graphData(t){return this.nodesMap.clear(),this.linkMap.clear(),t.nodes.forEach(i=>{this.nodesMap.has(i.id.toString())||this.nodesMap.set(i.id.toString(),i)}),t.links.forEach(i=>{const o=this.createLinkKey(i.source,i.target);this.linkMap.has(o)||this.linkMap.set(o,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.graph.cooldownTime(this.getCooldownTime()),this.graph.graphData(this.data),this}showGroups(t){return this.options.showGroups=t,t&&Object.entries({groupBy:"topic",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,groupPadding:20}).forEach(([o,e])=>{this.options[o]===void 0&&(this.options[o]=e)}),this.applyOptions(),this.refreshGraph(),this}setGroupBy(t){return this.options.groupBy=t,this.applyOptions(),this.refreshGraph(),this}setGroupOptions(t){return t.borderColor!==void 0&&(this.options.groupBorderColor=t.borderColor),t.borderWidth!==void 0&&(this.options.groupBorderWidth=t.borderWidth),t.borderOpacity!==void 0&&(this.options.groupBorderOpacity=t.borderOpacity),t.labelColor!==void 0&&(this.options.groupLabelColor=t.labelColor),t.labelSize!==void 0&&(this.options.groupLabelSize=t.labelSize),t.labelThreshold!==void 0&&(this.options.groupLabelThreshold=t.labelThreshold),t.padding!==void 0&&(this.options.groupPadding=t.padding),this.applyOptions(),this.refreshGraph(),this}getGroups(){const t=new Set;return this.data.nodes.forEach(i=>{const o=this.getNodeGroupId(i);o&&t.add(o)}),Array.from(t)}getNodesInGroup(t){return this.data.nodes.filter(i=>this.getNodeGroupId(i)===t)}getOptions(){return{...this.options}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.graph._destructor()}}h.ForceGraph=C,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"})}));
package/dist/index.d.ts CHANGED
@@ -9,7 +9,29 @@ declare interface ForceFn<N extends NodeObject = NodeObject> {
9
9
  [key: string]: any;
10
10
  }
11
11
 
12
- export declare class ForceGraph {
12
+ /**
13
+ * ForceGraph with flexible generic types.
14
+ *
15
+ * - `TNode`: your node shape; must include an `id` (`string | number`).
16
+ * - `TLink`: your link shape; by default `LinkObject<TNode>`.
17
+ *
18
+ * Usage examples:
19
+ *
20
+ * // 1) Use defaults (compatible with NodeData/LinkObject)
21
+ * const graph = new ForceGraph(container, defaultData)
22
+ *
23
+ * // 2) Provide custom shapes
24
+ * type MyNode = NodeObject & { id: string; color?: string; size?: number }
25
+ * type MyLink = LinkObject<MyNode> & { weight?: number }
26
+ * const graph = new ForceGraph<MyNode, MyLink>(container, myData, {
27
+ * nodeSize: (n) => n.size ?? 2,
28
+ * nodeColor: (n) => n.color ?? '#999',
29
+ * linkWidth: (l) => (l.weight ?? 1) * 2,
30
+ * })
31
+ */
32
+ export declare class ForceGraph<TNode extends NodeData & {
33
+ id: string | number;
34
+ } = NodeData, TLink extends LinkData<TNode> = LinkData<TNode>> {
13
35
  private container;
14
36
  private graph;
15
37
  private data;
@@ -18,16 +40,17 @@ export declare class ForceGraph {
18
40
  private options;
19
41
  private worker;
20
42
  private groupBounds;
21
- constructor(container: HTMLElement, initialData?: GraphData, options?: GraphOptions);
43
+ private isFirstRender;
44
+ constructor(container: HTMLElement, initialData?: GraphData<TNode, TLink>, options?: GraphOptions<TNode, TLink>);
22
45
  private initGraph;
23
- renderer(): default_2<NodeData, LinkObject<NodeData>>;
46
+ renderer(): default_2<TNode, TLink>;
24
47
  focusPosition(nodeData?: {
25
48
  id?: string;
26
49
  x?: number;
27
50
  y?: number;
28
51
  }): void;
29
52
  render(): void;
30
- force(key: ForceType, func: ForceFn<NodeData>): default_2<NodeData, LinkObject<NodeData>>;
53
+ force(key: ForceType, func: ForceFn<TNode>): default_2<TNode, TLink>;
31
54
  private applyOptions;
32
55
  private getNodeSize;
33
56
  private getNodeLabel;
@@ -36,7 +59,7 @@ export declare class ForceGraph {
36
59
  * The threshold determines when labels become too big relative to nodes
37
60
  */
38
61
  private shouldShowLabel;
39
- updateData(data: GraphData): void;
62
+ updateData(data: GraphData<TNode, TLink>): void;
40
63
  private getNodeColor;
41
64
  private getNodeBorderColor;
42
65
  private applyLinkOptions;
@@ -62,7 +85,7 @@ export declare class ForceGraph {
62
85
  * Get group label color
63
86
  */
64
87
  private getGroupLabelColor;
65
- getNodeById(id: string | number): NodeData | undefined;
88
+ getNodeById(id: string | number): TNode | undefined;
66
89
  hasNode(id: string | number): boolean;
67
90
  /**
68
91
  * Calculate dynamic cooldown time based on node count
@@ -79,11 +102,11 @@ export declare class ForceGraph {
79
102
  links: number;
80
103
  };
81
104
  getAllNodeIds(): string[];
82
- updateNode(id: string | number, updates: Partial<NodeData>): boolean;
105
+ updateNode(id: string | number, updates: Partial<TNode>): boolean;
83
106
  removeNode(id: string | number): boolean;
84
- addData(newData: GraphData): Promise<void>;
107
+ addData(newData: GraphData<TNode, TLink>): Promise<void>;
85
108
  setLabelThreshold(threshold: number): void;
86
- setOptions(options: Partial<GraphOptions>): void;
109
+ setOptions(options: Partial<GraphOptions<TNode, TLink>>): void;
87
110
  /**
88
111
  * Lightweight refresh - only updates graph data
89
112
  */
@@ -93,23 +116,23 @@ export declare class ForceGraph {
93
116
  */
94
117
  reinitialize(): void;
95
118
  reset(): void;
96
- getData(): GraphData;
97
- getNodesData(): GraphData['nodes'];
98
- getLinksData(): GraphData['links'];
119
+ getData(): GraphData<TNode, TLink>;
120
+ getNodesData(): GraphData<TNode, TLink>['nodes'];
121
+ getLinksData(): GraphData<TNode, TLink>['links'];
99
122
  private createLinkKey;
100
123
  /**
101
124
  * Set graph data (chainable method)
102
125
  * @param data - Graph data to set
103
126
  */
104
- graphData(data: GraphData): ForceGraph;
127
+ graphData(data: GraphData<TNode, TLink>): ForceGraph<TNode, TLink>;
105
128
  /**
106
129
  * Enable or disable group visualization
107
130
  */
108
- showGroups(show: boolean): ForceGraph;
131
+ showGroups(show: boolean): ForceGraph<TNode, TLink>;
109
132
  /**
110
133
  * Set the property to group nodes by
111
134
  */
112
- setGroupBy(groupBy: string | ((node: NodeData) => string | undefined)): ForceGraph;
135
+ setGroupBy(groupBy: string | ((node: TNode) => string | undefined)): ForceGraph<TNode, TLink>;
113
136
  /**
114
137
  * Set group visualization options
115
138
  */
@@ -121,7 +144,7 @@ export declare class ForceGraph {
121
144
  labelSize?: number;
122
145
  labelThreshold?: number;
123
146
  padding?: number;
124
- }): ForceGraph;
147
+ }): ForceGraph<TNode, TLink>;
125
148
  /**
126
149
  * Get all available groups
127
150
  */
@@ -129,46 +152,47 @@ export declare class ForceGraph {
129
152
  /**
130
153
  * Get nodes in a specific group
131
154
  */
132
- getNodesInGroup(groupId: string): NodeData[];
155
+ getNodesInGroup(groupId: string): TNode[];
133
156
  /**
134
157
  * Get current options
135
158
  */
136
- getOptions(): GraphOptions;
159
+ getOptions(): GraphOptions<TNode, TLink>;
137
160
  destroy(): void;
138
161
  }
139
162
 
140
163
  declare type ForceType = 'link' | 'charge' | 'center' | 'cluster' | string;
141
164
 
142
- export declare interface GraphData extends GraphData_2<NodeData, LinkData> {
165
+ export declare interface GraphData<N extends NodeData = NodeData, L extends LinkData<N> = LinkData<N>> extends GraphData_2<N, L> {
143
166
  }
144
167
 
145
- export declare interface GraphOptions {
168
+ export declare interface GraphOptions<N extends NodeData = NodeData, L extends LinkData<N> = LinkData<N>> {
146
169
  height?: number;
147
170
  width?: number;
148
171
  labelThreshold?: number;
149
- nodeSize?: number | ((node: NodeData) => number);
150
- linkWidth?: number | ((link: LinkData) => number);
151
- nodeLabel?: string | ((node: NodeData) => string);
152
- nodeLabelColor?: string | ((node: NodeData) => string);
172
+ nodeSize?: number | ((node: N) => number);
173
+ linkWidth?: number | ((link: L) => number);
174
+ nodeLabel?: string | ((node: N) => string);
175
+ nodeLabelColor?: string | ((node: N) => string);
153
176
  labelFontSize?: number;
154
- nodeColor?: string | ((node: NodeData) => string);
155
- nodeBorderColor?: string | ((node: NodeData) => string);
156
- nodeBorderWidth?: number | ((node: NodeData) => number);
177
+ nodeColor?: string | ((node: N) => string);
178
+ nodeBorderColor?: string | ((node: N) => string);
179
+ nodeBorderWidth?: number | ((node: N) => number);
157
180
  nodeGap?: number;
158
- linkLabel?: string | ((link: LinkData) => string);
159
- nodeIcon?: string | ((node: NodeData) => string);
160
- cluster?: (node: NodeData) => boolean | undefined | null;
181
+ linkLabel?: string | ((link: L) => string);
182
+ nodeIcon?: string | ((node: N) => string);
161
183
  loading?: boolean;
162
184
  pointerInteraction?: boolean;
163
185
  keepDragPosition?: boolean;
164
- nodeClickHandler?: (node: NodeData) => void;
165
- linkCurvature?: number | string | ((link: LinkData) => number);
166
- linkDirectionalParticles?: number | ((link: LinkData) => number);
167
- linkDirectionalParticleSpeed?: number | ((link: LinkData) => number);
168
- linkDirectionalParticleWidth?: number | ((link: LinkData) => number);
169
- linkDirectionalParticleColor?: string | ((link: LinkData) => string);
186
+ nodeClickHandler?: (node: N) => void;
187
+ linkCurvature?: number | string | ((link: L) => number);
188
+ linkDirectionalParticles?: number | ((link: L) => number);
189
+ linkDirectionalParticleSpeed?: number | ((link: L) => number);
190
+ linkDirectionalParticleWidth?: number | ((link: L) => number);
191
+ linkDirectionalParticleColor?: string | ((link: L) => string);
192
+ onRenderComplete?: () => void;
193
+ onGraphUpdated?: () => void;
170
194
  showGroups?: boolean;
171
- groupBy?: string | ((node: NodeData) => string | undefined);
195
+ groupBy?: string | ((node: N) => string | undefined);
172
196
  groupBorderColor?: string | ((groupId: string) => string);
173
197
  groupBorderWidth?: number;
174
198
  groupBorderOpacity?: number;
@@ -178,9 +202,9 @@ export declare interface GraphOptions {
178
202
  groupPadding?: number;
179
203
  }
180
204
 
181
- export declare interface LinkData extends LinkObject<NodeData> {
182
- source: string | number | NodeData;
183
- target: string | number | NodeData;
205
+ export declare interface LinkData<N extends NodeData = NodeData> extends LinkObject<N> {
206
+ source: string | number | N;
207
+ target: string | number | N;
184
208
  weight?: number;
185
209
  color?: string;
186
210
  curvature?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@logixode/force-graph-lib",
3
3
  "private": false,
4
- "version": "0.1.8",
4
+ "version": "0.1.10",
5
5
  "description": "Force-directed graph visualization library.",
6
6
  "author": "Rohmad Kurniadi",
7
7
  "license": "MIT",