@logixode/force-graph-lib 0.6.0 → 0.7.0

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.
@@ -1,5 +1,5 @@
1
- import y from "force-graph";
2
- import * as g from "d3";
1
+ import C from "force-graph";
2
+ import * as c from "d3";
3
3
  class S {
4
4
  container;
5
5
  graph;
@@ -9,10 +9,12 @@ class S {
9
9
  options;
10
10
  worker = null;
11
11
  groupHulls = /* @__PURE__ */ new Map();
12
+ nodeGroupsCache = null;
12
13
  isFirstRender = !0;
13
- autoColorScale = g.scaleOrdinal(g.schemeCategory10);
14
+ autoColorScale = c.scaleOrdinal(c.schemeCategory10);
15
+ charCache = /* @__PURE__ */ new Map();
14
16
  constructor(t, o = { nodes: [], links: [] }, i = {}) {
15
- this.container = t, this.data = o, this.graph = new y(this.container), o.nodes.forEach((r) => {
17
+ this.container = t, this.data = o, this.graph = new C(this.container), o.nodes.forEach((r) => {
16
18
  this.nodesMap.set(r.id.toString(), r);
17
19
  });
18
20
  const e = {
@@ -43,6 +45,8 @@ class S {
43
45
  return;
44
46
  }
45
47
  this.isFirstRender ? (this.options.onRenderComplete ? this.options.onRenderComplete() : this.graph.zoomToFit(400), this.isFirstRender = !1) : this.options.onGraphUpdated && this.options.onGraphUpdated();
48
+ }), this.graph.onEngineTick(() => {
49
+ this.calculateGroupHulls();
46
50
  });
47
51
  }
48
52
  renderer() {
@@ -68,12 +72,21 @@ class S {
68
72
  t.fx = t.x, t.fy = t.y;
69
73
  }), this.options.pointerInteraction || this.graph.enablePointerInteraction(!1), this.graph.nodeCanvasObject((t, o, i) => {
70
74
  t === this.data.nodes[0] && this.renderGroups(o, i);
71
- const e = this.getNodeSize(t) * 2, s = typeof this.options.nodeBorderWidth == "function" ? this.options.nodeBorderWidth(t) : this.options.nodeBorderWidth, r = this.resolveNodeShape(t), l = t.x || 0, n = t.y || 0;
72
- this.drawNodeShape(o, l, n, e, r), o.fillStyle = this.getNodeColor(t), o.fill(), s && s > 0 && (this.drawNodeShape(o, l, n, e, r), o.strokeStyle = this.getNodeBorderColor(t), o.lineWidth = s, o.stroke());
73
- const a = this.getNodeLabel(t);
74
- if (a && this.shouldShowLabel(t, i)) {
75
- const h = typeof this.options.nodeLabelColor == "function" ? this.options.nodeLabelColor(t) : this.options.nodeLabelColor ?? "#555", d = (this.options.labelFontSize || 14) / i;
76
- o.font = `${d}px Arial`, o.fillStyle = h, o.textAlign = "center", o.textBaseline = "middle", o.fillText(a, t.x || 0, (t.y || 0) + e + d / 2 + 2);
75
+ const e = this.getNodeSize(t) * 2, s = typeof this.options.nodeBorderWidth == "function" ? this.options.nodeBorderWidth(t) : this.options.nodeBorderWidth, r = this.resolveNodeShape(t), p = t.x || 0, n = t.y || 0;
76
+ if (this.drawNodeShape(o, p, n, e, r), o.fillStyle = this.getNodeColor(t), o.fill(), s && s > 0 && (this.drawNodeShape(o, p, n, e, r), o.strokeStyle = this.getNodeBorderColor(t), o.lineWidth = s, o.stroke()), this.options.nodeChar) {
77
+ const a = this.options.nodeChar(t);
78
+ if (a?.char) {
79
+ const d = `${a.char}|${a.color ?? "#000"}`, l = this.charCache.get(d);
80
+ if (l) {
81
+ const u = a.fontSize ?? e * (this.options.nodeCharSizeRatio ?? 0.8);
82
+ o.drawImage(l, p - u / 2, n - u / 2, u, u);
83
+ }
84
+ }
85
+ }
86
+ const h = this.getNodeLabel(t);
87
+ if (h && this.shouldShowLabel(t, i)) {
88
+ const a = typeof this.options.nodeLabelColor == "function" ? this.options.nodeLabelColor(t) : this.options.nodeLabelColor ?? "#555", l = (this.options.labelFontSize || 14) / i;
89
+ o.font = `${l}px Arial`, o.fillStyle = a, o.textAlign = "center", o.textBaseline = "middle", o.fillText(h, t.x || 0, (t.y || 0) + e + l / 2 + 2);
77
90
  }
78
91
  }), this.applyLinkOptions();
79
92
  }
@@ -83,6 +96,25 @@ class S {
83
96
  getNodeLabel(t) {
84
97
  return typeof this.options.nodeLabel == "function" ? this.options.nodeLabel(t) : t?.label || t.id;
85
98
  }
99
+ /** Render a single char/emoji to a 64×64 OffscreenCanvas and return an ImageBitmap */
100
+ buildCharBitmap(t, o) {
101
+ const e = new OffscreenCanvas(64, 64), s = e.getContext("2d");
102
+ return s.font = `${64 * 0.8}px sans-serif`, s.textAlign = "center", s.textBaseline = "middle", s.fillStyle = o, s.fillText(t, 64 / 2, 64 / 2), e.transferToImageBitmap();
103
+ }
104
+ /** Idempotent — only adds cache entries for chars not yet cached */
105
+ ensureCharCache() {
106
+ if (this.options.nodeChar)
107
+ for (const t of this.data.nodes) {
108
+ const o = this.options.nodeChar(t);
109
+ if (!o?.char) continue;
110
+ const i = o.color ?? "#000", e = `${o.char}|${i}`;
111
+ this.charCache.has(e) || this.charCache.set(e, this.buildCharBitmap(o.char, i));
112
+ }
113
+ }
114
+ /** Release GPU memory and clear the cache */
115
+ clearCharCache() {
116
+ this.charCache.forEach((t) => t.close()), this.charCache.clear();
117
+ }
86
118
  /**
87
119
  * Check if label should be shown based on threshold logic
88
120
  * The threshold determines when labels become too big relative to nodes
@@ -104,7 +136,7 @@ class S {
104
136
  this.data = {
105
137
  nodes: [...this.data.nodes, ...i],
106
138
  links: [...this.data.links, ...s]
107
- }, this.refreshGraph();
139
+ }, this.ensureCharCache(), this.refreshGraph();
108
140
  }
109
141
  getNodeColor(t) {
110
142
  if (typeof this.options.nodeColor == "function") {
@@ -135,18 +167,18 @@ class S {
135
167
  break;
136
168
  }
137
169
  case "star": {
138
- const r = e, l = e * 0.4, n = 5;
139
- for (let a = 0; a < n * 2; a++) {
140
- const h = a % 2 === 0 ? r : l, p = Math.PI / n * a - Math.PI / 2, d = o + h * Math.cos(p), u = i + h * Math.sin(p);
141
- a === 0 ? t.moveTo(d, u) : t.lineTo(d, u);
170
+ const r = e, p = e * 0.4, n = 5;
171
+ for (let h = 0; h < n * 2; h++) {
172
+ const a = h % 2 === 0 ? r : p, d = Math.PI / n * h - Math.PI / 2, l = o + a * Math.cos(d), u = i + a * Math.sin(d);
173
+ h === 0 ? t.moveTo(l, u) : t.lineTo(l, u);
142
174
  }
143
175
  t.closePath();
144
176
  break;
145
177
  }
146
178
  case "hexagon": {
147
179
  for (let r = 0; r < 6; r++) {
148
- const l = Math.PI / 3 * r - Math.PI / 6, n = o + e * Math.cos(l), a = i + e * Math.sin(l);
149
- r === 0 ? t.moveTo(n, a) : t.lineTo(n, a);
180
+ const p = Math.PI / 3 * r - Math.PI / 6, n = o + e * Math.cos(p), h = i + e * Math.sin(p);
181
+ r === 0 ? t.moveTo(n, h) : t.lineTo(n, h);
150
182
  }
151
183
  t.closePath();
152
184
  break;
@@ -182,33 +214,30 @@ class S {
182
214
  calculateGroupHulls() {
183
215
  if (!this.options.showGroups) return;
184
216
  this.groupHulls.clear();
185
- const t = this.options.groupPadding || 20, o = /* @__PURE__ */ new Map();
186
- this.data.nodes.forEach((i) => {
187
- const e = this.getNodeGroupId(i);
188
- e && (o.has(e) || o.set(e, []), o.get(e).push(i));
189
- }), o.forEach((i, e) => {
217
+ const t = this.options.groupPadding || 20;
218
+ this.ensureNodeGroupsCache(), this.nodeGroupsCache.forEach((i, e) => {
190
219
  if (i.length === 0) return;
191
220
  const s = [];
192
- let r = 1 / 0, l = 0;
221
+ let r = 1 / 0, p = 0;
193
222
  if (i.forEach((n) => {
194
223
  if (n.x !== void 0 && n.y !== void 0) {
195
- const h = this.getNodeSize(n) + t;
196
- for (let p = 0; p < 8; p++) {
197
- const d = p * Math.PI / 4;
224
+ const a = this.getNodeSize(n) + t;
225
+ for (let d = 0; d < 8; d++) {
226
+ const l = d * Math.PI / 4;
198
227
  s.push([
199
- n.x + h * Math.cos(d),
200
- n.y + h * Math.sin(d)
228
+ n.x + a * Math.cos(l),
229
+ n.y + a * Math.sin(l)
201
230
  ]);
202
231
  }
203
- n.y - h < r && (r = n.y - h, l = n.x);
232
+ n.y - a < r && (r = n.y - a, p = n.x);
204
233
  }
205
234
  }), s.length >= 3) {
206
- const n = g.polygonHull(s);
235
+ const n = c.polygonHull(s);
207
236
  if (n) {
208
- const a = n.filter((h) => Math.abs(h[1] - r) < t);
209
- a.length > 0 && (l = a.reduce((h, p) => h + p[0], 0) / a.length), this.groupHulls.set(e, {
237
+ const h = n.filter((a) => Math.abs(a[1] - r) < t);
238
+ h.length > 0 && (p = h.reduce((a, d) => a + d[0], 0) / h.length), this.groupHulls.set(e, {
210
239
  points: n,
211
- labelX: l,
240
+ labelX: p,
212
241
  labelY: r,
213
242
  nodes: i
214
243
  });
@@ -227,25 +256,33 @@ class S {
227
256
  return t[this.options.groupBy];
228
257
  }
229
258
  }
259
+ /**
260
+ * Populate group cache to prevent calculating on every frame
261
+ */
262
+ ensureNodeGroupsCache() {
263
+ this.nodeGroupsCache || (this.nodeGroupsCache = /* @__PURE__ */ new Map(), this.data.nodes.forEach((t) => {
264
+ const o = this.getNodeGroupId(t);
265
+ o && (this.nodeGroupsCache.has(o) || this.nodeGroupsCache.set(o, []), this.nodeGroupsCache.get(o).push(t));
266
+ }));
267
+ }
230
268
  /**
231
269
  * Render group hulls and labels
232
270
  */
233
271
  renderGroups(t, o) {
234
272
  if (!this.options.showGroups) return;
235
- this.calculateGroupHulls();
236
- const i = g.line().curve(g.curveCatmullRomClosed).context(t);
273
+ const i = c.line().curve(c.curveCatmullRomClosed).context(t);
237
274
  this.groupHulls.forEach((e, s) => {
238
- const r = this.getGroupFillColor(s), l = this.options.groupFillOpacity ?? 1, n = this.getGroupBorderColor(s), a = this.options.groupBorderWidth ?? 0, h = this.options.groupBorderOpacity ?? 0.3;
239
- t.save(), t.beginPath(), i(e.points), r && (t.globalAlpha = l, t.fillStyle = r, t.fill()), a > 0 && n && (t.globalAlpha = h, t.strokeStyle = n, t.lineWidth = a / o, t.setLineDash([]), t.stroke()), t.restore();
240
- const p = this.getGroupLabelColor(s), d = (this.options.groupLabelSize || 16) / o;
241
- t.save(), t.font = `bold ${d}px Arial`, t.fillStyle = p, t.textAlign = "center", t.textBaseline = "middle";
242
- const u = e.labelY - d / 2, f = t.measureText(s).width, c = d;
275
+ const r = this.getGroupFillColor(s), p = this.options.groupFillOpacity ?? 1, n = this.getGroupBorderColor(s), h = this.options.groupBorderWidth ?? 0, a = this.options.groupBorderOpacity ?? 0.3;
276
+ t.save(), t.beginPath(), i(e.points), r && (t.globalAlpha = p, t.fillStyle = r, t.fill()), h > 0 && n && (t.globalAlpha = a, t.strokeStyle = n, t.lineWidth = h / o, t.setLineDash([]), t.stroke()), t.restore();
277
+ const d = this.getGroupLabelColor(s), l = (this.options.groupLabelSize || 16) / o;
278
+ t.save(), t.font = `bold ${l}px Arial`, t.fillStyle = d, t.textAlign = "center", t.textBaseline = "middle";
279
+ const u = e.labelY - l / 2, f = t.measureText(s).width, g = l;
243
280
  t.globalAlpha = 0.8, t.fillStyle = "rgba(255, 255, 255, 0.9)", t.fillRect(
244
281
  e.labelX - f / 2 - 4,
245
- u - c / 2 - 2,
282
+ u - g / 2 - 2,
246
283
  f + 8,
247
- c + 4
248
- ), t.globalAlpha = 1, t.fillStyle = p, t.fillText(s, e.labelX, u), t.restore();
284
+ g + 4
285
+ ), t.globalAlpha = 1, t.fillStyle = d, t.fillText(s, e.labelX, u), t.restore();
249
286
  });
250
287
  }
251
288
  /**
@@ -324,13 +361,13 @@ class S {
324
361
  }), this.data = {
325
362
  nodes: Array.from(this.nodesMap.values()),
326
363
  links: Array.from(this.linkMap.values())
327
- }, this.graph.cooldownTime(this.getCooldownTime()), this.graph.graphData(this.data);
364
+ }, this.ensureCharCache(), this.graph.cooldownTime(this.getCooldownTime()), this.graph.graphData(this.data);
328
365
  }
329
366
  setLabelThreshold(t) {
330
367
  this.options.labelThreshold = t, this.render();
331
368
  }
332
369
  setOptions(t) {
333
- this.options = { ...this.options, ...t }, this.applyOptions();
370
+ this.options = { ...this.options, ...t }, t.nodeChar !== void 0 && (this.clearCharCache(), this.ensureCharCache()), this.applyOptions();
334
371
  }
335
372
  /**
336
373
  * Lightweight refresh - only updates graph data
@@ -345,7 +382,7 @@ class S {
345
382
  this.initGraph();
346
383
  }
347
384
  reset() {
348
- this.graph.pauseAnimation(), this.data = { nodes: [], links: [] }, this.nodesMap.clear(), this.graph = new y(this.container), this.initGraph();
385
+ this.graph.pauseAnimation(), this.data = { nodes: [], links: [] }, this.nodesMap.clear(), this.graph = new C(this.container), this.initGraph();
349
386
  }
350
387
  getData() {
351
388
  return this.data;
@@ -365,7 +402,7 @@ class S {
365
402
  * @param data - Graph data to set
366
403
  */
367
404
  graphData(t) {
368
- return this.nodesMap.clear(), this.linkMap.clear(), t.nodes.forEach((o) => {
405
+ return this.nodesMap.clear(), this.linkMap.clear(), this.nodeGroupsCache = null, this.clearCharCache(), t.nodes.forEach((o) => {
369
406
  this.nodesMap.has(o.id.toString()) || this.nodesMap.set(o.id.toString(), o);
370
407
  }), t.links.forEach((o) => {
371
408
  const i = this.createLinkKey(o.source, o.target);
@@ -373,7 +410,7 @@ class S {
373
410
  }), this.data = {
374
411
  nodes: Array.from(this.nodesMap.values()),
375
412
  links: Array.from(this.linkMap.values())
376
- }, this.graph.cooldownTime(this.getCooldownTime()), this.graph.graphData(this.data), this;
413
+ }, this.ensureCharCache(), this.graph.cooldownTime(this.getCooldownTime()), this.graph.graphData(this.data), this;
377
414
  }
378
415
  /**
379
416
  * Enable or disable group visualization
@@ -428,7 +465,7 @@ class S {
428
465
  return { ...this.options };
429
466
  }
430
467
  destroy() {
431
- this.worker && (this.worker.terminate(), this.worker = null), this.graph._destructor();
468
+ this.worker && (this.worker.terminate(), this.worker = null), this.clearCharCache(), this.graph._destructor();
432
469
  }
433
470
  }
434
471
  export {
@@ -1 +1 @@
1
- (function(u,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],f):(u=typeof globalThis<"u"?globalThis:u||self,f(u.ForceGraphLib={},u.ForceGraph,u.d3))})(this,(function(u,f,C){"use strict";function S(g){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(g){for(const o in g)if(o!=="default"){const i=Object.getOwnPropertyDescriptor(g,o);Object.defineProperty(t,o,i.get?i:{enumerable:!0,get:()=>g[o]})}}return t.default=g,Object.freeze(t)}const c=S(C);class v{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupHulls=new Map;isFirstRender=!0;autoColorScale=c.scaleOrdinal(c.schemeCategory10);constructor(t,o={nodes:[],links:[]},i={}){this.container=t,this.data=o,this.graph=new f(this.container),o.nodes.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e={labelThreshold:1.5,showGroups:!1,...i},s=e.showGroups?{groupBy:"topic",groupFillColor:"rgba(0, 0, 0, 0.05)",groupFillOpacity:1,groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupPadding:20}:{};this.options={...s,...e},this.initGraph()}initGraph(){this.applyOptions(),this.render(),this.graphData(this.data),this.graph.onEngineStop(()=>{if(!this.data.nodes.length){this.isFirstRender=!0;return}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:o,y:i}=this.nodesMap.get(t.id)??{x:0,y:0};o&&i&&(t={x:o,y:i})}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),this.options.cooldownTicks!==void 0&&this.graph.cooldownTicks(this.options.cooldownTicks)}force(t,o){return this.graph.d3Force(t,o)}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,o,i)=>{t===this.data.nodes[0]&&this.renderGroups(o,i);const e=this.getNodeSize(t)*2,s=typeof this.options.nodeBorderWidth=="function"?this.options.nodeBorderWidth(t):this.options.nodeBorderWidth,r=this.resolveNodeShape(t),l=t.x||0,n=t.y||0;this.drawNodeShape(o,l,n,e,r),o.fillStyle=this.getNodeColor(t),o.fill(),s&&s>0&&(this.drawNodeShape(o,l,n,e,r),o.strokeStyle=this.getNodeBorderColor(t),o.lineWidth=s,o.stroke());const a=this.getNodeLabel(t);if(a&&this.shouldShowLabel(t,i)){const h=typeof this.options.nodeLabelColor=="function"?this.options.nodeLabelColor(t):this.options.nodeLabelColor??"#555",d=(this.options.labelFontSize||14)/i;o.font=`${d}px Arial`,o.fillStyle=h,o.textAlign="center",o.textBaseline="middle",o.fillText(a,t.x||0,(t.y||0)+e+d/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,o){const i=this.getNodeSize(t)*2,s=(this.options.labelFontSize||14)/o,r=this.options.labelThreshold||1.5;return s<=i*r}updateData(t){const o=new Set(this.data.nodes.map(r=>r.id.toString())),i=t.nodes.filter(r=>!o.has(r.id.toString()));i.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,...i],links:[...this.data.links,...s]},this.refreshGraph()}getNodeColor(t){if(typeof this.options.nodeColor=="function"){const o=this.options.nodeColor(t);if(o)return o}return t?.color??""}getNodeBorderColor(t){return typeof this.options.nodeBorderColor=="function"?this.options.nodeBorderColor(t):this.options.nodeBorderColor||"#333"}resolveNodeShape(t){return t.shape?t.shape:typeof this.options.nodeShape=="function"?this.options.nodeShape(t):this.options.nodeShape??"circle"}drawNodeShape(t,o,i,e,s){switch(t.beginPath(),s){case"square":{t.rect(o-e,i-e,e*2,e*2);break}case"triangle":{const r=e*Math.sqrt(3);t.moveTo(o,i-e),t.lineTo(o+r/2,i+e/2),t.lineTo(o-r/2,i+e/2),t.closePath();break}case"diamond":{t.moveTo(o,i-e),t.lineTo(o+e,i),t.lineTo(o,i+e),t.lineTo(o-e,i),t.closePath();break}case"star":{const r=e,l=e*.4,n=5;for(let a=0;a<n*2;a++){const h=a%2===0?r:l,p=Math.PI/n*a-Math.PI/2,d=o+h*Math.cos(p),y=i+h*Math.sin(p);a===0?t.moveTo(d,y):t.lineTo(d,y)}t.closePath();break}case"hexagon":{for(let r=0;r<6;r++){const l=Math.PI/3*r-Math.PI/6,n=o+e*Math.cos(l),a=i+e*Math.sin(l);r===0?t.moveTo(n,a):t.lineTo(n,a)}t.closePath();break}case"circle":default:t.arc(o,i,e,0,2*Math.PI);break}}applyLinkOptions(){this.graph.linkColor(t=>typeof this.options.linkColor=="function"?this.options.linkColor(t):this.options.linkColor?this.options.linkColor:t.color??""),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,o){return typeof t=="function"?t(o):t}calculateGroupHulls(){if(!this.options.showGroups)return;this.groupHulls.clear();const t=this.options.groupPadding||20,o=new Map;this.data.nodes.forEach(i=>{const e=this.getNodeGroupId(i);e&&(o.has(e)||o.set(e,[]),o.get(e).push(i))}),o.forEach((i,e)=>{if(i.length===0)return;const s=[];let r=1/0,l=0;if(i.forEach(n=>{if(n.x!==void 0&&n.y!==void 0){const h=this.getNodeSize(n)+t;for(let p=0;p<8;p++){const d=p*Math.PI/4;s.push([n.x+h*Math.cos(d),n.y+h*Math.sin(d)])}n.y-h<r&&(r=n.y-h,l=n.x)}}),s.length>=3){const n=c.polygonHull(s);if(n){const a=n.filter(h=>Math.abs(h[1]-r)<t);a.length>0&&(l=a.reduce((h,p)=>h+p[0],0)/a.length),this.groupHulls.set(e,{points:n,labelX:l,labelY:r,nodes:i})}}})}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,o){if(!this.options.showGroups)return;this.calculateGroupHulls();const i=c.line().curve(c.curveCatmullRomClosed).context(t);this.groupHulls.forEach((e,s)=>{const r=this.getGroupFillColor(s),l=this.options.groupFillOpacity??1,n=this.getGroupBorderColor(s),a=this.options.groupBorderWidth??0,h=this.options.groupBorderOpacity??.3;t.save(),t.beginPath(),i(e.points),r&&(t.globalAlpha=l,t.fillStyle=r,t.fill()),a>0&&n&&(t.globalAlpha=h,t.strokeStyle=n,t.lineWidth=a/o,t.setLineDash([]),t.stroke()),t.restore();const p=this.getGroupLabelColor(s),d=(this.options.groupLabelSize||16)/o;t.save(),t.font=`bold ${d}px Arial`,t.fillStyle=p,t.textAlign="center",t.textBaseline="middle";const y=e.labelY-d/2,b=t.measureText(s).width,k=d;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(e.labelX-b/2-4,y-k/2-2,b+8,k+4),t.globalAlpha=1,t.fillStyle=p,t.fillText(s,e.labelX,y),t.restore()})}getGroupFillColor(t){let o;return typeof this.options.groupFillColor=="function"?o=this.options.groupFillColor(t):o=this.options.groupFillColor,o==="auto"?this.autoColorScale(t):o}getGroupBorderColor(t){let o;return typeof this.options.groupBorderColor=="function"?o=this.options.groupBorderColor(t):o=this.options.groupBorderColor,o==="auto"?this.autoColorScale(t):o||"#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(){if(this.options.cooldownTime!==void 0)return this.options.cooldownTime;const o=this.data.nodes.length*(125/100);return Math.max(4e3,o)}getDataSize(){const{nodes:t,links:o}=this.graph.graphData();return{nodes:t.length,links:o.length}}getAllNodeIds(){return Array.from(this.nodesMap.keys())}updateNode(t,o){const i=this.nodesMap.get(t.toString());if(i){Object.assign(i,o);const e=this.data.nodes.findIndex(s=>s.id.toString()===t.toString());return e!==-1&&(this.data.nodes[e]=i),this.refreshGraph(),!0}return!1}removeNode(t){const o=t.toString();return this.nodesMap.has(o)?(this.nodesMap.delete(o),this.data.nodes=this.data.nodes.filter(i=>i.id.toString()!==o),this.data.links=this.data.links.filter(i=>{const e=typeof i.source=="object"?i.source.id:i.source,s=typeof i.target=="object"?i.target.id:i.target;return e?.toString()!==o&&s?.toString()!==o}),this.refreshGraph(),!0):!1}async addData(t){t.nodes.forEach(o=>{this.nodesMap.has(o.id.toString())||this.nodesMap.set(o.id.toString(),o)}),t.links.forEach(o=>{const i=this.createLinkKey(o.source,o.target);this.linkMap.has(i)?console.log("link already exists",o):this.linkMap.set(i,o)}),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(),this.data={nodes:[],links:[]},this.nodesMap.clear(),this.graph=new f(this.container),this.initGraph()}getData(){return this.data}getNodesData(){return this.data.nodes}getLinksData(){return this.data.links}createLinkKey(t,o){const i=t===void 0?"undefined":typeof t=="object"?t.id:t,e=o===void 0?"undefined":typeof o=="object"?o.id:o;return`${i}-${e}`}graphData(t){return this.nodesMap.clear(),this.linkMap.clear(),t.nodes.forEach(o=>{this.nodesMap.has(o.id.toString())||this.nodesMap.set(o.id.toString(),o)}),t.links.forEach(o=>{const i=this.createLinkKey(o.source,o.target);this.linkMap.has(i)||this.linkMap.set(i,o)}),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",groupFillColor:"rgba(0, 0, 0, 0.05)",groupFillOpacity:1,groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupPadding:20}).forEach(([i,e])=>{this.options[i]===void 0&&(this.options[i]=e)}),this.applyOptions(),this.refreshGraph(),this}setGroupBy(t){return this.options.groupBy=t,this.applyOptions(),this.refreshGraph(),this}setGroupOptions(t){return t.fillColor!==void 0&&(this.options.groupFillColor=t.fillColor),t.fillOpacity!==void 0&&(this.options.groupFillOpacity=t.fillOpacity),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.padding!==void 0&&(this.options.groupPadding=t.padding),this.applyOptions(),this.refreshGraph(),this}getGroups(){const t=new Set;return this.data.nodes.forEach(o=>{const i=this.getNodeGroupId(o);i&&t.add(i)}),Array.from(t)}getNodesInGroup(t){return this.data.nodes.filter(o=>this.getNodeGroupId(o)===t)}getOptions(){return{...this.options}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.graph._destructor()}}u.ForceGraph=v,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(c,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],f):(c=typeof globalThis<"u"?globalThis:c||self,f(c.ForceGraphLib={},c.ForceGraph,c.d3))})(this,(function(c,f,k){"use strict";function S(g){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(g){for(const o in g)if(o!=="default"){const i=Object.getOwnPropertyDescriptor(g,o);Object.defineProperty(t,o,i.get?i:{enumerable:!0,get:()=>g[o]})}}return t.default=g,Object.freeze(t)}const C=S(k);class m{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupHulls=new Map;nodeGroupsCache=null;isFirstRender=!0;autoColorScale=C.scaleOrdinal(C.schemeCategory10);charCache=new Map;constructor(t,o={nodes:[],links:[]},i={}){this.container=t,this.data=o,this.graph=new f(this.container),o.nodes.forEach(r=>{this.nodesMap.set(r.id.toString(),r)});const e={labelThreshold:1.5,showGroups:!1,...i},s=e.showGroups?{groupBy:"topic",groupFillColor:"rgba(0, 0, 0, 0.05)",groupFillOpacity:1,groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupPadding:20}:{};this.options={...s,...e},this.initGraph()}initGraph(){this.applyOptions(),this.render(),this.graphData(this.data),this.graph.onEngineStop(()=>{if(!this.data.nodes.length){this.isFirstRender=!0;return}this.isFirstRender?(this.options.onRenderComplete?this.options.onRenderComplete():this.graph.zoomToFit(400),this.isFirstRender=!1):this.options.onGraphUpdated&&this.options.onGraphUpdated()}),this.graph.onEngineTick(()=>{this.calculateGroupHulls()})}renderer(){return this.graph}focusPosition(t={}){if(Object.values(t).length){if(t.id){const{x:o,y:i}=this.nodesMap.get(t.id)??{x:0,y:0};o&&i&&(t={x:o,y:i})}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),this.options.cooldownTicks!==void 0&&this.graph.cooldownTicks(this.options.cooldownTicks)}force(t,o){return this.graph.d3Force(t,o)}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,o,i)=>{t===this.data.nodes[0]&&this.renderGroups(o,i);const e=this.getNodeSize(t)*2,s=typeof this.options.nodeBorderWidth=="function"?this.options.nodeBorderWidth(t):this.options.nodeBorderWidth,r=this.resolveNodeShape(t),p=t.x||0,n=t.y||0;if(this.drawNodeShape(o,p,n,e,r),o.fillStyle=this.getNodeColor(t),o.fill(),s&&s>0&&(this.drawNodeShape(o,p,n,e,r),o.strokeStyle=this.getNodeBorderColor(t),o.lineWidth=s,o.stroke()),this.options.nodeChar){const a=this.options.nodeChar(t);if(a?.char){const d=`${a.char}|${a.color??"#000"}`,l=this.charCache.get(d);if(l){const u=a.fontSize??e*(this.options.nodeCharSizeRatio??.8);o.drawImage(l,p-u/2,n-u/2,u,u)}}}const h=this.getNodeLabel(t);if(h&&this.shouldShowLabel(t,i)){const a=typeof this.options.nodeLabelColor=="function"?this.options.nodeLabelColor(t):this.options.nodeLabelColor??"#555",l=(this.options.labelFontSize||14)/i;o.font=`${l}px Arial`,o.fillStyle=a,o.textAlign="center",o.textBaseline="middle",o.fillText(h,t.x||0,(t.y||0)+e+l/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}buildCharBitmap(t,o){const e=new OffscreenCanvas(64,64),s=e.getContext("2d");return s.font=`${64*.8}px sans-serif`,s.textAlign="center",s.textBaseline="middle",s.fillStyle=o,s.fillText(t,64/2,64/2),e.transferToImageBitmap()}ensureCharCache(){if(this.options.nodeChar)for(const t of this.data.nodes){const o=this.options.nodeChar(t);if(!o?.char)continue;const i=o.color??"#000",e=`${o.char}|${i}`;this.charCache.has(e)||this.charCache.set(e,this.buildCharBitmap(o.char,i))}}clearCharCache(){this.charCache.forEach(t=>t.close()),this.charCache.clear()}shouldShowLabel(t,o){const i=this.getNodeSize(t)*2,s=(this.options.labelFontSize||14)/o,r=this.options.labelThreshold||1.5;return s<=i*r}updateData(t){const o=new Set(this.data.nodes.map(r=>r.id.toString())),i=t.nodes.filter(r=>!o.has(r.id.toString()));i.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,...i],links:[...this.data.links,...s]},this.ensureCharCache(),this.refreshGraph()}getNodeColor(t){if(typeof this.options.nodeColor=="function"){const o=this.options.nodeColor(t);if(o)return o}return t?.color??""}getNodeBorderColor(t){return typeof this.options.nodeBorderColor=="function"?this.options.nodeBorderColor(t):this.options.nodeBorderColor||"#333"}resolveNodeShape(t){return t.shape?t.shape:typeof this.options.nodeShape=="function"?this.options.nodeShape(t):this.options.nodeShape??"circle"}drawNodeShape(t,o,i,e,s){switch(t.beginPath(),s){case"square":{t.rect(o-e,i-e,e*2,e*2);break}case"triangle":{const r=e*Math.sqrt(3);t.moveTo(o,i-e),t.lineTo(o+r/2,i+e/2),t.lineTo(o-r/2,i+e/2),t.closePath();break}case"diamond":{t.moveTo(o,i-e),t.lineTo(o+e,i),t.lineTo(o,i+e),t.lineTo(o-e,i),t.closePath();break}case"star":{const r=e,p=e*.4,n=5;for(let h=0;h<n*2;h++){const a=h%2===0?r:p,d=Math.PI/n*h-Math.PI/2,l=o+a*Math.cos(d),u=i+a*Math.sin(d);h===0?t.moveTo(l,u):t.lineTo(l,u)}t.closePath();break}case"hexagon":{for(let r=0;r<6;r++){const p=Math.PI/3*r-Math.PI/6,n=o+e*Math.cos(p),h=i+e*Math.sin(p);r===0?t.moveTo(n,h):t.lineTo(n,h)}t.closePath();break}case"circle":default:t.arc(o,i,e,0,2*Math.PI);break}}applyLinkOptions(){this.graph.linkColor(t=>typeof this.options.linkColor=="function"?this.options.linkColor(t):this.options.linkColor?this.options.linkColor:t.color??""),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,o){return typeof t=="function"?t(o):t}calculateGroupHulls(){if(!this.options.showGroups)return;this.groupHulls.clear();const t=this.options.groupPadding||20;this.ensureNodeGroupsCache(),this.nodeGroupsCache.forEach((i,e)=>{if(i.length===0)return;const s=[];let r=1/0,p=0;if(i.forEach(n=>{if(n.x!==void 0&&n.y!==void 0){const a=this.getNodeSize(n)+t;for(let d=0;d<8;d++){const l=d*Math.PI/4;s.push([n.x+a*Math.cos(l),n.y+a*Math.sin(l)])}n.y-a<r&&(r=n.y-a,p=n.x)}}),s.length>=3){const n=C.polygonHull(s);if(n){const h=n.filter(a=>Math.abs(a[1]-r)<t);h.length>0&&(p=h.reduce((a,d)=>a+d[0],0)/h.length),this.groupHulls.set(e,{points:n,labelX:p,labelY:r,nodes:i})}}})}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]}}ensureNodeGroupsCache(){this.nodeGroupsCache||(this.nodeGroupsCache=new Map,this.data.nodes.forEach(t=>{const o=this.getNodeGroupId(t);o&&(this.nodeGroupsCache.has(o)||this.nodeGroupsCache.set(o,[]),this.nodeGroupsCache.get(o).push(t))}))}renderGroups(t,o){if(!this.options.showGroups)return;const i=C.line().curve(C.curveCatmullRomClosed).context(t);this.groupHulls.forEach((e,s)=>{const r=this.getGroupFillColor(s),p=this.options.groupFillOpacity??1,n=this.getGroupBorderColor(s),h=this.options.groupBorderWidth??0,a=this.options.groupBorderOpacity??.3;t.save(),t.beginPath(),i(e.points),r&&(t.globalAlpha=p,t.fillStyle=r,t.fill()),h>0&&n&&(t.globalAlpha=a,t.strokeStyle=n,t.lineWidth=h/o,t.setLineDash([]),t.stroke()),t.restore();const d=this.getGroupLabelColor(s),l=(this.options.groupLabelSize||16)/o;t.save(),t.font=`bold ${l}px Arial`,t.fillStyle=d,t.textAlign="center",t.textBaseline="middle";const u=e.labelY-l/2,y=t.measureText(s).width,b=l;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(e.labelX-y/2-4,u-b/2-2,y+8,b+4),t.globalAlpha=1,t.fillStyle=d,t.fillText(s,e.labelX,u),t.restore()})}getGroupFillColor(t){let o;return typeof this.options.groupFillColor=="function"?o=this.options.groupFillColor(t):o=this.options.groupFillColor,o==="auto"?this.autoColorScale(t):o}getGroupBorderColor(t){let o;return typeof this.options.groupBorderColor=="function"?o=this.options.groupBorderColor(t):o=this.options.groupBorderColor,o==="auto"?this.autoColorScale(t):o||"#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(){if(this.options.cooldownTime!==void 0)return this.options.cooldownTime;const o=this.data.nodes.length*(125/100);return Math.max(4e3,o)}getDataSize(){const{nodes:t,links:o}=this.graph.graphData();return{nodes:t.length,links:o.length}}getAllNodeIds(){return Array.from(this.nodesMap.keys())}updateNode(t,o){const i=this.nodesMap.get(t.toString());if(i){Object.assign(i,o);const e=this.data.nodes.findIndex(s=>s.id.toString()===t.toString());return e!==-1&&(this.data.nodes[e]=i),this.refreshGraph(),!0}return!1}removeNode(t){const o=t.toString();return this.nodesMap.has(o)?(this.nodesMap.delete(o),this.data.nodes=this.data.nodes.filter(i=>i.id.toString()!==o),this.data.links=this.data.links.filter(i=>{const e=typeof i.source=="object"?i.source.id:i.source,s=typeof i.target=="object"?i.target.id:i.target;return e?.toString()!==o&&s?.toString()!==o}),this.refreshGraph(),!0):!1}async addData(t){t.nodes.forEach(o=>{this.nodesMap.has(o.id.toString())||this.nodesMap.set(o.id.toString(),o)}),t.links.forEach(o=>{const i=this.createLinkKey(o.source,o.target);this.linkMap.has(i)?console.log("link already exists",o):this.linkMap.set(i,o)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.ensureCharCache(),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},t.nodeChar!==void 0&&(this.clearCharCache(),this.ensureCharCache()),this.applyOptions()}refreshGraph(){this.graphData(this.data)}reinitialize(){this.initGraph()}reset(){this.graph.pauseAnimation(),this.data={nodes:[],links:[]},this.nodesMap.clear(),this.graph=new f(this.container),this.initGraph()}getData(){return this.data}getNodesData(){return this.data.nodes}getLinksData(){return this.data.links}createLinkKey(t,o){const i=t===void 0?"undefined":typeof t=="object"?t.id:t,e=o===void 0?"undefined":typeof o=="object"?o.id:o;return`${i}-${e}`}graphData(t){return this.nodesMap.clear(),this.linkMap.clear(),this.nodeGroupsCache=null,this.clearCharCache(),t.nodes.forEach(o=>{this.nodesMap.has(o.id.toString())||this.nodesMap.set(o.id.toString(),o)}),t.links.forEach(o=>{const i=this.createLinkKey(o.source,o.target);this.linkMap.has(i)||this.linkMap.set(i,o)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.ensureCharCache(),this.graph.cooldownTime(this.getCooldownTime()),this.graph.graphData(this.data),this}showGroups(t){return this.options.showGroups=t,t&&Object.entries({groupBy:"topic",groupFillColor:"rgba(0, 0, 0, 0.05)",groupFillOpacity:1,groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupPadding:20}).forEach(([i,e])=>{this.options[i]===void 0&&(this.options[i]=e)}),this.applyOptions(),this.refreshGraph(),this}setGroupBy(t){return this.options.groupBy=t,this.applyOptions(),this.refreshGraph(),this}setGroupOptions(t){return t.fillColor!==void 0&&(this.options.groupFillColor=t.fillColor),t.fillOpacity!==void 0&&(this.options.groupFillOpacity=t.fillOpacity),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.padding!==void 0&&(this.options.groupPadding=t.padding),this.applyOptions(),this.refreshGraph(),this}getGroups(){const t=new Set;return this.data.nodes.forEach(o=>{const i=this.getNodeGroupId(o);i&&t.add(i)}),Array.from(t)}getNodesInGroup(t){return this.data.nodes.filter(o=>this.getNodeGroupId(o)===t)}getOptions(){return{...this.options}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.clearCharCache(),this.graph._destructor()}}c.ForceGraph=m,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}));
package/dist/index.d.ts CHANGED
@@ -40,8 +40,10 @@ export declare class ForceGraph<TNode extends NodeData & {
40
40
  private options;
41
41
  private worker;
42
42
  private groupHulls;
43
+ private nodeGroupsCache;
43
44
  private isFirstRender;
44
45
  private autoColorScale;
46
+ private charCache;
45
47
  constructor(container: HTMLElement, initialData?: GraphData<TNode, TLink>, options?: GraphOptions<TNode, TLink>);
46
48
  private initGraph;
47
49
  renderer(): default_2<TNode, TLink>;
@@ -55,6 +57,12 @@ export declare class ForceGraph<TNode extends NodeData & {
55
57
  private applyOptions;
56
58
  private getNodeSize;
57
59
  private getNodeLabel;
60
+ /** Render a single char/emoji to a 64×64 OffscreenCanvas and return an ImageBitmap */
61
+ private buildCharBitmap;
62
+ /** Idempotent — only adds cache entries for chars not yet cached */
63
+ private ensureCharCache;
64
+ /** Release GPU memory and clear the cache */
65
+ private clearCharCache;
58
66
  /**
59
67
  * Check if label should be shown based on threshold logic
60
68
  * The threshold determines when labels become too big relative to nodes
@@ -76,6 +84,10 @@ export declare class ForceGraph<TNode extends NodeData & {
76
84
  * Get the group ID for a node
77
85
  */
78
86
  private getNodeGroupId;
87
+ /**
88
+ * Populate group cache to prevent calculating on every frame
89
+ */
90
+ private ensureNodeGroupsCache;
79
91
  /**
80
92
  * Render group hulls and labels
81
93
  */
@@ -189,6 +201,8 @@ export declare interface GraphOptions<N extends NodeData = NodeData, L extends L
189
201
  nodeGap?: number;
190
202
  linkLabel?: string | ((link: L) => string);
191
203
  nodeIcon?: string | ((node: N) => string);
204
+ nodeChar?: (node: N) => NodeChar | null | undefined;
205
+ nodeCharSizeRatio?: number;
192
206
  loading?: boolean;
193
207
  pointerInteraction?: boolean;
194
208
  keepDragPosition?: boolean;
@@ -223,6 +237,15 @@ export declare interface LinkData<N extends NodeData = NodeData> extends LinkObj
223
237
  [key: string]: any;
224
238
  }
225
239
 
240
+ export declare interface NodeChar {
241
+ /** Character, letter, number, or emoji to display inside the node */
242
+ char: string;
243
+ /** Absolute size in world units. If omitted, uses node size * nodeCharSizeRatio */
244
+ fontSize?: number;
245
+ /** Fill color. Defaults to '#000' */
246
+ color?: string;
247
+ }
248
+
226
249
  export declare interface NodeData extends NodeObject {
227
250
  id: string | number;
228
251
  x?: 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.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "Force-directed graph visualization library.",
6
6
  "author": "Rohmad Kurniadi",
7
7
  "license": "MIT",