@logixode/force-graph-lib 0.5.0 → 0.6.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,6 +1,6 @@
1
- import c from "force-graph";
2
- import "d3";
3
- class C {
1
+ import y from "force-graph";
2
+ import * as g from "d3";
3
+ class S {
4
4
  container;
5
5
  graph;
6
6
  data = { nodes: [], links: [] };
@@ -8,10 +8,11 @@ class C {
8
8
  linkMap = /* @__PURE__ */ new Map();
9
9
  options;
10
10
  worker = null;
11
- groupBounds = /* @__PURE__ */ new Map();
11
+ groupHulls = /* @__PURE__ */ new Map();
12
12
  isFirstRender = !0;
13
+ autoColorScale = g.scaleOrdinal(g.schemeCategory10);
13
14
  constructor(t, o = { nodes: [], links: [] }, i = {}) {
14
- this.container = t, this.data = o, this.graph = new c(this.container), o.nodes.forEach((r) => {
15
+ this.container = t, this.data = o, this.graph = new y(this.container), o.nodes.forEach((r) => {
15
16
  this.nodesMap.set(r.id.toString(), r);
16
17
  });
17
18
  const e = {
@@ -21,17 +22,18 @@ class C {
21
22
  ...i
22
23
  }, s = e.showGroups ? {
23
24
  groupBy: "topic",
25
+ groupFillColor: "rgba(0, 0, 0, 0.05)",
26
+ groupFillOpacity: 1,
24
27
  groupBorderColor: "#666",
25
28
  groupBorderWidth: 2,
26
29
  groupBorderOpacity: 0.3,
27
30
  groupLabelColor: "#333",
28
31
  groupLabelSize: 16,
29
- groupLabelThreshold: 0.8,
30
32
  groupPadding: 20
31
33
  } : {};
32
34
  this.options = {
33
- ...e,
34
- ...s
35
+ ...s,
36
+ ...e
35
37
  }, this.initGraph();
36
38
  }
37
39
  initGraph() {
@@ -66,12 +68,12 @@ class C {
66
68
  t.fx = t.x, t.fy = t.y;
67
69
  }), this.options.pointerInteraction || this.graph.enablePointerInteraction(!1), this.graph.nodeCanvasObject((t, o, i) => {
68
70
  t === this.data.nodes[0] && this.renderGroups(o, i);
69
- const e = this.getNodeSize(t) * 2, s = typeof this.options.nodeBorderWidth == "function" ? this.options.nodeBorderWidth(t) : this.options.nodeBorderWidth, r = this.resolveNodeShape(t), a = t.x || 0, h = t.y || 0;
70
- this.drawNodeShape(o, a, h, e, r), o.fillStyle = this.getNodeColor(t), o.fill(), s && s > 0 && (this.drawNodeShape(o, a, h, e, r), o.strokeStyle = this.getNodeBorderColor(t), o.lineWidth = s, o.stroke());
71
- const n = this.getNodeLabel(t);
72
- if (n && this.shouldShowLabel(t, i)) {
73
- const p = typeof this.options.nodeLabelColor == "function" ? this.options.nodeLabelColor(t) : this.options.nodeLabelColor ?? "#555", l = (this.options.labelFontSize || 14) / i;
74
- o.font = `${l}px Arial`, o.fillStyle = p, o.textAlign = "center", o.textBaseline = "middle", o.fillText(n, t.x || 0, (t.y || 0) + e + l / 2 + 2);
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
77
  }
76
78
  }), this.applyLinkOptions();
77
79
  }
@@ -133,18 +135,18 @@ class C {
133
135
  break;
134
136
  }
135
137
  case "star": {
136
- const r = e, a = e * 0.4, h = 5;
137
- for (let n = 0; n < h * 2; n++) {
138
- const p = n % 2 === 0 ? r : a, d = Math.PI / h * n - Math.PI / 2, l = o + p * Math.cos(d), u = i + p * Math.sin(d);
139
- n === 0 ? t.moveTo(l, u) : t.lineTo(l, u);
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);
140
142
  }
141
143
  t.closePath();
142
144
  break;
143
145
  }
144
146
  case "hexagon": {
145
147
  for (let r = 0; r < 6; r++) {
146
- const a = Math.PI / 3 * r - Math.PI / 6, h = o + e * Math.cos(a), n = i + e * Math.sin(a);
147
- r === 0 ? t.moveTo(h, n) : t.lineTo(h, n);
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);
148
150
  }
149
151
  t.closePath();
150
152
  break;
@@ -175,30 +177,43 @@ class C {
175
177
  return typeof t == "function" ? t(o) : t;
176
178
  }
177
179
  /**
178
- * Calculate group boundaries based on node positions
180
+ * Calculate group convex hulls based on node positions
179
181
  */
180
- calculateGroupBounds() {
182
+ calculateGroupHulls() {
181
183
  if (!this.options.showGroups) return;
182
- this.groupBounds.clear();
184
+ this.groupHulls.clear();
183
185
  const t = this.options.groupPadding || 20, o = /* @__PURE__ */ new Map();
184
186
  this.data.nodes.forEach((i) => {
185
187
  const e = this.getNodeGroupId(i);
186
188
  e && (o.has(e) || o.set(e, []), o.get(e).push(i));
187
189
  }), o.forEach((i, e) => {
188
190
  if (i.length === 0) return;
189
- let s = 1 / 0, r = 1 / 0, a = -1 / 0, h = -1 / 0;
190
- i.forEach((n) => {
191
+ const s = [];
192
+ let r = 1 / 0, l = 0;
193
+ if (i.forEach((n) => {
191
194
  if (n.x !== void 0 && n.y !== void 0) {
192
- const p = this.getNodeSize(n);
193
- s = Math.min(s, n.x - p), r = Math.min(r, n.y - p), a = Math.max(a, n.x + p), h = Math.max(h, n.y + p);
195
+ const h = this.getNodeSize(n) + t;
196
+ for (let p = 0; p < 8; p++) {
197
+ const d = p * Math.PI / 4;
198
+ s.push([
199
+ n.x + h * Math.cos(d),
200
+ n.y + h * Math.sin(d)
201
+ ]);
202
+ }
203
+ n.y - h < r && (r = n.y - h, l = n.x);
194
204
  }
195
- }), s !== 1 / 0 && this.groupBounds.set(e, {
196
- minX: s - t,
197
- minY: r - t,
198
- maxX: a + t,
199
- maxY: h + t,
200
- nodes: i
201
- });
205
+ }), s.length >= 3) {
206
+ const n = g.polygonHull(s);
207
+ 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, {
210
+ points: n,
211
+ labelX: l,
212
+ labelY: r,
213
+ nodes: i
214
+ });
215
+ }
216
+ }
202
217
  });
203
218
  }
204
219
  /**
@@ -213,31 +228,39 @@ class C {
213
228
  }
214
229
  }
215
230
  /**
216
- * Render group borders and labels
231
+ * Render group hulls and labels
217
232
  */
218
233
  renderGroups(t, o) {
219
- this.options.showGroups && (this.calculateGroupBounds(), this.groupBounds.forEach((i, e) => {
220
- const s = this.getGroupBorderColor(e), r = this.options.groupBorderWidth || 2, a = this.options.groupBorderOpacity || 0.3;
221
- t.save(), t.globalAlpha = a, t.strokeStyle = s, t.lineWidth = r / o, t.setLineDash([10 / o, 5 / o]), t.strokeRect(i.minX, i.minY, i.maxX - i.minX, i.maxY - i.minY), t.restore();
222
- const h = this.options.groupLabelThreshold || 0.8;
223
- if (o <= h) {
224
- const n = this.getGroupLabelColor(e), p = (this.options.groupLabelSize || 16) / o;
225
- t.save(), t.font = `bold ${p}px Arial`, t.fillStyle = n, t.textAlign = "center", t.textBaseline = "middle";
226
- const d = (i.minX + i.maxX) / 2, l = i.minY - p / 2, g = t.measureText(e).width, f = p;
227
- t.globalAlpha = 0.8, t.fillStyle = "rgba(255, 255, 255, 0.9)", t.fillRect(
228
- d - g / 2 - 4,
229
- l - f / 2 - 2,
230
- g + 8,
231
- f + 4
232
- ), t.globalAlpha = 1, t.fillStyle = n, t.fillText(e, d, l), t.restore();
233
- }
234
- }));
234
+ if (!this.options.showGroups) return;
235
+ this.calculateGroupHulls();
236
+ const i = g.line().curve(g.curveCatmullRomClosed).context(t);
237
+ 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;
243
+ t.globalAlpha = 0.8, t.fillStyle = "rgba(255, 255, 255, 0.9)", t.fillRect(
244
+ e.labelX - f / 2 - 4,
245
+ u - c / 2 - 2,
246
+ f + 8,
247
+ c + 4
248
+ ), t.globalAlpha = 1, t.fillStyle = p, t.fillText(s, e.labelX, u), t.restore();
249
+ });
250
+ }
251
+ /**
252
+ * Get group fill color
253
+ */
254
+ getGroupFillColor(t) {
255
+ let o;
256
+ return typeof this.options.groupFillColor == "function" ? o = this.options.groupFillColor(t) : o = this.options.groupFillColor, o === "auto" ? this.autoColorScale(t) : o;
235
257
  }
236
258
  /**
237
259
  * Get group border color
238
260
  */
239
261
  getGroupBorderColor(t) {
240
- return typeof this.options.groupBorderColor == "function" ? this.options.groupBorderColor(t) : this.options.groupBorderColor || "#666";
262
+ let o;
263
+ return typeof this.options.groupBorderColor == "function" ? o = this.options.groupBorderColor(t) : o = this.options.groupBorderColor, o === "auto" ? this.autoColorScale(t) : o || "#666";
241
264
  }
242
265
  /**
243
266
  * Get group label color
@@ -322,7 +345,7 @@ class C {
322
345
  this.initGraph();
323
346
  }
324
347
  reset() {
325
- this.graph.pauseAnimation(), this.data = { nodes: [], links: [] }, this.nodesMap.clear(), this.graph = new c(this.container), this.initGraph();
348
+ this.graph.pauseAnimation(), this.data = { nodes: [], links: [] }, this.nodesMap.clear(), this.graph = new y(this.container), this.initGraph();
326
349
  }
327
350
  getData() {
328
351
  return this.data;
@@ -358,12 +381,13 @@ class C {
358
381
  showGroups(t) {
359
382
  return this.options.showGroups = t, t && Object.entries({
360
383
  groupBy: "topic",
384
+ groupFillColor: "rgba(0, 0, 0, 0.05)",
385
+ groupFillOpacity: 1,
361
386
  groupBorderColor: "#666",
362
387
  groupBorderWidth: 2,
363
388
  groupBorderOpacity: 0.3,
364
389
  groupLabelColor: "#333",
365
390
  groupLabelSize: 16,
366
- groupLabelThreshold: 0.8,
367
391
  groupPadding: 20
368
392
  }).forEach(([i, e]) => {
369
393
  this.options[i] === void 0 && (this.options[i] = e);
@@ -379,7 +403,7 @@ class C {
379
403
  * Set group visualization options
380
404
  */
381
405
  setGroupOptions(t) {
382
- 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;
406
+ 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;
383
407
  }
384
408
  /**
385
409
  * Get all available groups
@@ -408,5 +432,5 @@ class C {
408
432
  }
409
433
  }
410
434
  export {
411
- C as ForceGraph
435
+ S as ForceGraph
412
436
  };
@@ -1 +1 @@
1
- (function(d,u){typeof exports=="object"&&typeof module<"u"?u(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],u):(d=typeof globalThis<"u"?globalThis:d||self,u(d.ForceGraphLib={},d.ForceGraph))})(this,(function(d,u){"use strict";class k{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupBounds=new Map;isFirstRender=!0;constructor(t,o={nodes:[],links:[]},i={}){this.container=t,this.data=o,this.graph=new u(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",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(()=>{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),a=t.x||0,h=t.y||0;this.drawNodeShape(o,a,h,e,r),o.fillStyle=this.getNodeColor(t),o.fill(),s&&s>0&&(this.drawNodeShape(o,a,h,e,r),o.strokeStyle=this.getNodeBorderColor(t),o.lineWidth=s,o.stroke());const n=this.getNodeLabel(t);if(n&&this.shouldShowLabel(t,i)){const p=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=p,o.textAlign="center",o.textBaseline="middle",o.fillText(n,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}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,a=e*.4,h=5;for(let n=0;n<h*2;n++){const p=n%2===0?r:a,f=Math.PI/h*n-Math.PI/2,l=o+p*Math.cos(f),g=i+p*Math.sin(f);n===0?t.moveTo(l,g):t.lineTo(l,g)}t.closePath();break}case"hexagon":{for(let r=0;r<6;r++){const a=Math.PI/3*r-Math.PI/6,h=o+e*Math.cos(a),n=i+e*Math.sin(a);r===0?t.moveTo(h,n):t.lineTo(h,n)}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}calculateGroupBounds(){if(!this.options.showGroups)return;this.groupBounds.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;let s=1/0,r=1/0,a=-1/0,h=-1/0;i.forEach(n=>{if(n.x!==void 0&&n.y!==void 0){const p=this.getNodeSize(n);s=Math.min(s,n.x-p),r=Math.min(r,n.y-p),a=Math.max(a,n.x+p),h=Math.max(h,n.y+p)}}),s!==1/0&&this.groupBounds.set(e,{minX:s-t,minY:r-t,maxX:a+t,maxY:h+t,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){this.options.showGroups&&(this.calculateGroupBounds(),this.groupBounds.forEach((i,e)=>{const s=this.getGroupBorderColor(e),r=this.options.groupBorderWidth||2,a=this.options.groupBorderOpacity||.3;t.save(),t.globalAlpha=a,t.strokeStyle=s,t.lineWidth=r/o,t.setLineDash([10/o,5/o]),t.strokeRect(i.minX,i.minY,i.maxX-i.minX,i.maxY-i.minY),t.restore();const h=this.options.groupLabelThreshold||.8;if(o<=h){const n=this.getGroupLabelColor(e),p=(this.options.groupLabelSize||16)/o;t.save(),t.font=`bold ${p}px Arial`,t.fillStyle=n,t.textAlign="center",t.textBaseline="middle";const f=(i.minX+i.maxX)/2,l=i.minY-p/2,c=t.measureText(e).width,y=p;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(f-c/2-4,l-y/2-2,c+8,y+4),t.globalAlpha=1,t.fillStyle=n,t.fillText(e,f,l),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(){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 u(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",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,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.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(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()}}d.ForceGraph=k,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
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"})}));
package/dist/index.d.ts CHANGED
@@ -39,8 +39,9 @@ export declare class ForceGraph<TNode extends NodeData & {
39
39
  private linkMap;
40
40
  private options;
41
41
  private worker;
42
- private groupBounds;
42
+ private groupHulls;
43
43
  private isFirstRender;
44
+ private autoColorScale;
44
45
  constructor(container: HTMLElement, initialData?: GraphData<TNode, TLink>, options?: GraphOptions<TNode, TLink>);
45
46
  private initGraph;
46
47
  renderer(): default_2<TNode, TLink>;
@@ -68,17 +69,21 @@ export declare class ForceGraph<TNode extends NodeData & {
68
69
  private getLinkCurvature;
69
70
  private getLinkProperty;
70
71
  /**
71
- * Calculate group boundaries based on node positions
72
+ * Calculate group convex hulls based on node positions
72
73
  */
73
- private calculateGroupBounds;
74
+ private calculateGroupHulls;
74
75
  /**
75
76
  * Get the group ID for a node
76
77
  */
77
78
  private getNodeGroupId;
78
79
  /**
79
- * Render group borders and labels
80
+ * Render group hulls and labels
80
81
  */
81
82
  private renderGroups;
83
+ /**
84
+ * Get group fill color
85
+ */
86
+ private getGroupFillColor;
82
87
  /**
83
88
  * Get group border color
84
89
  */
@@ -139,12 +144,13 @@ export declare class ForceGraph<TNode extends NodeData & {
139
144
  * Set group visualization options
140
145
  */
141
146
  setGroupOptions(options: {
147
+ fillColor?: string | ((groupId: string) => string);
148
+ fillOpacity?: number;
142
149
  borderColor?: string | ((groupId: string) => string);
143
150
  borderWidth?: number;
144
151
  borderOpacity?: number;
145
152
  labelColor?: string | ((groupId: string) => string);
146
153
  labelSize?: number;
147
- labelThreshold?: number;
148
154
  padding?: number;
149
155
  }): ForceGraph<TNode, TLink>;
150
156
  /**
@@ -196,12 +202,13 @@ export declare interface GraphOptions<N extends NodeData = NodeData, L extends L
196
202
  onGraphUpdated?: () => void;
197
203
  showGroups?: boolean;
198
204
  groupBy?: string | ((node: N) => string | undefined);
205
+ groupFillColor?: string | ((groupId: string) => string);
206
+ groupFillOpacity?: number;
199
207
  groupBorderColor?: string | ((groupId: string) => string);
200
208
  groupBorderWidth?: number;
201
209
  groupBorderOpacity?: number;
202
210
  groupLabelColor?: string | ((groupId: string) => string);
203
211
  groupLabelSize?: number;
204
- groupLabelThreshold?: number;
205
212
  groupPadding?: number;
206
213
  cooldownTime?: number;
207
214
  cooldownTicks?: 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.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "Force-directed graph visualization library.",
6
6
  "author": "Rohmad Kurniadi",
7
7
  "license": "MIT",