@logixode/force-graph-lib 0.1.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.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/force-graph-lib.es.js +504 -0
- package/dist/force-graph-lib.umd.js +3 -0
- package/dist/index.d.ts +195 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rohmad Kurniadi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Force Graph Library
|
|
2
|
+
|
|
3
|
+
A TypeScript library for creating interactive force-directed graphs with advanced features and optimizations for handling large datasets.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 Dynamic label threshold control
|
|
8
|
+
- 🔄 Multiple layout algorithms (Force-directed and Circle Pack)
|
|
9
|
+
- 🎨 Customizable styling for nodes and edges
|
|
10
|
+
- 📊 Efficient handling of large datasets
|
|
11
|
+
- 🔄 Pagination support for incremental data loading
|
|
12
|
+
- 🔄 Graph refresh and reset capabilities
|
|
13
|
+
- ⚡ Non-blocking calculations using Web Workers
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install force-graph-lib
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ForceGraph } from 'force-graph-lib'
|
|
25
|
+
|
|
26
|
+
// Initialize the graph
|
|
27
|
+
const container = document.getElementById('graph-container')
|
|
28
|
+
const graph = new ForceGraph(
|
|
29
|
+
container,
|
|
30
|
+
{
|
|
31
|
+
nodes: [
|
|
32
|
+
{ id: '1', name: 'Node 1' },
|
|
33
|
+
{ id: '2', name: 'Node 2' },
|
|
34
|
+
],
|
|
35
|
+
links: [{ source: '1', target: '2' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
labelThreshold: 1.2,
|
|
39
|
+
nodeSize: (node) => node.size || 5,
|
|
40
|
+
nodeLabel: (node) => node.name,
|
|
41
|
+
nodeIcon: '●',
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Add more data incrementally
|
|
46
|
+
graph.addData({
|
|
47
|
+
nodes: [{ id: '3', name: 'Node 3' }],
|
|
48
|
+
links: [{ source: '2', target: '3' }],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Change layout
|
|
52
|
+
graph.setLayout('circlepack')
|
|
53
|
+
|
|
54
|
+
// Update label threshold
|
|
55
|
+
graph.setLabelThreshold(1.5)
|
|
56
|
+
|
|
57
|
+
// Refresh or reset the graph
|
|
58
|
+
graph.refreshGraph()
|
|
59
|
+
graph.resetGraph()
|
|
60
|
+
|
|
61
|
+
// Check loading state
|
|
62
|
+
console.log(graph.isLoading())
|
|
63
|
+
|
|
64
|
+
// Clean up
|
|
65
|
+
graph.destroy()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### Constructor
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
new ForceGraph(container: HTMLElement, data: GraphData, options?: GraphOptions)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### GraphData Interface
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
interface GraphData {
|
|
80
|
+
nodes: Array<{
|
|
81
|
+
id: string
|
|
82
|
+
[key: string]: any
|
|
83
|
+
}>
|
|
84
|
+
links: Array<{
|
|
85
|
+
source: string
|
|
86
|
+
target: string
|
|
87
|
+
[key: string]: any
|
|
88
|
+
}>
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### GraphOptions Interface
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface GraphOptions {
|
|
96
|
+
labelThreshold?: number
|
|
97
|
+
layout?: 'force' | 'circlepack'
|
|
98
|
+
nodeSize?: number | ((node: any) => number)
|
|
99
|
+
linkWidth?: number | ((link: any) => number)
|
|
100
|
+
nodeLabel?: string | ((node: any) => string)
|
|
101
|
+
linkLabel?: string | ((link: any) => string)
|
|
102
|
+
nodeIcon?: string | ((node: any) => string)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Methods
|
|
107
|
+
|
|
108
|
+
- `addData(newData: GraphData)`: Add new nodes and edges to the graph
|
|
109
|
+
- `setLayout(layout: 'force' | 'circlepack')`: Change the graph layout algorithm
|
|
110
|
+
- `setLabelThreshold(threshold: number)`: Set the zoom threshold for showing labels
|
|
111
|
+
- `updateStyles(options: Partial<GraphOptions>)`: Update graph styling options
|
|
112
|
+
- `refreshGraph()`: Refresh the graph with current data
|
|
113
|
+
- `resetGraph()`: Reset the graph to its initial state
|
|
114
|
+
- `isLoading()`: Check if the graph is currently calculating layout
|
|
115
|
+
- `destroy()`: Clean up resources and remove the graph
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import S from "force-graph";
|
|
2
|
+
import * as k from "d3";
|
|
3
|
+
function w(o, t) {
|
|
4
|
+
(t == null || t > o.length) && (t = o.length);
|
|
5
|
+
for (var i = 0, r = Array(t); i < t; i++) r[i] = o[i];
|
|
6
|
+
return r;
|
|
7
|
+
}
|
|
8
|
+
function G(o) {
|
|
9
|
+
if (Array.isArray(o)) return o;
|
|
10
|
+
}
|
|
11
|
+
function A(o) {
|
|
12
|
+
if (Array.isArray(o)) return w(o);
|
|
13
|
+
}
|
|
14
|
+
function I(o) {
|
|
15
|
+
if (typeof Symbol < "u" && o[Symbol.iterator] != null || o["@@iterator"] != null) return Array.from(o);
|
|
16
|
+
}
|
|
17
|
+
function P(o, t) {
|
|
18
|
+
var i = o == null ? null : typeof Symbol < "u" && o[Symbol.iterator] || o["@@iterator"];
|
|
19
|
+
if (i != null) {
|
|
20
|
+
var r, n, a, s, h = [], p = !0, e = !1;
|
|
21
|
+
try {
|
|
22
|
+
if (a = (i = i.call(o)).next, t !== 0) for (; !(p = (r = a.call(i)).done) && (h.push(r.value), h.length !== t); p = !0) ;
|
|
23
|
+
} catch (l) {
|
|
24
|
+
e = !0, n = l;
|
|
25
|
+
} finally {
|
|
26
|
+
try {
|
|
27
|
+
if (!p && i.return != null && (s = i.return(), Object(s) !== s)) return;
|
|
28
|
+
} finally {
|
|
29
|
+
if (e) throw n;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return h;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function z() {
|
|
36
|
+
throw new TypeError(`Invalid attempt to destructure non-iterable instance.
|
|
37
|
+
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`);
|
|
38
|
+
}
|
|
39
|
+
function N() {
|
|
40
|
+
throw new TypeError(`Invalid attempt to spread non-iterable instance.
|
|
41
|
+
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`);
|
|
42
|
+
}
|
|
43
|
+
function M(o, t) {
|
|
44
|
+
return G(o) || P(o, t) || L(o, t) || z();
|
|
45
|
+
}
|
|
46
|
+
function C(o) {
|
|
47
|
+
return A(o) || I(o) || L(o) || N();
|
|
48
|
+
}
|
|
49
|
+
function L(o, t) {
|
|
50
|
+
if (o) {
|
|
51
|
+
if (typeof o == "string") return w(o, t);
|
|
52
|
+
var i = {}.toString.call(o).slice(8, -1);
|
|
53
|
+
return i === "Object" && o.constructor && (i = o.constructor.name), i === "Map" || i === "Set" ? Array.from(o) : i === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) ? w(o, t) : void 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function O() {
|
|
57
|
+
var o = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : function(e) {
|
|
58
|
+
return e.cluster;
|
|
59
|
+
}, t, i = [], r = v(1), n = v(0.2), a = 0, s = /* @__PURE__ */ new Map();
|
|
60
|
+
function h(e) {
|
|
61
|
+
var l = new Map(C(s.entries()).map(function(u) {
|
|
62
|
+
var d = M(u, 2), g = d[0], y = d[1];
|
|
63
|
+
return [g, W(y, r, t)];
|
|
64
|
+
})), f = new Map(C(s.entries()).map(function(u) {
|
|
65
|
+
var d = M(u, 2), g = d[0], y = d[1];
|
|
66
|
+
return [g, n(g, y)];
|
|
67
|
+
})), c = ["x", t > 1 && "y", t > 2 && "z"].filter(function(u) {
|
|
68
|
+
return u;
|
|
69
|
+
}), m = c.map(function(u) {
|
|
70
|
+
return "v".concat(u);
|
|
71
|
+
});
|
|
72
|
+
i.forEach(function(u) {
|
|
73
|
+
var d = o(u);
|
|
74
|
+
if (l.has(d)) {
|
|
75
|
+
var g = l.get(d), y = c.map(function(b) {
|
|
76
|
+
return g[b] - u[b];
|
|
77
|
+
});
|
|
78
|
+
if (!(D.apply(void 0, C(y)) <= a)) {
|
|
79
|
+
var B = f.get(d) * e;
|
|
80
|
+
m.forEach(function(b, T) {
|
|
81
|
+
return u[b] += y[T] * B;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function p() {
|
|
88
|
+
s.clear(), i.forEach(function(e) {
|
|
89
|
+
var l = o(e);
|
|
90
|
+
s.has(l) || s.set(l, []), s.get(l).push(e);
|
|
91
|
+
}), s.forEach(function(e, l) {
|
|
92
|
+
return e.length <= 1 && s.delete(l);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return h.initialize = function(e) {
|
|
96
|
+
i = e;
|
|
97
|
+
for (var l = arguments.length, f = new Array(l > 1 ? l - 1 : 0), c = 1; c < l; c++)
|
|
98
|
+
f[c - 1] = arguments[c];
|
|
99
|
+
t = f.find(function(m) {
|
|
100
|
+
return [1, 2, 3].includes(m);
|
|
101
|
+
}) || 2, p();
|
|
102
|
+
}, h.clusterId = function(e) {
|
|
103
|
+
return arguments.length ? (o = e, p(), h) : o;
|
|
104
|
+
}, h.weight = function(e) {
|
|
105
|
+
return arguments.length ? (r = typeof e == "function" ? e : v(+e), h) : r;
|
|
106
|
+
}, h.strength = function(e) {
|
|
107
|
+
return arguments.length ? (n = typeof e == "function" ? e : v(+e), h) : n;
|
|
108
|
+
}, h.distanceMin = function(e) {
|
|
109
|
+
return arguments.length ? (a = e, h) : a;
|
|
110
|
+
}, h;
|
|
111
|
+
}
|
|
112
|
+
function D(o) {
|
|
113
|
+
var t = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 0, i = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : 0;
|
|
114
|
+
return Math.sqrt(o * o + t * t + i * i);
|
|
115
|
+
}
|
|
116
|
+
function v(o) {
|
|
117
|
+
return function() {
|
|
118
|
+
return o;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function W(o, t, i) {
|
|
122
|
+
var r = 0, n = 0, a = 0, s = 0;
|
|
123
|
+
o.forEach(function(p) {
|
|
124
|
+
var e = t(p);
|
|
125
|
+
n += p.x * e, i > 1 && (a += p.y * e), i > 2 && (s += p.z * e), r += e;
|
|
126
|
+
});
|
|
127
|
+
var h = {};
|
|
128
|
+
return h.x = n / r, i > 1 && (h.y = a / r), i > 2 && (h.z = s / r), h;
|
|
129
|
+
}
|
|
130
|
+
class x {
|
|
131
|
+
container;
|
|
132
|
+
graph;
|
|
133
|
+
data = { nodes: [], links: [] };
|
|
134
|
+
nodesMap = /* @__PURE__ */ new Map();
|
|
135
|
+
linkMap = /* @__PURE__ */ new Map();
|
|
136
|
+
options;
|
|
137
|
+
worker = null;
|
|
138
|
+
groupBounds = /* @__PURE__ */ new Map();
|
|
139
|
+
constructor(t, i = { nodes: [], links: [] }, r = {}) {
|
|
140
|
+
this.container = t, this.data = i, this.graph = new S(this.container), i.nodes.forEach((s) => {
|
|
141
|
+
this.nodesMap.set(s.id.toString(), s);
|
|
142
|
+
});
|
|
143
|
+
const n = {
|
|
144
|
+
labelThreshold: 1.5,
|
|
145
|
+
showGroups: !1,
|
|
146
|
+
...r
|
|
147
|
+
}, a = n.showGroups ? {
|
|
148
|
+
groupBy: "topic",
|
|
149
|
+
groupBorderColor: "#666",
|
|
150
|
+
groupBorderWidth: 2,
|
|
151
|
+
groupBorderOpacity: 0.3,
|
|
152
|
+
groupLabelColor: "#333",
|
|
153
|
+
groupLabelSize: 16,
|
|
154
|
+
groupLabelThreshold: 0.8,
|
|
155
|
+
groupPadding: 20
|
|
156
|
+
} : {};
|
|
157
|
+
this.options = {
|
|
158
|
+
...n,
|
|
159
|
+
...a
|
|
160
|
+
}, this.initGraph();
|
|
161
|
+
}
|
|
162
|
+
initGraph() {
|
|
163
|
+
console.log(this.options.width, this.options.width), this.graph.width(this.options.width ?? 800).height(this.options.height ?? 400).onNodeClick((t) => {
|
|
164
|
+
console.log("Node clicked:", t), this.options.nodeClickHandler && this.options.nodeClickHandler(t), this.focusPosition({ x: t.x, y: t.y });
|
|
165
|
+
}).cooldownTime(this.calculateCooldownTime()).d3Force(
|
|
166
|
+
"cluster",
|
|
167
|
+
this.options.cluster ? O().clusterId(this.options.cluster) : null
|
|
168
|
+
).d3Force(
|
|
169
|
+
"collide",
|
|
170
|
+
this.options.collide ? k.forceCollide().radius((t) => this.options.collide ? this.options.collide(t) : t.marker.radius) : null
|
|
171
|
+
), this.options.keepDragPosition && this.graph.onNodeDragEnd((t) => {
|
|
172
|
+
t.fx = t.x, t.fy = t.y;
|
|
173
|
+
}), this.graphData(this.data), this.applyOptions(), this.render(), this.graph.onEngineStop(() => this.graph.zoomToFit(400));
|
|
174
|
+
}
|
|
175
|
+
renderer() {
|
|
176
|
+
return this.graph;
|
|
177
|
+
}
|
|
178
|
+
focusPosition(t = {}) {
|
|
179
|
+
if (Object.values(t).length) {
|
|
180
|
+
if (t.id) {
|
|
181
|
+
const { x: i, y: r } = this.nodesMap.get(t.id) ?? { x: 0, y: 0 };
|
|
182
|
+
i && r && (t = { x: i, y: r });
|
|
183
|
+
}
|
|
184
|
+
t && (this.graph.centerAt(t.x, t.y, 1e3), this.graph.zoom(8, 2e3));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
render() {
|
|
188
|
+
this.graph.d3Force("charge", k.forceManyBody().strength(-100)).d3Force(
|
|
189
|
+
"link",
|
|
190
|
+
k.forceLink().id((t) => t.index ?? "").distance(30)
|
|
191
|
+
).d3Force("center", k.forceCenter());
|
|
192
|
+
}
|
|
193
|
+
applyOptions() {
|
|
194
|
+
this.graph.nodeCanvasObject((t, i, r) => {
|
|
195
|
+
t === this.data.nodes[0] && this.renderGroups(i, r);
|
|
196
|
+
const n = this.getNodeSize(t) * 2, a = typeof this.options.nodeBorderWidth == "function" ? this.options.nodeBorderWidth(t) : this.options.nodeBorderWidth;
|
|
197
|
+
i.beginPath(), i.arc(t.x || 0, t.y || 0, n, 0, 2 * Math.PI), i.fillStyle = this.getNodeColor(t), i.fill(), a && a > 0 && (i.beginPath(), i.arc(t.x || 0, t.y || 0, n, 0, 2 * Math.PI), i.strokeStyle = this.getNodeBorderColor(t), i.lineWidth = a, i.stroke());
|
|
198
|
+
const s = this.getNodeLabel(t);
|
|
199
|
+
if (s && r >= (this.options.labelThreshold || 1.5)) {
|
|
200
|
+
const h = typeof this.options.nodeLabelColor == "function" ? this.options.nodeLabelColor(t) : this.options.nodeLabelColor ?? "#555";
|
|
201
|
+
i.font = `${Math.max(n, 8)}px Arial`, i.fillStyle = h, i.textAlign = "center", i.textBaseline = "middle", i.fillText(s, t.x || 0, (t.y || 0) + n + 2);
|
|
202
|
+
}
|
|
203
|
+
}), this.applyLinkOptions();
|
|
204
|
+
}
|
|
205
|
+
getNodeSize(t) {
|
|
206
|
+
return typeof this.options.nodeSize == "function" ? this.options.nodeSize(t) || t.marker?.radius : this.options.nodeSize || t.marker?.radius || 1;
|
|
207
|
+
}
|
|
208
|
+
getNodeLabel(t) {
|
|
209
|
+
return typeof this.options.nodeLabel == "function" ? this.options.nodeLabel(t) : this.graph.zoom() < (this.options.labelThreshold || 1.5) ? "" : t.label || t.id;
|
|
210
|
+
}
|
|
211
|
+
updateData(t) {
|
|
212
|
+
const i = new Set(this.data.nodes.map((s) => s.id.toString())), r = t.nodes.filter((s) => !i.has(s.id.toString()));
|
|
213
|
+
r.forEach((s) => {
|
|
214
|
+
this.nodesMap.set(s.id.toString(), s);
|
|
215
|
+
});
|
|
216
|
+
const n = new Set(
|
|
217
|
+
this.data.links.map((s) => this.createLinkKey(s.source, s.target))
|
|
218
|
+
), a = t.links.filter(
|
|
219
|
+
(s) => !n.has(this.createLinkKey(s.source, s.target))
|
|
220
|
+
);
|
|
221
|
+
this.data = {
|
|
222
|
+
nodes: [...this.data.nodes, ...r],
|
|
223
|
+
links: [...this.data.links, ...a]
|
|
224
|
+
}, this.refreshGraph();
|
|
225
|
+
}
|
|
226
|
+
getNodeColor(t) {
|
|
227
|
+
if (typeof this.options.nodeColor == "function") {
|
|
228
|
+
const i = this.options.nodeColor(t);
|
|
229
|
+
if (i) return i;
|
|
230
|
+
}
|
|
231
|
+
return t.color ?? "";
|
|
232
|
+
}
|
|
233
|
+
getNodeBorderColor(t) {
|
|
234
|
+
return typeof this.options.nodeBorderColor == "function" ? this.options.nodeBorderColor(t) : this.options.nodeBorderColor || "#333";
|
|
235
|
+
}
|
|
236
|
+
applyLinkOptions() {
|
|
237
|
+
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(
|
|
238
|
+
(t) => this.getLinkProperty(this.options.linkDirectionalParticles, t) ?? 0
|
|
239
|
+
), this.options.linkDirectionalParticleSpeed !== void 0 && this.graph.linkDirectionalParticleSpeed(
|
|
240
|
+
(t) => this.getLinkProperty(this.options.linkDirectionalParticleSpeed, t) ?? 0
|
|
241
|
+
), this.options.linkDirectionalParticleWidth !== void 0 && this.graph.linkDirectionalParticleWidth(
|
|
242
|
+
(t) => this.getLinkProperty(this.options.linkDirectionalParticleWidth, t) ?? 0
|
|
243
|
+
), this.options.linkDirectionalParticleColor !== void 0 && this.graph.linkDirectionalParticleColor(
|
|
244
|
+
(t) => this.getLinkProperty(this.options.linkDirectionalParticleColor, t) ?? "#aaa"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
getLinkCurvature(t) {
|
|
248
|
+
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;
|
|
249
|
+
}
|
|
250
|
+
getLinkProperty(t, i) {
|
|
251
|
+
return typeof t == "function" ? t(i) : t;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Calculate group boundaries based on node positions
|
|
255
|
+
*/
|
|
256
|
+
calculateGroupBounds() {
|
|
257
|
+
if (!this.options.showGroups) return;
|
|
258
|
+
this.groupBounds.clear();
|
|
259
|
+
const t = this.options.groupPadding || 20, i = /* @__PURE__ */ new Map();
|
|
260
|
+
this.data.nodes.forEach((r) => {
|
|
261
|
+
const n = this.getNodeGroupId(r);
|
|
262
|
+
n && (i.has(n) || i.set(n, []), i.get(n).push(r));
|
|
263
|
+
}), i.forEach((r, n) => {
|
|
264
|
+
if (r.length === 0) return;
|
|
265
|
+
let a = 1 / 0, s = 1 / 0, h = -1 / 0, p = -1 / 0;
|
|
266
|
+
r.forEach((e) => {
|
|
267
|
+
if (e.x !== void 0 && e.y !== void 0) {
|
|
268
|
+
const l = this.getNodeSize(e);
|
|
269
|
+
a = Math.min(a, e.x - l), s = Math.min(s, e.y - l), h = Math.max(h, e.x + l), p = Math.max(p, e.y + l);
|
|
270
|
+
}
|
|
271
|
+
}), a !== 1 / 0 && this.groupBounds.set(n, {
|
|
272
|
+
minX: a - t,
|
|
273
|
+
minY: s - t,
|
|
274
|
+
maxX: h + t,
|
|
275
|
+
maxY: p + t,
|
|
276
|
+
nodes: r
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get the group ID for a node
|
|
282
|
+
*/
|
|
283
|
+
getNodeGroupId(t) {
|
|
284
|
+
if (this.options.groupBy) {
|
|
285
|
+
if (typeof this.options.groupBy == "function")
|
|
286
|
+
return this.options.groupBy(t);
|
|
287
|
+
if (typeof this.options.groupBy == "string")
|
|
288
|
+
return t[this.options.groupBy];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Render group borders and labels
|
|
293
|
+
*/
|
|
294
|
+
renderGroups(t, i) {
|
|
295
|
+
this.options.showGroups && (this.calculateGroupBounds(), this.groupBounds.forEach((r, n) => {
|
|
296
|
+
const a = this.getGroupBorderColor(n), s = this.options.groupBorderWidth || 2, h = this.options.groupBorderOpacity || 0.3;
|
|
297
|
+
t.save(), t.globalAlpha = h, t.strokeStyle = a, t.lineWidth = s / i, t.setLineDash([10 / i, 5 / i]), t.strokeRect(r.minX, r.minY, r.maxX - r.minX, r.maxY - r.minY), t.restore();
|
|
298
|
+
const p = this.options.groupLabelThreshold || 0.8;
|
|
299
|
+
if (i <= p) {
|
|
300
|
+
const e = this.getGroupLabelColor(n), l = (this.options.groupLabelSize || 16) / i;
|
|
301
|
+
t.save(), t.font = `bold ${l}px Arial`, t.fillStyle = e, t.textAlign = "center", t.textBaseline = "middle";
|
|
302
|
+
const f = (r.minX + r.maxX) / 2, c = r.minY - l / 2, u = t.measureText(n).width, d = l;
|
|
303
|
+
t.globalAlpha = 0.8, t.fillStyle = "rgba(255, 255, 255, 0.9)", t.fillRect(
|
|
304
|
+
f - u / 2 - 4,
|
|
305
|
+
c - d / 2 - 2,
|
|
306
|
+
u + 8,
|
|
307
|
+
d + 4
|
|
308
|
+
), t.globalAlpha = 1, t.fillStyle = e, t.fillText(n, f, c), t.restore();
|
|
309
|
+
}
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get group border color
|
|
314
|
+
*/
|
|
315
|
+
getGroupBorderColor(t) {
|
|
316
|
+
return typeof this.options.groupBorderColor == "function" ? this.options.groupBorderColor(t) : this.options.groupBorderColor || "#666";
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get group label color
|
|
320
|
+
*/
|
|
321
|
+
getGroupLabelColor(t) {
|
|
322
|
+
return typeof this.options.groupLabelColor == "function" ? this.options.groupLabelColor(t) : this.options.groupLabelColor || "#333";
|
|
323
|
+
}
|
|
324
|
+
getNodeById(t) {
|
|
325
|
+
return this.nodesMap.get(t.toString());
|
|
326
|
+
}
|
|
327
|
+
hasNode(t) {
|
|
328
|
+
return this.nodesMap.has(t.toString());
|
|
329
|
+
}
|
|
330
|
+
getNodeCount() {
|
|
331
|
+
return this.nodesMap.size;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Calculate dynamic cooldown time based on node count
|
|
335
|
+
* Minimum: 2500ms
|
|
336
|
+
* Normal: node.length * 150ms
|
|
337
|
+
*/
|
|
338
|
+
calculateCooldownTime() {
|
|
339
|
+
const t = this.data.nodes.length, i = t * 5 / 2, r = Math.max(2500, i);
|
|
340
|
+
return console.log(
|
|
341
|
+
`Dynamic Cooldown Time: ${t} nodes × 150ms = ${i}ms, final: ${r}ms`
|
|
342
|
+
), r;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Get the current calculated cooldown time
|
|
346
|
+
*/
|
|
347
|
+
getCooldownTime() {
|
|
348
|
+
return this.calculateCooldownTime();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Manually update the cooldown time based on current node count
|
|
352
|
+
*/
|
|
353
|
+
updateCooldownTime() {
|
|
354
|
+
this.graph.cooldownTime(this.calculateCooldownTime());
|
|
355
|
+
}
|
|
356
|
+
getLinkCount() {
|
|
357
|
+
return this.graph.graphData().links.length;
|
|
358
|
+
}
|
|
359
|
+
getDataSize() {
|
|
360
|
+
const { nodes: t, links: i } = this.graph.graphData();
|
|
361
|
+
return {
|
|
362
|
+
nodes: t.length,
|
|
363
|
+
links: i.length
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
getAllNodeIds() {
|
|
367
|
+
return Array.from(this.nodesMap.keys());
|
|
368
|
+
}
|
|
369
|
+
updateNode(t, i) {
|
|
370
|
+
const r = this.nodesMap.get(t.toString());
|
|
371
|
+
if (r) {
|
|
372
|
+
Object.assign(r, i);
|
|
373
|
+
const n = this.data.nodes.findIndex((a) => a.id.toString() === t.toString());
|
|
374
|
+
return n !== -1 && (this.data.nodes[n] = r), this.refreshGraph(), !0;
|
|
375
|
+
}
|
|
376
|
+
return !1;
|
|
377
|
+
}
|
|
378
|
+
removeNode(t) {
|
|
379
|
+
const i = t.toString();
|
|
380
|
+
return this.nodesMap.has(i) ? (this.nodesMap.delete(i), this.data.nodes = this.data.nodes.filter((r) => r.id.toString() !== i), this.data.links = this.data.links.filter((r) => {
|
|
381
|
+
const n = typeof r.source == "object" ? r.source.id : r.source, a = typeof r.target == "object" ? r.target.id : r.target;
|
|
382
|
+
return n.toString() !== i && a.toString() !== i;
|
|
383
|
+
}), this.graph.cooldownTime(this.calculateCooldownTime()), this.refreshGraph(), !0) : !1;
|
|
384
|
+
}
|
|
385
|
+
async addData(t) {
|
|
386
|
+
t.nodes.forEach((i) => {
|
|
387
|
+
this.nodesMap.has(i.id.toString()) || this.nodesMap.set(i.id.toString(), i);
|
|
388
|
+
}), t.links.forEach((i) => {
|
|
389
|
+
const r = this.createLinkKey(i.source, i.target);
|
|
390
|
+
this.linkMap.has(r) ? console.log("link already exists", i) : this.linkMap.set(r, i);
|
|
391
|
+
}), this.data = {
|
|
392
|
+
nodes: Array.from(this.nodesMap.values()),
|
|
393
|
+
links: Array.from(this.linkMap.values())
|
|
394
|
+
}, console.log("this.data", this.data, t), this.graph.cooldownTime(this.calculateCooldownTime()), this.graph.graphData(this.data);
|
|
395
|
+
}
|
|
396
|
+
setLabelThreshold(t) {
|
|
397
|
+
this.options.labelThreshold = t, this.render();
|
|
398
|
+
}
|
|
399
|
+
setOptions(t) {
|
|
400
|
+
this.options = { ...this.options, ...t }, this.applyOptions();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Lightweight refresh - only updates graph data
|
|
404
|
+
*/
|
|
405
|
+
refreshGraph() {
|
|
406
|
+
this.graphData(this.data);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Complete reinitialization - use when major changes are needed
|
|
410
|
+
*/
|
|
411
|
+
reinitialize() {
|
|
412
|
+
this.initGraph();
|
|
413
|
+
}
|
|
414
|
+
reset() {
|
|
415
|
+
this.graph.pauseAnimation();
|
|
416
|
+
const t = this.graph.d3Force("simulation");
|
|
417
|
+
t && t.stop(), this.data = { nodes: [], links: [] }, this.nodesMap.clear(), this.graph = new S(this.container), this.initGraph();
|
|
418
|
+
}
|
|
419
|
+
getData() {
|
|
420
|
+
return this.data;
|
|
421
|
+
}
|
|
422
|
+
getNodesData() {
|
|
423
|
+
return this.data.nodes;
|
|
424
|
+
}
|
|
425
|
+
getLinksData() {
|
|
426
|
+
return this.data.links;
|
|
427
|
+
}
|
|
428
|
+
createLinkKey(t, i) {
|
|
429
|
+
const r = typeof t == "object" ? t.id : t, n = typeof i == "object" ? i.id : i;
|
|
430
|
+
return `${r}-${n}`;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Set graph data (chainable method)
|
|
434
|
+
* @param data - Graph data to set
|
|
435
|
+
*/
|
|
436
|
+
graphData(t) {
|
|
437
|
+
return this.nodesMap.clear(), this.linkMap.clear(), t.nodes.forEach((i) => {
|
|
438
|
+
this.nodesMap.has(i.id.toString()) || this.nodesMap.set(i.id.toString(), i);
|
|
439
|
+
}), t.links.forEach((i) => {
|
|
440
|
+
const r = this.createLinkKey(i.source, i.target);
|
|
441
|
+
this.linkMap.has(r) || this.linkMap.set(r, i);
|
|
442
|
+
}), this.data = {
|
|
443
|
+
nodes: Array.from(this.nodesMap.values()),
|
|
444
|
+
links: Array.from(this.linkMap.values())
|
|
445
|
+
}, this.graph.cooldownTime(this.calculateCooldownTime()), this.graph.graphData(this.data), this;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Enable or disable group visualization
|
|
449
|
+
*/
|
|
450
|
+
showGroups(t) {
|
|
451
|
+
return this.options.showGroups = t, t && Object.entries({
|
|
452
|
+
groupBy: "topic",
|
|
453
|
+
groupBorderColor: "#666",
|
|
454
|
+
groupBorderWidth: 2,
|
|
455
|
+
groupBorderOpacity: 0.3,
|
|
456
|
+
groupLabelColor: "#333",
|
|
457
|
+
groupLabelSize: 16,
|
|
458
|
+
groupLabelThreshold: 0.8,
|
|
459
|
+
groupPadding: 20
|
|
460
|
+
}).forEach(([r, n]) => {
|
|
461
|
+
this.options[r] === void 0 && (this.options[r] = n);
|
|
462
|
+
}), this.applyOptions(), this.refreshGraph(), this;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Set the property to group nodes by
|
|
466
|
+
*/
|
|
467
|
+
setGroupBy(t) {
|
|
468
|
+
return this.options.groupBy = t, this.applyOptions(), this.refreshGraph(), this;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Set group visualization options
|
|
472
|
+
*/
|
|
473
|
+
setGroupOptions(t) {
|
|
474
|
+
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;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Get all available groups
|
|
478
|
+
*/
|
|
479
|
+
getGroups() {
|
|
480
|
+
const t = /* @__PURE__ */ new Set();
|
|
481
|
+
return this.data.nodes.forEach((i) => {
|
|
482
|
+
const r = this.getNodeGroupId(i);
|
|
483
|
+
r && t.add(r);
|
|
484
|
+
}), Array.from(t);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get nodes in a specific group
|
|
488
|
+
*/
|
|
489
|
+
getNodesInGroup(t) {
|
|
490
|
+
return this.data.nodes.filter((i) => this.getNodeGroupId(i) === t);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get current options
|
|
494
|
+
*/
|
|
495
|
+
getOptions() {
|
|
496
|
+
return { ...this.options };
|
|
497
|
+
}
|
|
498
|
+
destroy() {
|
|
499
|
+
this.worker && (this.worker.terminate(), this.worker = null), this.graph._destructor();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export {
|
|
503
|
+
x as ForceGraph
|
|
504
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
(function(f,y){typeof exports=="object"&&typeof module<"u"?y(exports,require("force-graph"),require("d3")):typeof define=="function"&&define.amd?define(["exports","force-graph","d3"],y):(f=typeof globalThis<"u"?globalThis:f||self,y(f.ForceGraphLib={},f.ForceGraph,f.d3))})(this,function(f,y,B){"use strict";function G(r){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(r){for(const i in r)if(i!=="default"){const e=Object.getOwnPropertyDescriptor(r,i);Object.defineProperty(t,i,e.get?e:{enumerable:!0,get:()=>r[i]})}}return t.default=r,Object.freeze(t)}const v=G(B);function w(r,t){(t==null||t>r.length)&&(t=r.length);for(var i=0,e=Array(t);i<t;i++)e[i]=r[i];return e}function A(r){if(Array.isArray(r))return r}function I(r){if(Array.isArray(r))return w(r)}function P(r){if(typeof Symbol<"u"&&r[Symbol.iterator]!=null||r["@@iterator"]!=null)return Array.from(r)}function O(r,t){var i=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(i!=null){var e,n,a,s,h=[],u=!0,o=!1;try{if(a=(i=i.call(r)).next,t!==0)for(;!(u=(e=a.call(i)).done)&&(h.push(e.value),h.length!==t);u=!0);}catch(l){o=!0,n=l}finally{try{if(!u&&i.return!=null&&(s=i.return(),Object(s)!==s))return}finally{if(o)throw n}}return h}}function z(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
|
|
2
|
+
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function N(){throw new TypeError(`Invalid attempt to spread non-iterable instance.
|
|
3
|
+
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function L(r,t){return A(r)||O(r,t)||T(r,t)||z()}function M(r){return I(r)||P(r)||T(r)||N()}function T(r,t){if(r){if(typeof r=="string")return w(r,t);var i={}.toString.call(r).slice(8,-1);return i==="Object"&&r.constructor&&(i=r.constructor.name),i==="Map"||i==="Set"?Array.from(r):i==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?w(r,t):void 0}}function D(){var r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(o){return o.cluster},t,i=[],e=k(1),n=k(.2),a=0,s=new Map;function h(o){var l=new Map(M(s.entries()).map(function(p){var d=L(p,2),m=d[0],b=d[1];return[m,E(b,e,t)]})),g=new Map(M(s.entries()).map(function(p){var d=L(p,2),m=d[0],b=d[1];return[m,n(m,b)]})),c=["x",t>1&&"y",t>2&&"z"].filter(function(p){return p}),C=c.map(function(p){return"v".concat(p)});i.forEach(function(p){var d=r(p);if(l.has(d)){var m=l.get(d),b=c.map(function(S){return m[S]-p[S]});if(!(W.apply(void 0,M(b))<=a)){var x=g.get(d)*o;C.forEach(function(S,F){return p[S]+=b[F]*x})}}})}function u(){s.clear(),i.forEach(function(o){var l=r(o);s.has(l)||s.set(l,[]),s.get(l).push(o)}),s.forEach(function(o,l){return o.length<=1&&s.delete(l)})}return h.initialize=function(o){i=o;for(var l=arguments.length,g=new Array(l>1?l-1:0),c=1;c<l;c++)g[c-1]=arguments[c];t=g.find(function(C){return[1,2,3].includes(C)})||2,u()},h.clusterId=function(o){return arguments.length?(r=o,u(),h):r},h.weight=function(o){return arguments.length?(e=typeof o=="function"?o:k(+o),h):e},h.strength=function(o){return arguments.length?(n=typeof o=="function"?o:k(+o),h):n},h.distanceMin=function(o){return arguments.length?(a=o,h):a},h}function W(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0;return Math.sqrt(r*r+t*t+i*i)}function k(r){return function(){return r}}function E(r,t,i){var e=0,n=0,a=0,s=0;r.forEach(function(u){var o=t(u);n+=u.x*o,i>1&&(a+=u.y*o),i>2&&(s+=u.z*o),e+=o});var h={};return h.x=n/e,i>1&&(h.y=a/e),i>2&&(h.z=s/e),h}class j{container;graph;data={nodes:[],links:[]};nodesMap=new Map;linkMap=new Map;options;worker=null;groupBounds=new Map;constructor(t,i={nodes:[],links:[]},e={}){this.container=t,this.data=i,this.graph=new y(this.container),i.nodes.forEach(s=>{this.nodesMap.set(s.id.toString(),s)});const n={labelThreshold:1.5,showGroups:!1,...e},a=n.showGroups?{groupBy:"topic",groupBorderColor:"#666",groupBorderWidth:2,groupBorderOpacity:.3,groupLabelColor:"#333",groupLabelSize:16,groupLabelThreshold:.8,groupPadding:20}:{};this.options={...n,...a},this.initGraph()}initGraph(){console.log(this.options.width,this.options.width),this.graph.width(this.options.width??800).height(this.options.height??400).onNodeClick(t=>{console.log("Node clicked:",t),this.options.nodeClickHandler&&this.options.nodeClickHandler(t),this.focusPosition({x:t.x,y:t.y})}).cooldownTime(this.calculateCooldownTime()).d3Force("cluster",this.options.cluster?D().clusterId(this.options.cluster):null).d3Force("collide",this.options.collide?v.forceCollide().radius(t=>this.options.collide?this.options.collide(t):t.marker.radius):null),this.options.keepDragPosition&&this.graph.onNodeDragEnd(t=>{t.fx=t.x,t.fy=t.y}),this.graphData(this.data),this.applyOptions(),this.render(),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:e}=this.nodesMap.get(t.id)??{x:0,y:0};i&&e&&(t={x:i,y:e})}t&&(this.graph.centerAt(t.x,t.y,1e3),this.graph.zoom(8,2e3))}}render(){this.graph.d3Force("charge",v.forceManyBody().strength(-100)).d3Force("link",v.forceLink().id(t=>t.index??"").distance(30)).d3Force("center",v.forceCenter())}applyOptions(){this.graph.nodeCanvasObject((t,i,e)=>{t===this.data.nodes[0]&&this.renderGroups(i,e);const n=this.getNodeSize(t)*2,a=typeof this.options.nodeBorderWidth=="function"?this.options.nodeBorderWidth(t):this.options.nodeBorderWidth;i.beginPath(),i.arc(t.x||0,t.y||0,n,0,2*Math.PI),i.fillStyle=this.getNodeColor(t),i.fill(),a&&a>0&&(i.beginPath(),i.arc(t.x||0,t.y||0,n,0,2*Math.PI),i.strokeStyle=this.getNodeBorderColor(t),i.lineWidth=a,i.stroke());const s=this.getNodeLabel(t);if(s&&e>=(this.options.labelThreshold||1.5)){const h=typeof this.options.nodeLabelColor=="function"?this.options.nodeLabelColor(t):this.options.nodeLabelColor??"#555";i.font=`${Math.max(n,8)}px Arial`,i.fillStyle=h,i.textAlign="center",i.textBaseline="middle",i.fillText(s,t.x||0,(t.y||0)+n+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):this.graph.zoom()<(this.options.labelThreshold||1.5)?"":t.label||t.id}updateData(t){const i=new Set(this.data.nodes.map(s=>s.id.toString())),e=t.nodes.filter(s=>!i.has(s.id.toString()));e.forEach(s=>{this.nodesMap.set(s.id.toString(),s)});const n=new Set(this.data.links.map(s=>this.createLinkKey(s.source,s.target))),a=t.links.filter(s=>!n.has(this.createLinkKey(s.source,s.target)));this.data={nodes:[...this.data.nodes,...e],links:[...this.data.links,...a]},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(e=>{const n=this.getNodeGroupId(e);n&&(i.has(n)||i.set(n,[]),i.get(n).push(e))}),i.forEach((e,n)=>{if(e.length===0)return;let a=1/0,s=1/0,h=-1/0,u=-1/0;e.forEach(o=>{if(o.x!==void 0&&o.y!==void 0){const l=this.getNodeSize(o);a=Math.min(a,o.x-l),s=Math.min(s,o.y-l),h=Math.max(h,o.x+l),u=Math.max(u,o.y+l)}}),a!==1/0&&this.groupBounds.set(n,{minX:a-t,minY:s-t,maxX:h+t,maxY:u+t,nodes:e})})}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((e,n)=>{const a=this.getGroupBorderColor(n),s=this.options.groupBorderWidth||2,h=this.options.groupBorderOpacity||.3;t.save(),t.globalAlpha=h,t.strokeStyle=a,t.lineWidth=s/i,t.setLineDash([10/i,5/i]),t.strokeRect(e.minX,e.minY,e.maxX-e.minX,e.maxY-e.minY),t.restore();const u=this.options.groupLabelThreshold||.8;if(i<=u){const o=this.getGroupLabelColor(n),l=(this.options.groupLabelSize||16)/i;t.save(),t.font=`bold ${l}px Arial`,t.fillStyle=o,t.textAlign="center",t.textBaseline="middle";const g=(e.minX+e.maxX)/2,c=e.minY-l/2,p=t.measureText(n).width,d=l;t.globalAlpha=.8,t.fillStyle="rgba(255, 255, 255, 0.9)",t.fillRect(g-p/2-4,c-d/2-2,p+8,d+4),t.globalAlpha=1,t.fillStyle=o,t.fillText(n,g,c),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())}getNodeCount(){return this.nodesMap.size}calculateCooldownTime(){const t=this.data.nodes.length,i=t*5/2,e=Math.max(2500,i);return console.log(`Dynamic Cooldown Time: ${t} nodes × 150ms = ${i}ms, final: ${e}ms`),e}getCooldownTime(){return this.calculateCooldownTime()}updateCooldownTime(){this.graph.cooldownTime(this.calculateCooldownTime())}getLinkCount(){return this.graph.graphData().links.length}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 e=this.nodesMap.get(t.toString());if(e){Object.assign(e,i);const n=this.data.nodes.findIndex(a=>a.id.toString()===t.toString());return n!==-1&&(this.data.nodes[n]=e),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(e=>e.id.toString()!==i),this.data.links=this.data.links.filter(e=>{const n=typeof e.source=="object"?e.source.id:e.source,a=typeof e.target=="object"?e.target.id:e.target;return n.toString()!==i&&a.toString()!==i}),this.graph.cooldownTime(this.calculateCooldownTime()),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 e=this.createLinkKey(i.source,i.target);this.linkMap.has(e)?console.log("link already exists",i):this.linkMap.set(e,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},console.log("this.data",this.data,t),this.graph.cooldownTime(this.calculateCooldownTime()),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 y(this.container),this.initGraph()}getData(){return this.data}getNodesData(){return this.data.nodes}getLinksData(){return this.data.links}createLinkKey(t,i){const e=typeof t=="object"?t.id:t,n=typeof i=="object"?i.id:i;return`${e}-${n}`}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 e=this.createLinkKey(i.source,i.target);this.linkMap.has(e)||this.linkMap.set(e,i)}),this.data={nodes:Array.from(this.nodesMap.values()),links:Array.from(this.linkMap.values())},this.graph.cooldownTime(this.calculateCooldownTime()),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(([e,n])=>{this.options[e]===void 0&&(this.options[e]=n)}),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 e=this.getNodeGroupId(i);e&&t.add(e)}),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()}}f.ForceGraph=j,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { default as default_2 } from 'force-graph';
|
|
2
|
+
import { GraphData as GraphData_2 } from 'force-graph';
|
|
3
|
+
import { LinkObject } from 'force-graph';
|
|
4
|
+
import { NodeObject } from 'force-graph';
|
|
5
|
+
|
|
6
|
+
export declare class ForceGraph {
|
|
7
|
+
private container;
|
|
8
|
+
private graph;
|
|
9
|
+
private data;
|
|
10
|
+
private nodesMap;
|
|
11
|
+
private linkMap;
|
|
12
|
+
private options;
|
|
13
|
+
private worker;
|
|
14
|
+
private groupBounds;
|
|
15
|
+
constructor(container: HTMLElement, initialData?: GraphData, options?: GraphOptions);
|
|
16
|
+
private initGraph;
|
|
17
|
+
renderer(): default_2<NodeData, LinkObject<NodeData>>;
|
|
18
|
+
focusPosition(nodeData?: {
|
|
19
|
+
id?: string;
|
|
20
|
+
x?: number;
|
|
21
|
+
y?: number;
|
|
22
|
+
}): void;
|
|
23
|
+
render(): void;
|
|
24
|
+
applyOptions(): void;
|
|
25
|
+
private getNodeSize;
|
|
26
|
+
private getNodeLabel;
|
|
27
|
+
updateData(data: GraphData): void;
|
|
28
|
+
private getNodeColor;
|
|
29
|
+
private getNodeBorderColor;
|
|
30
|
+
private applyLinkOptions;
|
|
31
|
+
private getLinkCurvature;
|
|
32
|
+
private getLinkProperty;
|
|
33
|
+
/**
|
|
34
|
+
* Calculate group boundaries based on node positions
|
|
35
|
+
*/
|
|
36
|
+
private calculateGroupBounds;
|
|
37
|
+
/**
|
|
38
|
+
* Get the group ID for a node
|
|
39
|
+
*/
|
|
40
|
+
private getNodeGroupId;
|
|
41
|
+
/**
|
|
42
|
+
* Render group borders and labels
|
|
43
|
+
*/
|
|
44
|
+
private renderGroups;
|
|
45
|
+
/**
|
|
46
|
+
* Get group border color
|
|
47
|
+
*/
|
|
48
|
+
private getGroupBorderColor;
|
|
49
|
+
/**
|
|
50
|
+
* Get group label color
|
|
51
|
+
*/
|
|
52
|
+
private getGroupLabelColor;
|
|
53
|
+
getNodeById(id: string | number): NodeData | undefined;
|
|
54
|
+
hasNode(id: string | number): boolean;
|
|
55
|
+
getNodeCount(): number;
|
|
56
|
+
/**
|
|
57
|
+
* Calculate dynamic cooldown time based on node count
|
|
58
|
+
* Minimum: 2500ms
|
|
59
|
+
* Normal: node.length * 150ms
|
|
60
|
+
*/
|
|
61
|
+
private calculateCooldownTime;
|
|
62
|
+
/**
|
|
63
|
+
* Get the current calculated cooldown time
|
|
64
|
+
*/
|
|
65
|
+
getCooldownTime(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Manually update the cooldown time based on current node count
|
|
68
|
+
*/
|
|
69
|
+
updateCooldownTime(): void;
|
|
70
|
+
getLinkCount(): number;
|
|
71
|
+
getDataSize(): {
|
|
72
|
+
nodes: number;
|
|
73
|
+
links: number;
|
|
74
|
+
};
|
|
75
|
+
getAllNodeIds(): string[];
|
|
76
|
+
updateNode(id: string | number, updates: Partial<NodeData>): boolean;
|
|
77
|
+
removeNode(id: string | number): boolean;
|
|
78
|
+
addData(newData: GraphData): Promise<void>;
|
|
79
|
+
setLabelThreshold(threshold: number): void;
|
|
80
|
+
setOptions(options: Partial<GraphOptions>): void;
|
|
81
|
+
/**
|
|
82
|
+
* Lightweight refresh - only updates graph data
|
|
83
|
+
*/
|
|
84
|
+
refreshGraph(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Complete reinitialization - use when major changes are needed
|
|
87
|
+
*/
|
|
88
|
+
reinitialize(): void;
|
|
89
|
+
reset(): void;
|
|
90
|
+
getData(): GraphData;
|
|
91
|
+
getNodesData(): GraphData['nodes'];
|
|
92
|
+
getLinksData(): GraphData['links'];
|
|
93
|
+
private createLinkKey;
|
|
94
|
+
/**
|
|
95
|
+
* Set graph data (chainable method)
|
|
96
|
+
* @param data - Graph data to set
|
|
97
|
+
*/
|
|
98
|
+
graphData(data: GraphData): ForceGraph;
|
|
99
|
+
/**
|
|
100
|
+
* Enable or disable group visualization
|
|
101
|
+
*/
|
|
102
|
+
showGroups(show: boolean): ForceGraph;
|
|
103
|
+
/**
|
|
104
|
+
* Set the property to group nodes by
|
|
105
|
+
*/
|
|
106
|
+
setGroupBy(groupBy: string | ((node: NodeData) => string | undefined)): ForceGraph;
|
|
107
|
+
/**
|
|
108
|
+
* Set group visualization options
|
|
109
|
+
*/
|
|
110
|
+
setGroupOptions(options: {
|
|
111
|
+
borderColor?: string | ((groupId: string) => string);
|
|
112
|
+
borderWidth?: number;
|
|
113
|
+
borderOpacity?: number;
|
|
114
|
+
labelColor?: string | ((groupId: string) => string);
|
|
115
|
+
labelSize?: number;
|
|
116
|
+
labelThreshold?: number;
|
|
117
|
+
padding?: number;
|
|
118
|
+
}): ForceGraph;
|
|
119
|
+
/**
|
|
120
|
+
* Get all available groups
|
|
121
|
+
*/
|
|
122
|
+
getGroups(): string[];
|
|
123
|
+
/**
|
|
124
|
+
* Get nodes in a specific group
|
|
125
|
+
*/
|
|
126
|
+
getNodesInGroup(groupId: string): NodeData[];
|
|
127
|
+
/**
|
|
128
|
+
* Get current options
|
|
129
|
+
*/
|
|
130
|
+
getOptions(): GraphOptions;
|
|
131
|
+
destroy(): void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export declare interface GraphData extends GraphData_2<NodeData, LinkData> {
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export declare interface GraphOptions {
|
|
138
|
+
height?: number;
|
|
139
|
+
width?: number;
|
|
140
|
+
labelThreshold?: number;
|
|
141
|
+
nodeSize?: number | ((node: NodeData) => number);
|
|
142
|
+
linkWidth?: number | ((link: LinkData) => number);
|
|
143
|
+
nodeLabel?: string | ((node: NodeData) => string);
|
|
144
|
+
nodeLabelColor?: string | ((node: NodeData) => string);
|
|
145
|
+
nodeColor?: string | ((node: NodeData) => string);
|
|
146
|
+
nodeBorderColor?: string | ((node: NodeData) => string);
|
|
147
|
+
nodeBorderWidth?: number | ((node: NodeData) => number);
|
|
148
|
+
linkLabel?: string | ((link: LinkData) => string);
|
|
149
|
+
nodeIcon?: string | ((node: NodeData) => string);
|
|
150
|
+
cluster?: (node: NodeData) => boolean | undefined | null;
|
|
151
|
+
collide?: (node: NodeData) => number;
|
|
152
|
+
loading?: boolean;
|
|
153
|
+
keepDragPosition?: boolean;
|
|
154
|
+
nodeClickHandler?: (node: NodeData) => void;
|
|
155
|
+
linkCurvature?: number | string | ((link: LinkData) => number);
|
|
156
|
+
linkDirectionalParticles?: number | ((link: LinkData) => number);
|
|
157
|
+
linkDirectionalParticleSpeed?: number | ((link: LinkData) => number);
|
|
158
|
+
linkDirectionalParticleWidth?: number | ((link: LinkData) => number);
|
|
159
|
+
linkDirectionalParticleColor?: string | ((link: LinkData) => string);
|
|
160
|
+
showGroups?: boolean;
|
|
161
|
+
groupBy?: string | ((node: NodeData) => string | undefined);
|
|
162
|
+
groupBorderColor?: string | ((groupId: string) => string);
|
|
163
|
+
groupBorderWidth?: number;
|
|
164
|
+
groupBorderOpacity?: number;
|
|
165
|
+
groupLabelColor?: string | ((groupId: string) => string);
|
|
166
|
+
groupLabelSize?: number;
|
|
167
|
+
groupLabelThreshold?: number;
|
|
168
|
+
groupPadding?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export declare interface LinkData extends LinkObject<NodeData> {
|
|
172
|
+
source: string | number | NodeData;
|
|
173
|
+
target: string | number | NodeData;
|
|
174
|
+
weight?: number;
|
|
175
|
+
color?: string;
|
|
176
|
+
curvature?: number;
|
|
177
|
+
[key: string]: any;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export declare interface NodeData extends NodeObject {
|
|
181
|
+
id: string | number;
|
|
182
|
+
x?: number;
|
|
183
|
+
y?: number;
|
|
184
|
+
fx?: number;
|
|
185
|
+
fy?: number;
|
|
186
|
+
color?: string;
|
|
187
|
+
value?: number;
|
|
188
|
+
label?: string;
|
|
189
|
+
platform?: string;
|
|
190
|
+
type?: string;
|
|
191
|
+
topic?: string;
|
|
192
|
+
[key: string]: any;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { }
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logixode/force-graph-lib",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/force-graph-lib.es.js",
|
|
8
|
+
"dist/force-graph-lib.umd.js",
|
|
9
|
+
"dist/index.d.ts"
|
|
10
|
+
],
|
|
11
|
+
"main": "./dist/force-graph-lib.umd.js",
|
|
12
|
+
"module": "./dist/force-graph-lib.es.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/force-graph-lib.es.js",
|
|
18
|
+
"require": "./dist/force-graph-lib.umd.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "run-p type-check \"build-only {@}\" --",
|
|
24
|
+
"build:lib": "VITE_BUILD_TARGET=lib vite build && npx tsc --project tsconfig.lib.json --noEmit",
|
|
25
|
+
"preview": "vite preview",
|
|
26
|
+
"test:unit": "vitest",
|
|
27
|
+
"test:e2e": "playwright test",
|
|
28
|
+
"build-only": "vite build",
|
|
29
|
+
"type-check": "vue-tsc --build",
|
|
30
|
+
"lint": "eslint . --fix",
|
|
31
|
+
"format": "prettier --write src/"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@formkit/auto-animate": "^0.8.2",
|
|
35
|
+
"@tailwindcss/vite": "^4.1.11",
|
|
36
|
+
"@tanstack/vue-query": "^5.83.1",
|
|
37
|
+
"@vueuse/core": "^13.6.0",
|
|
38
|
+
"axios": "^1.11.0",
|
|
39
|
+
"class-variance-authority": "^0.7.1",
|
|
40
|
+
"clsx": "^2.1.1",
|
|
41
|
+
"d3-force-clustering": "^1.0.0",
|
|
42
|
+
"lucide-vue-next": "^0.525.0",
|
|
43
|
+
"reka-ui": "^2.4.1",
|
|
44
|
+
"tailwind-merge": "^3.3.1",
|
|
45
|
+
"tailwindcss": "^4.1.11",
|
|
46
|
+
"tw-animate-css": "^1.3.5",
|
|
47
|
+
"vue-router": "^4.5.1",
|
|
48
|
+
"vue-sonner": "^2.0.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"d3": "^7.9.0",
|
|
52
|
+
"force-graph": "^1.50.1",
|
|
53
|
+
"vue": "^3.5.17"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@playwright/test": "^1.53.1",
|
|
57
|
+
"@tailwindcss/cli": "^4.1.11",
|
|
58
|
+
"@tsconfig/node22": "^22.0.2",
|
|
59
|
+
"@types/d3": "^7.4.3",
|
|
60
|
+
"@types/jsdom": "^21.1.7",
|
|
61
|
+
"@types/node": "^22.16.5",
|
|
62
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
63
|
+
"@vitest/eslint-plugin": "^1.2.7",
|
|
64
|
+
"@vue/eslint-config-prettier": "^10.2.0",
|
|
65
|
+
"@vue/eslint-config-typescript": "^14.5.1",
|
|
66
|
+
"@vue/test-utils": "^2.4.6",
|
|
67
|
+
"@vue/tsconfig": "^0.7.0",
|
|
68
|
+
"eslint": "^9.29.0",
|
|
69
|
+
"eslint-plugin-playwright": "^2.2.0",
|
|
70
|
+
"eslint-plugin-vue": "~10.2.0",
|
|
71
|
+
"jiti": "^2.4.2",
|
|
72
|
+
"jsdom": "^26.1.0",
|
|
73
|
+
"npm-run-all2": "^8.0.4",
|
|
74
|
+
"prettier": "3.5.3",
|
|
75
|
+
"typescript": "~5.8.0",
|
|
76
|
+
"unplugin-vue-router": "^0.14.0",
|
|
77
|
+
"vite": "^7.0.0",
|
|
78
|
+
"vite-plugin-dts": "^4.5.4",
|
|
79
|
+
"vite-plugin-vue-devtools": "^7.7.7",
|
|
80
|
+
"vitest": "^3.2.4",
|
|
81
|
+
"vue-tsc": "^2.2.10"
|
|
82
|
+
}
|
|
83
|
+
}
|