lightgraph 0.1.0__tar.gz
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.
- lightgraph-0.1.0/PKG-INFO +11 -0
- lightgraph-0.1.0/README.md +1 -0
- lightgraph-0.1.0/lightgraph/.ipynb_checkpoints/__init__-checkpoint.py +1 -0
- lightgraph-0.1.0/lightgraph/.ipynb_checkpoints/network-checkpoint.py +74 -0
- lightgraph-0.1.0/lightgraph/__init__.py +1 -0
- lightgraph-0.1.0/lightgraph/assets/.ipynb_checkpoints/script-checkpoint.js +573 -0
- lightgraph-0.1.0/lightgraph/assets/script.js +573 -0
- lightgraph-0.1.0/lightgraph/network.py +74 -0
- lightgraph-0.1.0/pyproject.toml +24 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: lightgraph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python binding for lightGraph network visualization
|
|
5
|
+
Author-email: Hao Zhu <haozhu233@gmail.com>, Donna Slonim <donna.slonim@tufts.edu>
|
|
6
|
+
Requires-Python: >=3.6
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: IPython
|
|
10
|
+
|
|
11
|
+
# lightgraph
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# lightgraph
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .network import net_vis
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
from IPython.display import display, HTML
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
def net_vis(adj_matrix, node_names, node_groups=None, remove_unconnected=True,
|
|
7
|
+
save_as=None):
|
|
8
|
+
"""
|
|
9
|
+
Visualizes a network using lightGraph in Jupyter.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
- adj_matrix (numpy.ndarray): The adjacency matrix of the network (n x n).
|
|
13
|
+
- node_names (list of str): Array of node names corresponding to rows/columns of the matrix.
|
|
14
|
+
- node_groups (dict, optional): A dictionary mapping node names to group identifiers. Defaults to None.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
- None. Displays the visualization below the cell.
|
|
18
|
+
"""
|
|
19
|
+
if not isinstance(adj_matrix, np.ndarray):
|
|
20
|
+
raise ValueError("adj_matrix must be a numpy.ndarray.")
|
|
21
|
+
if len(node_names) != adj_matrix.shape[0]:
|
|
22
|
+
raise ValueError("Length of node_names must match the dimensions of adj_matrix.")
|
|
23
|
+
if node_groups is not None and not isinstance(node_groups, dict):
|
|
24
|
+
raise ValueError("node_groups must be a dictionary.")
|
|
25
|
+
|
|
26
|
+
if remove_unconnected:
|
|
27
|
+
connected_nodes = (adj_matrix.sum(0) > 0) + (adj_matrix.sum(1) > 0)
|
|
28
|
+
adj_matrix = adj_matrix[connected_nodes, :][:, connected_nodes]
|
|
29
|
+
node_names = node_names[connected_nodes]
|
|
30
|
+
if node_groups is not None:
|
|
31
|
+
node_names_set = set(node_names)
|
|
32
|
+
node_groups_ = {}
|
|
33
|
+
for x in node_groups.keys():
|
|
34
|
+
if x in node_names_set:
|
|
35
|
+
node_groups_[x] = node_groups[x]
|
|
36
|
+
node_groups = node_groups_
|
|
37
|
+
|
|
38
|
+
nodes = []
|
|
39
|
+
for node in node_names:
|
|
40
|
+
node_data = {'id': str(node)}
|
|
41
|
+
if node_groups and node in node_groups:
|
|
42
|
+
node_data['group'] = str(node_groups[node])
|
|
43
|
+
nodes.append(node_data)
|
|
44
|
+
|
|
45
|
+
edges = []
|
|
46
|
+
for i in range(adj_matrix.shape[0]):
|
|
47
|
+
for j in range(adj_matrix.shape[1]):
|
|
48
|
+
if adj_matrix[i, j] > 0: # Include only non-zero edges
|
|
49
|
+
edges.append({
|
|
50
|
+
'source': str(node_names[i]),
|
|
51
|
+
'target': str(node_names[j]),
|
|
52
|
+
'weight': float(adj_matrix[i, j])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
nodes_json = json.dumps(nodes)
|
|
56
|
+
edges_json = json.dumps(edges)
|
|
57
|
+
|
|
58
|
+
script_path = os.path.join(os.path.dirname(__file__), "assets", "script.js")
|
|
59
|
+
with open(script_path, 'r') as f:
|
|
60
|
+
script_js = f.read()
|
|
61
|
+
|
|
62
|
+
html_content = f"""
|
|
63
|
+
<div id="lightGraph" style="width: 100%; height: 800px;"></div>
|
|
64
|
+
<script type="application/json" id="nodesData">{nodes_json}</script>
|
|
65
|
+
<script type="application/json" id="edgesData">{edges_json}</script>
|
|
66
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
67
|
+
<script>{script_js}</script>
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if save_as:
|
|
71
|
+
with open(save_as, 'w', encoding='utf-8') as f:
|
|
72
|
+
f.write(html_content)
|
|
73
|
+
|
|
74
|
+
display(HTML(html_content))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .network import net_vis
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
window.lightGraph = window.lightGraph || {};
|
|
3
|
+
|
|
4
|
+
window.lightGraph.initializeVisualization = () => {
|
|
5
|
+
// =====================================================================
|
|
6
|
+
// 1. Visual Element Section -------------------------------------------
|
|
7
|
+
// =====================================================================
|
|
8
|
+
|
|
9
|
+
// #region 1.1 Element constructors ------------------------------------
|
|
10
|
+
function createElement(tag, options = {}, styles = {}) {
|
|
11
|
+
const element = document.createElement(tag);
|
|
12
|
+
Object.assign(element, options);
|
|
13
|
+
Object.assign(element.style, styles);
|
|
14
|
+
return element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createContainer() {
|
|
18
|
+
return createElement('div', {}, {
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
right: '10px',
|
|
21
|
+
gap: '10px',
|
|
22
|
+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
23
|
+
padding: '5px',
|
|
24
|
+
borderRadius: '5px',
|
|
25
|
+
boxShadow: '0 0 5px rgba(0, 0, 0, 0.2)'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createButton({ id, title, htmlContent }) {
|
|
30
|
+
return createElement('button', { id, title, innerHTML: htmlContent }, {
|
|
31
|
+
padding: '5px 15px',
|
|
32
|
+
fontSize: '14px',
|
|
33
|
+
fontWeight: 'bold',
|
|
34
|
+
cursor: 'pointer'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createInput({ id, placeholder }) {
|
|
39
|
+
return createElement('input', { id, type: 'text', placeholder }, {
|
|
40
|
+
padding: '5px',
|
|
41
|
+
fontSize: '14px',
|
|
42
|
+
borderRadius: '3px',
|
|
43
|
+
border: '1px solid #ccc',
|
|
44
|
+
width: '120px'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTextBlock({ id, header, content }) {
|
|
49
|
+
const textBlockHeader = createElement('span', { innerHTML: header }, {
|
|
50
|
+
fontSize: '14px',
|
|
51
|
+
fontWeight: 'bold'
|
|
52
|
+
});
|
|
53
|
+
const textBlockContent = createElement('span', { id, innerHTML: content });
|
|
54
|
+
const textBlock = createElement('div');
|
|
55
|
+
textBlock.append(textBlockHeader, textBlockContent);
|
|
56
|
+
return [textBlock, textBlockContent];
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
|
|
60
|
+
// #region 1.2 Creating canvas -----------------------------------------
|
|
61
|
+
const lightGraph = document.getElementById("lightGraph");
|
|
62
|
+
Object.assign(lightGraph.style, {
|
|
63
|
+
height: '800px', position: 'relative'});
|
|
64
|
+
const canvas = createElement("canvas", {
|
|
65
|
+
id: "lightGraphCanvas",
|
|
66
|
+
width: lightGraph.clientWidth,
|
|
67
|
+
height: lightGraph.clientHeight });
|
|
68
|
+
const context = canvas.getContext("2d");
|
|
69
|
+
lightGraph.appendChild(canvas);
|
|
70
|
+
//#endregion
|
|
71
|
+
|
|
72
|
+
// #region 1.3 Additional visual elements ------------------------------
|
|
73
|
+
// 1.3.1 Control and search panel
|
|
74
|
+
const controlContainer = createContainer();
|
|
75
|
+
Object.assign(controlContainer.style, {display: 'flex', top: '10px'});
|
|
76
|
+
const toggleButton = createButton({
|
|
77
|
+
id: 'toggleButton',
|
|
78
|
+
title: 'Click to switch between selection and zoom modes',
|
|
79
|
+
htmlContent: '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>'
|
|
80
|
+
});
|
|
81
|
+
const arrowToggleButton = createButton({
|
|
82
|
+
id: 'arrowToggleButton',
|
|
83
|
+
title: 'Click to toggle arrows on edges',
|
|
84
|
+
htmlContent: '<span style="color:lightgray;">Arrows</span>'
|
|
85
|
+
});
|
|
86
|
+
const searchBox = createInput({
|
|
87
|
+
id: 'searchBox',
|
|
88
|
+
placeholder: 'Search node...'});
|
|
89
|
+
|
|
90
|
+
// 1.3.2 Cluster/selected node panel
|
|
91
|
+
const groupPanel = createContainer();
|
|
92
|
+
Object.assign(groupPanel.style, {
|
|
93
|
+
width: '240px', maxHeight: '200px',
|
|
94
|
+
overflowY: 'auto', top: '60px' });
|
|
95
|
+
const [existingGroupBlock, existingGroupBlockContent] = createTextBlock({
|
|
96
|
+
id: "existingGroups",
|
|
97
|
+
header: "Clusters: ",
|
|
98
|
+
content: "None"
|
|
99
|
+
});
|
|
100
|
+
const [selectedNodesBlock, selectedNodesBlockContent] = createTextBlock({
|
|
101
|
+
id: "selectedNodes",
|
|
102
|
+
header: "Selected: ",
|
|
103
|
+
content: "None",
|
|
104
|
+
});
|
|
105
|
+
const groupInputBox = createInput({
|
|
106
|
+
id: 'groupLabelInput',
|
|
107
|
+
placeholder: 'Enter label'
|
|
108
|
+
});
|
|
109
|
+
groupInputBox.style.width = '80px';
|
|
110
|
+
groupInputBox.disabled = true;
|
|
111
|
+
const groupButton = createButton({
|
|
112
|
+
id: 'groupLabelButton',
|
|
113
|
+
title: 'Click to assign group to selected nodes',
|
|
114
|
+
htmlContent: 'Add',
|
|
115
|
+
})
|
|
116
|
+
groupButton.disabled = true;
|
|
117
|
+
const clearGroupButton = createButton({
|
|
118
|
+
id: 'clearGroupLabelButton',
|
|
119
|
+
title: 'Click to clear labels on selected nodes',
|
|
120
|
+
htmlContent: 'Clear',
|
|
121
|
+
})
|
|
122
|
+
clearGroupButton.disabled = true;
|
|
123
|
+
//#endregion
|
|
124
|
+
|
|
125
|
+
// #region 1.4 Element assemble ----------------------------------------
|
|
126
|
+
lightGraph.append(controlContainer, groupPanel);
|
|
127
|
+
controlContainer.append(toggleButton, arrowToggleButton, searchBox);
|
|
128
|
+
groupPanel.append(
|
|
129
|
+
existingGroupBlock,
|
|
130
|
+
groupInputBox, groupButton, clearGroupButton,
|
|
131
|
+
selectedNodesBlock
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// #endregion
|
|
135
|
+
|
|
136
|
+
// =====================================================================
|
|
137
|
+
// 2. UI logics --------------------------------------------------------
|
|
138
|
+
// =====================================================================
|
|
139
|
+
|
|
140
|
+
// #region 2.1 global variables ----------------------------------------
|
|
141
|
+
let selectionMode = false;
|
|
142
|
+
let transform = d3.zoomIdentity;
|
|
143
|
+
let showArrows = false;
|
|
144
|
+
let nodes = [];
|
|
145
|
+
let edges = [];
|
|
146
|
+
let selectedNodes = new Set([]);
|
|
147
|
+
let selectionBox = null;
|
|
148
|
+
let draggingNode = null;
|
|
149
|
+
let dragOffsetX = 0;
|
|
150
|
+
let dragOffsetY = 0;
|
|
151
|
+
let simulation = d3.forceSimulation([]);
|
|
152
|
+
const groupColorScale = d3.scaleOrdinal(d3.schemeSet1);
|
|
153
|
+
let zoom = d3.zoom().scaleExtent([0.1, 5])
|
|
154
|
+
.on("zoom", (event) => {
|
|
155
|
+
if (!selectionMode) {
|
|
156
|
+
transform = event.transform;
|
|
157
|
+
ticked();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// #endregion
|
|
161
|
+
|
|
162
|
+
// #region 2.2 Interaction functions -----------------------------------
|
|
163
|
+
function clearSelection() {
|
|
164
|
+
selectedNodes.forEach(node => selectedNodes.delete(node));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addToSelection(nodes) {
|
|
168
|
+
nodes.forEach(node => selectedNodes.add(node));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function newSelection(nodes) {
|
|
172
|
+
clearSelection();
|
|
173
|
+
addToSelection(nodes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateGroupPanel() {
|
|
177
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
178
|
+
existingGroupBlockContent.innerHTML = groups.length ? '' : 'None';
|
|
179
|
+
groups.sort().forEach(group => {
|
|
180
|
+
const groupLabel = createElement(
|
|
181
|
+
'span', { innerHTML: `${group}, ` }, {
|
|
182
|
+
color: groupColorScale(group), cursor: 'pointer'
|
|
183
|
+
});
|
|
184
|
+
groupLabel.addEventListener('click', () => {
|
|
185
|
+
newSelection(nodes.filter(node => node.group === group));
|
|
186
|
+
printSelectedNodes();
|
|
187
|
+
ticked();
|
|
188
|
+
});
|
|
189
|
+
existingGroupBlockContent.appendChild(groupLabel);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ticked() {
|
|
194
|
+
context.save();
|
|
195
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
196
|
+
context.translate(transform.x, transform.y);
|
|
197
|
+
context.scale(transform.k, transform.k);
|
|
198
|
+
|
|
199
|
+
drawGroupEllipses();
|
|
200
|
+
edges.forEach(drawEdge);
|
|
201
|
+
nodes.forEach(drawLabel);
|
|
202
|
+
nodes.forEach(drawNode);
|
|
203
|
+
|
|
204
|
+
updateSelectionBox();
|
|
205
|
+
updateGroupPanel();
|
|
206
|
+
context.restore();
|
|
207
|
+
}
|
|
208
|
+
function updateSelectionBox() {
|
|
209
|
+
if (selectionBox) {
|
|
210
|
+
context.strokeStyle = "#55c667";
|
|
211
|
+
context.strokeRect(
|
|
212
|
+
selectionBox.x, selectionBox.y,
|
|
213
|
+
selectionBox.width, selectionBox.height
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printSelectedNodes() {
|
|
219
|
+
selectedNodeArray = Array.from(selectedNodes);
|
|
220
|
+
selectedNodesBlockContent.innerText = selectedNodeArray.length ? selectedNodeArray.map(node => node.id).sort().join(', ') : "None";
|
|
221
|
+
const enableControls = selectedNodeArray.length > 0;
|
|
222
|
+
[groupInputBox, groupButton, clearGroupButton].forEach(el => el.disabled = !enableControls);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function drawEdge(d) {
|
|
226
|
+
context.beginPath();
|
|
227
|
+
context.moveTo(d.source.x, d.source.y);
|
|
228
|
+
context.lineTo(d.target.x, d.target.y);
|
|
229
|
+
|
|
230
|
+
const includeEitherEnd = selectedNodes.has(d.source) || selectedNodes.has(d.target)
|
|
231
|
+
context.strokeStyle = includeEitherEnd ? "#99999911" : "#33333310";
|
|
232
|
+
context.lineWidth = includeEitherEnd ? 2 : 1;
|
|
233
|
+
|
|
234
|
+
context.stroke();
|
|
235
|
+
if (showArrows) drawArrow(d);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function drawArrow(d) {
|
|
239
|
+
const arrowLength = 10;
|
|
240
|
+
const arrowWidth = 5;
|
|
241
|
+
const dx = d.target.x - d.source.x;
|
|
242
|
+
const dy = d.target.y - d.source.y;
|
|
243
|
+
const angle = Math.atan2(dy, dx);
|
|
244
|
+
const arrowX = d.target.x - arrowLength * Math.cos(angle);
|
|
245
|
+
const arrowY = d.target.y - arrowLength * Math.sin(angle);
|
|
246
|
+
|
|
247
|
+
context.beginPath();
|
|
248
|
+
context.moveTo(arrowX, arrowY);
|
|
249
|
+
context.lineTo(arrowX - arrowWidth * Math.cos(angle - Math.PI / 6), arrowY - arrowWidth * Math.sin(angle - Math.PI / 6));
|
|
250
|
+
context.moveTo(arrowX, arrowY);
|
|
251
|
+
context.lineTo(arrowX - arrowWidth * Math.cos(angle + Math.PI / 6), arrowY - arrowWidth * Math.sin(angle + Math.PI / 6));
|
|
252
|
+
context.stroke();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function drawNode(d) {
|
|
256
|
+
const color = d.group ? groupColorScale(d.group) : (d.color || "#548ff0");
|
|
257
|
+
|
|
258
|
+
context.fillStyle = color;
|
|
259
|
+
context.strokeStyle = selectedNodes.has(d) ? "#000000" : "#FFFFFF";
|
|
260
|
+
context.lineWidth = selectedNodes.has(d) ? 1 : 1;
|
|
261
|
+
const size = d.size || 7;
|
|
262
|
+
nodeSize = selectedNodes.has(d) ? size + 5 : size;
|
|
263
|
+
|
|
264
|
+
context.beginPath();
|
|
265
|
+
context.arc(d.x, d.y, nodeSize / 2, 0, 2 * Math.PI);
|
|
266
|
+
context.fill();
|
|
267
|
+
context.stroke();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function drawLabel(d) {
|
|
271
|
+
const size = d.size || 5;
|
|
272
|
+
const labelFontSize = d.labelFontSize || 5;
|
|
273
|
+
context.font = `${labelFontSize}px sans-serif`;
|
|
274
|
+
context.fillStyle = selectedNodes.has(d) ? "#000" : "#555";
|
|
275
|
+
const textWidth = context.measureText(d.id).width;
|
|
276
|
+
context.fillText(d.id, d.x - textWidth - 4, d.y + size / 2 + 4);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function computeEigen(covMatrix) {
|
|
280
|
+
const a = covMatrix[0][0];
|
|
281
|
+
const b = covMatrix[0][1];
|
|
282
|
+
const d = covMatrix[1][1];
|
|
283
|
+
|
|
284
|
+
const trace = a + d;
|
|
285
|
+
const determinant = a * d - b * b;
|
|
286
|
+
const discriminant = Math.sqrt(trace * trace - 4 * determinant);
|
|
287
|
+
const eigenvalue1 = (trace + discriminant) / 2;
|
|
288
|
+
const eigenvalue2 = (trace - discriminant) / 2;
|
|
289
|
+
|
|
290
|
+
let eigenvector1, eigenvector2;
|
|
291
|
+
if (b !== 0) {
|
|
292
|
+
eigenvector1 = [eigenvalue1 - d, b];
|
|
293
|
+
eigenvector2 = [eigenvalue2 - d, b];
|
|
294
|
+
} else {
|
|
295
|
+
eigenvector1 = [1, 0];
|
|
296
|
+
eigenvector2 = [0, 1];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const normalize = (v) => {
|
|
300
|
+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
|
301
|
+
return [v[0] / length, v[1] / length];
|
|
302
|
+
};
|
|
303
|
+
eigenvector1 = normalize(eigenvector1);
|
|
304
|
+
eigenvector2 = normalize(eigenvector2);
|
|
305
|
+
|
|
306
|
+
return [eigenvalue1, eigenvalue2, eigenvector1, eigenvector2];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function drawGroupEllipses() {
|
|
310
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
311
|
+
|
|
312
|
+
groups.forEach(group => {
|
|
313
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
314
|
+
|
|
315
|
+
if (groupNodes.length > 1) {
|
|
316
|
+
// Calculate the centroid of the group
|
|
317
|
+
const centroid = {
|
|
318
|
+
x: d3.mean(groupNodes, d => d.x),
|
|
319
|
+
y: d3.mean(groupNodes, d => d.y)
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Calculate the covariance matrix
|
|
323
|
+
let sumXX = 0, sumXY = 0, sumYY = 0;
|
|
324
|
+
groupNodes.forEach(node => {
|
|
325
|
+
const dx = node.x - centroid.x;
|
|
326
|
+
const dy = node.y - centroid.y;
|
|
327
|
+
sumXX += dx * dx;
|
|
328
|
+
sumXY += dx * dy;
|
|
329
|
+
sumYY += dy * dy;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const covarianceMatrix = [
|
|
333
|
+
[sumXX / groupNodes.length, sumXY / groupNodes.length],
|
|
334
|
+
[sumXY / groupNodes.length, sumYY / groupNodes.length]
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const [lambda1, lambda2, v1, v2] = computeEigen(covarianceMatrix);
|
|
338
|
+
|
|
339
|
+
// Calculate rotation angle of the ellipse
|
|
340
|
+
const angle = Math.atan2(v1[1], v1[0]);
|
|
341
|
+
|
|
342
|
+
// Semi-axis lengths (scaled by a factor for better visual coverage)
|
|
343
|
+
const radiusX = Math.sqrt(lambda1) * 2;
|
|
344
|
+
const radiusY = Math.sqrt(lambda2) * 2;
|
|
345
|
+
|
|
346
|
+
// Draw the ellipse
|
|
347
|
+
context.save();
|
|
348
|
+
context.translate(centroid.x, centroid.y);
|
|
349
|
+
context.rotate(angle);
|
|
350
|
+
context.beginPath();
|
|
351
|
+
context.ellipse(0, 0, radiusX + 5, radiusY + 5, 0, 0, 2 * Math.PI); // Add padding for better visual coverage
|
|
352
|
+
context.fillStyle = `${groupColorScale(group)}20`; // Fill with group color, alpha = 0.2
|
|
353
|
+
context.fill();
|
|
354
|
+
context.strokeStyle = groupColorScale(group);
|
|
355
|
+
context.lineWidth = 2;
|
|
356
|
+
context.stroke();
|
|
357
|
+
context.restore();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isNodeInSelection(node, box) {
|
|
363
|
+
const x0 = Math.min(box.x, box.x + box.width),
|
|
364
|
+
x1 = Math.max(box.x, box.x + box.width),
|
|
365
|
+
y0 = Math.min(box.y, box.y + box.height),
|
|
366
|
+
y1 = Math.max(box.y, box.y + box.height);
|
|
367
|
+
|
|
368
|
+
return node.x >= x0 && node.x <= x1 && node.y >= y0 && node.y <= y1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getNodeAtCoordinates(x, y) {
|
|
372
|
+
return nodes.find(node => Math.sqrt((node.x - x) ** 2 + (node.y - y) ** 2) < (node.size || 15) / 2);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function reloadData() {
|
|
376
|
+
try {
|
|
377
|
+
const nodesData = document.getElementById('nodesData');
|
|
378
|
+
const edgesData = document.getElementById('edgesData');
|
|
379
|
+
|
|
380
|
+
if (!nodesData || !edgesData) {
|
|
381
|
+
console.error('nodesData or edgesData element not found');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
selectionMode = false;
|
|
386
|
+
transform = d3.zoomIdentity;
|
|
387
|
+
clearSelection();
|
|
388
|
+
selectionBox = null;
|
|
389
|
+
draggingNode = null;
|
|
390
|
+
dragOffsetX = 0;
|
|
391
|
+
dragOffsetY = 0;
|
|
392
|
+
|
|
393
|
+
nodes = JSON.parse(nodesData.textContent);
|
|
394
|
+
edges = JSON.parse(edgesData.textContent);
|
|
395
|
+
|
|
396
|
+
console.log('nodesData:', nodes);
|
|
397
|
+
console.log('edgesData:', edges);
|
|
398
|
+
|
|
399
|
+
toggleButton.innerHTML = '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>';
|
|
400
|
+
recalculateForce();
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error('Error reloading data:', error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function recalculateForce() {
|
|
406
|
+
try {
|
|
407
|
+
simulationForce = 4000 / nodes.length;
|
|
408
|
+
|
|
409
|
+
simulation = d3.forceSimulation(nodes)
|
|
410
|
+
.force("link", d3.forceLink(edges).id(d => d.id).distance(100))
|
|
411
|
+
.force("charge", d3.forceManyBody().strength(-simulationForce))
|
|
412
|
+
.force("center", d3.forceCenter(lightGraph.clientWidth / 2, lightGraph.clientHeight / 2))
|
|
413
|
+
.on("tick", ticked);
|
|
414
|
+
|
|
415
|
+
d3.select(canvas).call(zoom);
|
|
416
|
+
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Error updating visualization:', error);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// #endregion
|
|
422
|
+
|
|
423
|
+
// 2.3 Interactions ----------------------------------------------------
|
|
424
|
+
canvas.addEventListener("mousedown", (event) => {
|
|
425
|
+
console.log('Mouse down event triggered');
|
|
426
|
+
if (selectionMode) {
|
|
427
|
+
const [mouseX, mouseY] = d3.pointer(event);
|
|
428
|
+
const transformedMouseX = (mouseX - transform.x) / transform.k;
|
|
429
|
+
const transformedMouseY = (mouseY - transform.y) / transform.k;
|
|
430
|
+
const onNode = getNodeAtCoordinates(transformedMouseX, transformedMouseY);
|
|
431
|
+
|
|
432
|
+
if (onNode) {
|
|
433
|
+
if (event.shiftKey) {
|
|
434
|
+
// Shift-click to add or remove node from selected nodes
|
|
435
|
+
if (selectedNodes.has(onNode)) {
|
|
436
|
+
selectedNodes.delete(onNode);
|
|
437
|
+
} else {
|
|
438
|
+
selectedNodes.add(onNode);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
draggingNode = onNode;
|
|
442
|
+
dragOffsetX = onNode.x - transformedMouseX;
|
|
443
|
+
dragOffsetY = onNode.y - transformedMouseY;
|
|
444
|
+
if (!selectedNodes.has(onNode)) {
|
|
445
|
+
newSelection([onNode]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
} else {
|
|
450
|
+
if (!event.shiftKey) {
|
|
451
|
+
clearSelection();
|
|
452
|
+
}
|
|
453
|
+
selectionBox = { x: transformedMouseX, y: transformedMouseY, width: 0, height: 0 };
|
|
454
|
+
}
|
|
455
|
+
ticked();
|
|
456
|
+
printSelectedNodes();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
canvas.addEventListener("mousemove", (event) => {
|
|
461
|
+
if (selectionMode) {
|
|
462
|
+
const [mouseX, mouseY] = d3.pointer(event);
|
|
463
|
+
const transformedMouseX = (mouseX - transform.x) / transform.k;
|
|
464
|
+
const transformedMouseY = (mouseY - transform.y) / transform.k;
|
|
465
|
+
|
|
466
|
+
if (draggingNode) {
|
|
467
|
+
const dx = transformedMouseX + dragOffsetX - draggingNode.x;
|
|
468
|
+
const dy = transformedMouseY + dragOffsetY - draggingNode.y;
|
|
469
|
+
|
|
470
|
+
if (selectedNodes.size > 0 && selectedNodes.has(draggingNode)) {
|
|
471
|
+
selectedNodes.forEach(node => {
|
|
472
|
+
node.x += dx;
|
|
473
|
+
node.y += dy;
|
|
474
|
+
});
|
|
475
|
+
} else {
|
|
476
|
+
draggingNode.x = transformedMouseX + dragOffsetX;
|
|
477
|
+
draggingNode.y = transformedMouseY + dragOffsetY;
|
|
478
|
+
}
|
|
479
|
+
simulation.alpha(0.1).restart();
|
|
480
|
+
ticked();
|
|
481
|
+
} else if (selectionBox) {
|
|
482
|
+
selectionBox.width = transformedMouseX - selectionBox.x;
|
|
483
|
+
selectionBox.height = transformedMouseY - selectionBox.y;
|
|
484
|
+
ticked();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
canvas.addEventListener("mouseup", (event) => {
|
|
490
|
+
console.log('Mouse up event listener attached to canvas');
|
|
491
|
+
if (selectionMode) {
|
|
492
|
+
console.log('Mouse up event triggered');
|
|
493
|
+
if (draggingNode) {
|
|
494
|
+
console.log('Releasing draggingNode:', draggingNode);
|
|
495
|
+
draggingNode = null;
|
|
496
|
+
} else if (selectionBox) {
|
|
497
|
+
console.log('Final selection box:', selectionBox);
|
|
498
|
+
addToSelection(nodes.filter(node => isNodeInSelection(node, selectionBox)));
|
|
499
|
+
printSelectedNodes();
|
|
500
|
+
selectionBox = null;
|
|
501
|
+
}
|
|
502
|
+
ticked();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
toggleButton.addEventListener('click', () => {
|
|
507
|
+
selectionMode = !selectionMode;
|
|
508
|
+
toggleButton.innerHTML = selectionMode
|
|
509
|
+
? '<span style="font-weight:bold; color:black;">Select</span> / <span style="color:lightgray;">Zoom</span>'
|
|
510
|
+
: '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>';
|
|
511
|
+
if (selectionMode) {
|
|
512
|
+
d3.select(canvas).on("mousedown.zoom", null).on("mousemove.zoom", null).on("mouseup.zoom", null);
|
|
513
|
+
} else {
|
|
514
|
+
d3.select(canvas).call(zoom);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
arrowToggleButton.addEventListener('click', () => {
|
|
518
|
+
showArrows = !showArrows;
|
|
519
|
+
arrowToggleButton.innerHTML = showArrows ? '<span style="font-weight:bold; color:black;">Arrows</span>' : '<span style="color:lightgray;">Arrows</span>';
|
|
520
|
+
ticked();
|
|
521
|
+
});
|
|
522
|
+
searchBox.addEventListener('input', () => {
|
|
523
|
+
const searchTerm = searchBox.value.toLowerCase();
|
|
524
|
+
newSelection(nodes.filter(node => node.id.toLowerCase().includes(searchTerm)));
|
|
525
|
+
printSelectedNodes();
|
|
526
|
+
ticked();
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
groupButton.addEventListener('click', () => {
|
|
530
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
531
|
+
const groupLabel = groupInputBox.value || `Group ${groups.length+1}`;
|
|
532
|
+
if (groupLabel && selectedNodes.size > 0) {
|
|
533
|
+
selectedNodes.forEach(node => node.group = groupLabel);
|
|
534
|
+
updateGroupPanel();
|
|
535
|
+
ticked();
|
|
536
|
+
};
|
|
537
|
+
groupInputBox.value = "";
|
|
538
|
+
});
|
|
539
|
+
clearGroupButton.addEventListener('click', () => {
|
|
540
|
+
if (selectedNodes.size > 0) {
|
|
541
|
+
selectedNodes.forEach(node => delete node.group);
|
|
542
|
+
updateGroupPanel();
|
|
543
|
+
ticked();
|
|
544
|
+
}
|
|
545
|
+
groupInputBox.value = "";
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
reloadData();
|
|
549
|
+
|
|
550
|
+
window.addEventListener('resize', () => {
|
|
551
|
+
canvas.width = lightGraph.clientWidth;
|
|
552
|
+
canvas.height = lightGraph.clientHeight;
|
|
553
|
+
recalculateForce();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const observer = new MutationObserver((mutationsList, observer) => {
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
console.log('Mutation detected:', mutationsList);
|
|
559
|
+
reloadData();
|
|
560
|
+
}, 500);
|
|
561
|
+
});
|
|
562
|
+
observer.observe(
|
|
563
|
+
document.getElementById('networkData'),
|
|
564
|
+
{ childList: true, subtree: true, characterData: true });
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const checkCanvas = setInterval(() => {
|
|
568
|
+
if (document.getElementById("lightGraph")) {
|
|
569
|
+
clearInterval(checkCanvas);
|
|
570
|
+
window.lightGraph.initializeVisualization();
|
|
571
|
+
}
|
|
572
|
+
}, 100);
|
|
573
|
+
})();
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
window.lightGraph = window.lightGraph || {};
|
|
3
|
+
|
|
4
|
+
window.lightGraph.initializeVisualization = () => {
|
|
5
|
+
// =====================================================================
|
|
6
|
+
// 1. Visual Element Section -------------------------------------------
|
|
7
|
+
// =====================================================================
|
|
8
|
+
|
|
9
|
+
// #region 1.1 Element constructors ------------------------------------
|
|
10
|
+
function createElement(tag, options = {}, styles = {}) {
|
|
11
|
+
const element = document.createElement(tag);
|
|
12
|
+
Object.assign(element, options);
|
|
13
|
+
Object.assign(element.style, styles);
|
|
14
|
+
return element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createContainer() {
|
|
18
|
+
return createElement('div', {}, {
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
right: '10px',
|
|
21
|
+
gap: '10px',
|
|
22
|
+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
23
|
+
padding: '5px',
|
|
24
|
+
borderRadius: '5px',
|
|
25
|
+
boxShadow: '0 0 5px rgba(0, 0, 0, 0.2)'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createButton({ id, title, htmlContent }) {
|
|
30
|
+
return createElement('button', { id, title, innerHTML: htmlContent }, {
|
|
31
|
+
padding: '5px 15px',
|
|
32
|
+
fontSize: '14px',
|
|
33
|
+
fontWeight: 'bold',
|
|
34
|
+
cursor: 'pointer'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createInput({ id, placeholder }) {
|
|
39
|
+
return createElement('input', { id, type: 'text', placeholder }, {
|
|
40
|
+
padding: '5px',
|
|
41
|
+
fontSize: '14px',
|
|
42
|
+
borderRadius: '3px',
|
|
43
|
+
border: '1px solid #ccc',
|
|
44
|
+
width: '120px'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTextBlock({ id, header, content }) {
|
|
49
|
+
const textBlockHeader = createElement('span', { innerHTML: header }, {
|
|
50
|
+
fontSize: '14px',
|
|
51
|
+
fontWeight: 'bold'
|
|
52
|
+
});
|
|
53
|
+
const textBlockContent = createElement('span', { id, innerHTML: content });
|
|
54
|
+
const textBlock = createElement('div');
|
|
55
|
+
textBlock.append(textBlockHeader, textBlockContent);
|
|
56
|
+
return [textBlock, textBlockContent];
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
|
|
60
|
+
// #region 1.2 Creating canvas -----------------------------------------
|
|
61
|
+
const lightGraph = document.getElementById("lightGraph");
|
|
62
|
+
Object.assign(lightGraph.style, {
|
|
63
|
+
height: '800px', position: 'relative'});
|
|
64
|
+
const canvas = createElement("canvas", {
|
|
65
|
+
id: "lightGraphCanvas",
|
|
66
|
+
width: lightGraph.clientWidth,
|
|
67
|
+
height: lightGraph.clientHeight });
|
|
68
|
+
const context = canvas.getContext("2d");
|
|
69
|
+
lightGraph.appendChild(canvas);
|
|
70
|
+
//#endregion
|
|
71
|
+
|
|
72
|
+
// #region 1.3 Additional visual elements ------------------------------
|
|
73
|
+
// 1.3.1 Control and search panel
|
|
74
|
+
const controlContainer = createContainer();
|
|
75
|
+
Object.assign(controlContainer.style, {display: 'flex', top: '10px'});
|
|
76
|
+
const toggleButton = createButton({
|
|
77
|
+
id: 'toggleButton',
|
|
78
|
+
title: 'Click to switch between selection and zoom modes',
|
|
79
|
+
htmlContent: '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>'
|
|
80
|
+
});
|
|
81
|
+
const arrowToggleButton = createButton({
|
|
82
|
+
id: 'arrowToggleButton',
|
|
83
|
+
title: 'Click to toggle arrows on edges',
|
|
84
|
+
htmlContent: '<span style="color:lightgray;">Arrows</span>'
|
|
85
|
+
});
|
|
86
|
+
const searchBox = createInput({
|
|
87
|
+
id: 'searchBox',
|
|
88
|
+
placeholder: 'Search node...'});
|
|
89
|
+
|
|
90
|
+
// 1.3.2 Cluster/selected node panel
|
|
91
|
+
const groupPanel = createContainer();
|
|
92
|
+
Object.assign(groupPanel.style, {
|
|
93
|
+
width: '240px', maxHeight: '200px',
|
|
94
|
+
overflowY: 'auto', top: '60px' });
|
|
95
|
+
const [existingGroupBlock, existingGroupBlockContent] = createTextBlock({
|
|
96
|
+
id: "existingGroups",
|
|
97
|
+
header: "Clusters: ",
|
|
98
|
+
content: "None"
|
|
99
|
+
});
|
|
100
|
+
const [selectedNodesBlock, selectedNodesBlockContent] = createTextBlock({
|
|
101
|
+
id: "selectedNodes",
|
|
102
|
+
header: "Selected: ",
|
|
103
|
+
content: "None",
|
|
104
|
+
});
|
|
105
|
+
const groupInputBox = createInput({
|
|
106
|
+
id: 'groupLabelInput',
|
|
107
|
+
placeholder: 'Enter label'
|
|
108
|
+
});
|
|
109
|
+
groupInputBox.style.width = '80px';
|
|
110
|
+
groupInputBox.disabled = true;
|
|
111
|
+
const groupButton = createButton({
|
|
112
|
+
id: 'groupLabelButton',
|
|
113
|
+
title: 'Click to assign group to selected nodes',
|
|
114
|
+
htmlContent: 'Add',
|
|
115
|
+
})
|
|
116
|
+
groupButton.disabled = true;
|
|
117
|
+
const clearGroupButton = createButton({
|
|
118
|
+
id: 'clearGroupLabelButton',
|
|
119
|
+
title: 'Click to clear labels on selected nodes',
|
|
120
|
+
htmlContent: 'Clear',
|
|
121
|
+
})
|
|
122
|
+
clearGroupButton.disabled = true;
|
|
123
|
+
//#endregion
|
|
124
|
+
|
|
125
|
+
// #region 1.4 Element assemble ----------------------------------------
|
|
126
|
+
lightGraph.append(controlContainer, groupPanel);
|
|
127
|
+
controlContainer.append(toggleButton, arrowToggleButton, searchBox);
|
|
128
|
+
groupPanel.append(
|
|
129
|
+
existingGroupBlock,
|
|
130
|
+
groupInputBox, groupButton, clearGroupButton,
|
|
131
|
+
selectedNodesBlock
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// #endregion
|
|
135
|
+
|
|
136
|
+
// =====================================================================
|
|
137
|
+
// 2. UI logics --------------------------------------------------------
|
|
138
|
+
// =====================================================================
|
|
139
|
+
|
|
140
|
+
// #region 2.1 global variables ----------------------------------------
|
|
141
|
+
let selectionMode = false;
|
|
142
|
+
let transform = d3.zoomIdentity;
|
|
143
|
+
let showArrows = false;
|
|
144
|
+
let nodes = [];
|
|
145
|
+
let edges = [];
|
|
146
|
+
let selectedNodes = new Set([]);
|
|
147
|
+
let selectionBox = null;
|
|
148
|
+
let draggingNode = null;
|
|
149
|
+
let dragOffsetX = 0;
|
|
150
|
+
let dragOffsetY = 0;
|
|
151
|
+
let simulation = d3.forceSimulation([]);
|
|
152
|
+
const groupColorScale = d3.scaleOrdinal(d3.schemeSet1);
|
|
153
|
+
let zoom = d3.zoom().scaleExtent([0.1, 5])
|
|
154
|
+
.on("zoom", (event) => {
|
|
155
|
+
if (!selectionMode) {
|
|
156
|
+
transform = event.transform;
|
|
157
|
+
ticked();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// #endregion
|
|
161
|
+
|
|
162
|
+
// #region 2.2 Interaction functions -----------------------------------
|
|
163
|
+
function clearSelection() {
|
|
164
|
+
selectedNodes.forEach(node => selectedNodes.delete(node));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addToSelection(nodes) {
|
|
168
|
+
nodes.forEach(node => selectedNodes.add(node));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function newSelection(nodes) {
|
|
172
|
+
clearSelection();
|
|
173
|
+
addToSelection(nodes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateGroupPanel() {
|
|
177
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
178
|
+
existingGroupBlockContent.innerHTML = groups.length ? '' : 'None';
|
|
179
|
+
groups.sort().forEach(group => {
|
|
180
|
+
const groupLabel = createElement(
|
|
181
|
+
'span', { innerHTML: `${group}, ` }, {
|
|
182
|
+
color: groupColorScale(group), cursor: 'pointer'
|
|
183
|
+
});
|
|
184
|
+
groupLabel.addEventListener('click', () => {
|
|
185
|
+
newSelection(nodes.filter(node => node.group === group));
|
|
186
|
+
printSelectedNodes();
|
|
187
|
+
ticked();
|
|
188
|
+
});
|
|
189
|
+
existingGroupBlockContent.appendChild(groupLabel);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ticked() {
|
|
194
|
+
context.save();
|
|
195
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
196
|
+
context.translate(transform.x, transform.y);
|
|
197
|
+
context.scale(transform.k, transform.k);
|
|
198
|
+
|
|
199
|
+
drawGroupEllipses();
|
|
200
|
+
edges.forEach(drawEdge);
|
|
201
|
+
nodes.forEach(drawLabel);
|
|
202
|
+
nodes.forEach(drawNode);
|
|
203
|
+
|
|
204
|
+
updateSelectionBox();
|
|
205
|
+
updateGroupPanel();
|
|
206
|
+
context.restore();
|
|
207
|
+
}
|
|
208
|
+
function updateSelectionBox() {
|
|
209
|
+
if (selectionBox) {
|
|
210
|
+
context.strokeStyle = "#55c667";
|
|
211
|
+
context.strokeRect(
|
|
212
|
+
selectionBox.x, selectionBox.y,
|
|
213
|
+
selectionBox.width, selectionBox.height
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function printSelectedNodes() {
|
|
219
|
+
selectedNodeArray = Array.from(selectedNodes);
|
|
220
|
+
selectedNodesBlockContent.innerText = selectedNodeArray.length ? selectedNodeArray.map(node => node.id).sort().join(', ') : "None";
|
|
221
|
+
const enableControls = selectedNodeArray.length > 0;
|
|
222
|
+
[groupInputBox, groupButton, clearGroupButton].forEach(el => el.disabled = !enableControls);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function drawEdge(d) {
|
|
226
|
+
context.beginPath();
|
|
227
|
+
context.moveTo(d.source.x, d.source.y);
|
|
228
|
+
context.lineTo(d.target.x, d.target.y);
|
|
229
|
+
|
|
230
|
+
const includeEitherEnd = selectedNodes.has(d.source) || selectedNodes.has(d.target)
|
|
231
|
+
context.strokeStyle = includeEitherEnd ? "#99999911" : "#33333310";
|
|
232
|
+
context.lineWidth = includeEitherEnd ? 2 : 1;
|
|
233
|
+
|
|
234
|
+
context.stroke();
|
|
235
|
+
if (showArrows) drawArrow(d);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function drawArrow(d) {
|
|
239
|
+
const arrowLength = 10;
|
|
240
|
+
const arrowWidth = 5;
|
|
241
|
+
const dx = d.target.x - d.source.x;
|
|
242
|
+
const dy = d.target.y - d.source.y;
|
|
243
|
+
const angle = Math.atan2(dy, dx);
|
|
244
|
+
const arrowX = d.target.x - arrowLength * Math.cos(angle);
|
|
245
|
+
const arrowY = d.target.y - arrowLength * Math.sin(angle);
|
|
246
|
+
|
|
247
|
+
context.beginPath();
|
|
248
|
+
context.moveTo(arrowX, arrowY);
|
|
249
|
+
context.lineTo(arrowX - arrowWidth * Math.cos(angle - Math.PI / 6), arrowY - arrowWidth * Math.sin(angle - Math.PI / 6));
|
|
250
|
+
context.moveTo(arrowX, arrowY);
|
|
251
|
+
context.lineTo(arrowX - arrowWidth * Math.cos(angle + Math.PI / 6), arrowY - arrowWidth * Math.sin(angle + Math.PI / 6));
|
|
252
|
+
context.stroke();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function drawNode(d) {
|
|
256
|
+
const color = d.group ? groupColorScale(d.group) : (d.color || "#548ff0");
|
|
257
|
+
|
|
258
|
+
context.fillStyle = color;
|
|
259
|
+
context.strokeStyle = selectedNodes.has(d) ? "#000000" : "#FFFFFF";
|
|
260
|
+
context.lineWidth = selectedNodes.has(d) ? 1 : 1;
|
|
261
|
+
const size = d.size || 7;
|
|
262
|
+
nodeSize = selectedNodes.has(d) ? size + 5 : size;
|
|
263
|
+
|
|
264
|
+
context.beginPath();
|
|
265
|
+
context.arc(d.x, d.y, nodeSize / 2, 0, 2 * Math.PI);
|
|
266
|
+
context.fill();
|
|
267
|
+
context.stroke();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function drawLabel(d) {
|
|
271
|
+
const size = d.size || 5;
|
|
272
|
+
const labelFontSize = d.labelFontSize || 5;
|
|
273
|
+
context.font = `${labelFontSize}px sans-serif`;
|
|
274
|
+
context.fillStyle = selectedNodes.has(d) ? "#000" : "#555";
|
|
275
|
+
const textWidth = context.measureText(d.id).width;
|
|
276
|
+
context.fillText(d.id, d.x - textWidth - 4, d.y + size / 2 + 4);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function computeEigen(covMatrix) {
|
|
280
|
+
const a = covMatrix[0][0];
|
|
281
|
+
const b = covMatrix[0][1];
|
|
282
|
+
const d = covMatrix[1][1];
|
|
283
|
+
|
|
284
|
+
const trace = a + d;
|
|
285
|
+
const determinant = a * d - b * b;
|
|
286
|
+
const discriminant = Math.sqrt(trace * trace - 4 * determinant);
|
|
287
|
+
const eigenvalue1 = (trace + discriminant) / 2;
|
|
288
|
+
const eigenvalue2 = (trace - discriminant) / 2;
|
|
289
|
+
|
|
290
|
+
let eigenvector1, eigenvector2;
|
|
291
|
+
if (b !== 0) {
|
|
292
|
+
eigenvector1 = [eigenvalue1 - d, b];
|
|
293
|
+
eigenvector2 = [eigenvalue2 - d, b];
|
|
294
|
+
} else {
|
|
295
|
+
eigenvector1 = [1, 0];
|
|
296
|
+
eigenvector2 = [0, 1];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const normalize = (v) => {
|
|
300
|
+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
|
301
|
+
return [v[0] / length, v[1] / length];
|
|
302
|
+
};
|
|
303
|
+
eigenvector1 = normalize(eigenvector1);
|
|
304
|
+
eigenvector2 = normalize(eigenvector2);
|
|
305
|
+
|
|
306
|
+
return [eigenvalue1, eigenvalue2, eigenvector1, eigenvector2];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function drawGroupEllipses() {
|
|
310
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
311
|
+
|
|
312
|
+
groups.forEach(group => {
|
|
313
|
+
const groupNodes = nodes.filter(node => node.group === group);
|
|
314
|
+
|
|
315
|
+
if (groupNodes.length > 1) {
|
|
316
|
+
// Calculate the centroid of the group
|
|
317
|
+
const centroid = {
|
|
318
|
+
x: d3.mean(groupNodes, d => d.x),
|
|
319
|
+
y: d3.mean(groupNodes, d => d.y)
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Calculate the covariance matrix
|
|
323
|
+
let sumXX = 0, sumXY = 0, sumYY = 0;
|
|
324
|
+
groupNodes.forEach(node => {
|
|
325
|
+
const dx = node.x - centroid.x;
|
|
326
|
+
const dy = node.y - centroid.y;
|
|
327
|
+
sumXX += dx * dx;
|
|
328
|
+
sumXY += dx * dy;
|
|
329
|
+
sumYY += dy * dy;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const covarianceMatrix = [
|
|
333
|
+
[sumXX / groupNodes.length, sumXY / groupNodes.length],
|
|
334
|
+
[sumXY / groupNodes.length, sumYY / groupNodes.length]
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const [lambda1, lambda2, v1, v2] = computeEigen(covarianceMatrix);
|
|
338
|
+
|
|
339
|
+
// Calculate rotation angle of the ellipse
|
|
340
|
+
const angle = Math.atan2(v1[1], v1[0]);
|
|
341
|
+
|
|
342
|
+
// Semi-axis lengths (scaled by a factor for better visual coverage)
|
|
343
|
+
const radiusX = Math.sqrt(lambda1) * 2;
|
|
344
|
+
const radiusY = Math.sqrt(lambda2) * 2;
|
|
345
|
+
|
|
346
|
+
// Draw the ellipse
|
|
347
|
+
context.save();
|
|
348
|
+
context.translate(centroid.x, centroid.y);
|
|
349
|
+
context.rotate(angle);
|
|
350
|
+
context.beginPath();
|
|
351
|
+
context.ellipse(0, 0, radiusX + 5, radiusY + 5, 0, 0, 2 * Math.PI); // Add padding for better visual coverage
|
|
352
|
+
context.fillStyle = `${groupColorScale(group)}20`; // Fill with group color, alpha = 0.2
|
|
353
|
+
context.fill();
|
|
354
|
+
context.strokeStyle = groupColorScale(group);
|
|
355
|
+
context.lineWidth = 2;
|
|
356
|
+
context.stroke();
|
|
357
|
+
context.restore();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isNodeInSelection(node, box) {
|
|
363
|
+
const x0 = Math.min(box.x, box.x + box.width),
|
|
364
|
+
x1 = Math.max(box.x, box.x + box.width),
|
|
365
|
+
y0 = Math.min(box.y, box.y + box.height),
|
|
366
|
+
y1 = Math.max(box.y, box.y + box.height);
|
|
367
|
+
|
|
368
|
+
return node.x >= x0 && node.x <= x1 && node.y >= y0 && node.y <= y1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getNodeAtCoordinates(x, y) {
|
|
372
|
+
return nodes.find(node => Math.sqrt((node.x - x) ** 2 + (node.y - y) ** 2) < (node.size || 15) / 2);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function reloadData() {
|
|
376
|
+
try {
|
|
377
|
+
const nodesData = document.getElementById('nodesData');
|
|
378
|
+
const edgesData = document.getElementById('edgesData');
|
|
379
|
+
|
|
380
|
+
if (!nodesData || !edgesData) {
|
|
381
|
+
console.error('nodesData or edgesData element not found');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
selectionMode = false;
|
|
386
|
+
transform = d3.zoomIdentity;
|
|
387
|
+
clearSelection();
|
|
388
|
+
selectionBox = null;
|
|
389
|
+
draggingNode = null;
|
|
390
|
+
dragOffsetX = 0;
|
|
391
|
+
dragOffsetY = 0;
|
|
392
|
+
|
|
393
|
+
nodes = JSON.parse(nodesData.textContent);
|
|
394
|
+
edges = JSON.parse(edgesData.textContent);
|
|
395
|
+
|
|
396
|
+
console.log('nodesData:', nodes);
|
|
397
|
+
console.log('edgesData:', edges);
|
|
398
|
+
|
|
399
|
+
toggleButton.innerHTML = '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>';
|
|
400
|
+
recalculateForce();
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error('Error reloading data:', error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function recalculateForce() {
|
|
406
|
+
try {
|
|
407
|
+
simulationForce = 4000 / nodes.length;
|
|
408
|
+
|
|
409
|
+
simulation = d3.forceSimulation(nodes)
|
|
410
|
+
.force("link", d3.forceLink(edges).id(d => d.id).distance(100))
|
|
411
|
+
.force("charge", d3.forceManyBody().strength(-simulationForce))
|
|
412
|
+
.force("center", d3.forceCenter(lightGraph.clientWidth / 2, lightGraph.clientHeight / 2))
|
|
413
|
+
.on("tick", ticked);
|
|
414
|
+
|
|
415
|
+
d3.select(canvas).call(zoom);
|
|
416
|
+
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Error updating visualization:', error);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// #endregion
|
|
422
|
+
|
|
423
|
+
// 2.3 Interactions ----------------------------------------------------
|
|
424
|
+
canvas.addEventListener("mousedown", (event) => {
|
|
425
|
+
console.log('Mouse down event triggered');
|
|
426
|
+
if (selectionMode) {
|
|
427
|
+
const [mouseX, mouseY] = d3.pointer(event);
|
|
428
|
+
const transformedMouseX = (mouseX - transform.x) / transform.k;
|
|
429
|
+
const transformedMouseY = (mouseY - transform.y) / transform.k;
|
|
430
|
+
const onNode = getNodeAtCoordinates(transformedMouseX, transformedMouseY);
|
|
431
|
+
|
|
432
|
+
if (onNode) {
|
|
433
|
+
if (event.shiftKey) {
|
|
434
|
+
// Shift-click to add or remove node from selected nodes
|
|
435
|
+
if (selectedNodes.has(onNode)) {
|
|
436
|
+
selectedNodes.delete(onNode);
|
|
437
|
+
} else {
|
|
438
|
+
selectedNodes.add(onNode);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
draggingNode = onNode;
|
|
442
|
+
dragOffsetX = onNode.x - transformedMouseX;
|
|
443
|
+
dragOffsetY = onNode.y - transformedMouseY;
|
|
444
|
+
if (!selectedNodes.has(onNode)) {
|
|
445
|
+
newSelection([onNode]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
} else {
|
|
450
|
+
if (!event.shiftKey) {
|
|
451
|
+
clearSelection();
|
|
452
|
+
}
|
|
453
|
+
selectionBox = { x: transformedMouseX, y: transformedMouseY, width: 0, height: 0 };
|
|
454
|
+
}
|
|
455
|
+
ticked();
|
|
456
|
+
printSelectedNodes();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
canvas.addEventListener("mousemove", (event) => {
|
|
461
|
+
if (selectionMode) {
|
|
462
|
+
const [mouseX, mouseY] = d3.pointer(event);
|
|
463
|
+
const transformedMouseX = (mouseX - transform.x) / transform.k;
|
|
464
|
+
const transformedMouseY = (mouseY - transform.y) / transform.k;
|
|
465
|
+
|
|
466
|
+
if (draggingNode) {
|
|
467
|
+
const dx = transformedMouseX + dragOffsetX - draggingNode.x;
|
|
468
|
+
const dy = transformedMouseY + dragOffsetY - draggingNode.y;
|
|
469
|
+
|
|
470
|
+
if (selectedNodes.size > 0 && selectedNodes.has(draggingNode)) {
|
|
471
|
+
selectedNodes.forEach(node => {
|
|
472
|
+
node.x += dx;
|
|
473
|
+
node.y += dy;
|
|
474
|
+
});
|
|
475
|
+
} else {
|
|
476
|
+
draggingNode.x = transformedMouseX + dragOffsetX;
|
|
477
|
+
draggingNode.y = transformedMouseY + dragOffsetY;
|
|
478
|
+
}
|
|
479
|
+
simulation.alpha(0.1).restart();
|
|
480
|
+
ticked();
|
|
481
|
+
} else if (selectionBox) {
|
|
482
|
+
selectionBox.width = transformedMouseX - selectionBox.x;
|
|
483
|
+
selectionBox.height = transformedMouseY - selectionBox.y;
|
|
484
|
+
ticked();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
canvas.addEventListener("mouseup", (event) => {
|
|
490
|
+
console.log('Mouse up event listener attached to canvas');
|
|
491
|
+
if (selectionMode) {
|
|
492
|
+
console.log('Mouse up event triggered');
|
|
493
|
+
if (draggingNode) {
|
|
494
|
+
console.log('Releasing draggingNode:', draggingNode);
|
|
495
|
+
draggingNode = null;
|
|
496
|
+
} else if (selectionBox) {
|
|
497
|
+
console.log('Final selection box:', selectionBox);
|
|
498
|
+
addToSelection(nodes.filter(node => isNodeInSelection(node, selectionBox)));
|
|
499
|
+
printSelectedNodes();
|
|
500
|
+
selectionBox = null;
|
|
501
|
+
}
|
|
502
|
+
ticked();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
toggleButton.addEventListener('click', () => {
|
|
507
|
+
selectionMode = !selectionMode;
|
|
508
|
+
toggleButton.innerHTML = selectionMode
|
|
509
|
+
? '<span style="font-weight:bold; color:black;">Select</span> / <span style="color:lightgray;">Zoom</span>'
|
|
510
|
+
: '<span style="color:lightgray;">Select</span> / <span style="font-weight:bold; color:black;">Zoom</span>';
|
|
511
|
+
if (selectionMode) {
|
|
512
|
+
d3.select(canvas).on("mousedown.zoom", null).on("mousemove.zoom", null).on("mouseup.zoom", null);
|
|
513
|
+
} else {
|
|
514
|
+
d3.select(canvas).call(zoom);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
arrowToggleButton.addEventListener('click', () => {
|
|
518
|
+
showArrows = !showArrows;
|
|
519
|
+
arrowToggleButton.innerHTML = showArrows ? '<span style="font-weight:bold; color:black;">Arrows</span>' : '<span style="color:lightgray;">Arrows</span>';
|
|
520
|
+
ticked();
|
|
521
|
+
});
|
|
522
|
+
searchBox.addEventListener('input', () => {
|
|
523
|
+
const searchTerm = searchBox.value.toLowerCase();
|
|
524
|
+
newSelection(nodes.filter(node => node.id.toLowerCase().includes(searchTerm)));
|
|
525
|
+
printSelectedNodes();
|
|
526
|
+
ticked();
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
groupButton.addEventListener('click', () => {
|
|
530
|
+
const groups = [...new Set(nodes.map(node => node.group).filter(Boolean))];
|
|
531
|
+
const groupLabel = groupInputBox.value || `Group ${groups.length+1}`;
|
|
532
|
+
if (groupLabel && selectedNodes.size > 0) {
|
|
533
|
+
selectedNodes.forEach(node => node.group = groupLabel);
|
|
534
|
+
updateGroupPanel();
|
|
535
|
+
ticked();
|
|
536
|
+
};
|
|
537
|
+
groupInputBox.value = "";
|
|
538
|
+
});
|
|
539
|
+
clearGroupButton.addEventListener('click', () => {
|
|
540
|
+
if (selectedNodes.size > 0) {
|
|
541
|
+
selectedNodes.forEach(node => delete node.group);
|
|
542
|
+
updateGroupPanel();
|
|
543
|
+
ticked();
|
|
544
|
+
}
|
|
545
|
+
groupInputBox.value = "";
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
reloadData();
|
|
549
|
+
|
|
550
|
+
window.addEventListener('resize', () => {
|
|
551
|
+
canvas.width = lightGraph.clientWidth;
|
|
552
|
+
canvas.height = lightGraph.clientHeight;
|
|
553
|
+
recalculateForce();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const observer = new MutationObserver((mutationsList, observer) => {
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
console.log('Mutation detected:', mutationsList);
|
|
559
|
+
reloadData();
|
|
560
|
+
}, 500);
|
|
561
|
+
});
|
|
562
|
+
observer.observe(
|
|
563
|
+
document.getElementById('networkData'),
|
|
564
|
+
{ childList: true, subtree: true, characterData: true });
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const checkCanvas = setInterval(() => {
|
|
568
|
+
if (document.getElementById("lightGraph")) {
|
|
569
|
+
clearInterval(checkCanvas);
|
|
570
|
+
window.lightGraph.initializeVisualization();
|
|
571
|
+
}
|
|
572
|
+
}, 100);
|
|
573
|
+
})();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
from IPython.display import display, HTML
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
def net_vis(adj_matrix, node_names, node_groups=None, remove_unconnected=True,
|
|
7
|
+
save_as=None):
|
|
8
|
+
"""
|
|
9
|
+
Visualizes a network using lightGraph in Jupyter.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
- adj_matrix (numpy.ndarray): The adjacency matrix of the network (n x n).
|
|
13
|
+
- node_names (list of str): Array of node names corresponding to rows/columns of the matrix.
|
|
14
|
+
- node_groups (dict, optional): A dictionary mapping node names to group identifiers. Defaults to None.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
- None. Displays the visualization below the cell.
|
|
18
|
+
"""
|
|
19
|
+
if not isinstance(adj_matrix, np.ndarray):
|
|
20
|
+
raise ValueError("adj_matrix must be a numpy.ndarray.")
|
|
21
|
+
if len(node_names) != adj_matrix.shape[0]:
|
|
22
|
+
raise ValueError("Length of node_names must match the dimensions of adj_matrix.")
|
|
23
|
+
if node_groups is not None and not isinstance(node_groups, dict):
|
|
24
|
+
raise ValueError("node_groups must be a dictionary.")
|
|
25
|
+
|
|
26
|
+
if remove_unconnected:
|
|
27
|
+
connected_nodes = (adj_matrix.sum(0) > 0) + (adj_matrix.sum(1) > 0)
|
|
28
|
+
adj_matrix = adj_matrix[connected_nodes, :][:, connected_nodes]
|
|
29
|
+
node_names = node_names[connected_nodes]
|
|
30
|
+
if node_groups is not None:
|
|
31
|
+
node_names_set = set(node_names)
|
|
32
|
+
node_groups_ = {}
|
|
33
|
+
for x in node_groups.keys():
|
|
34
|
+
if x in node_names_set:
|
|
35
|
+
node_groups_[x] = node_groups[x]
|
|
36
|
+
node_groups = node_groups_
|
|
37
|
+
|
|
38
|
+
nodes = []
|
|
39
|
+
for node in node_names:
|
|
40
|
+
node_data = {'id': str(node)}
|
|
41
|
+
if node_groups and node in node_groups:
|
|
42
|
+
node_data['group'] = str(node_groups[node])
|
|
43
|
+
nodes.append(node_data)
|
|
44
|
+
|
|
45
|
+
edges = []
|
|
46
|
+
for i in range(adj_matrix.shape[0]):
|
|
47
|
+
for j in range(adj_matrix.shape[1]):
|
|
48
|
+
if adj_matrix[i, j] > 0: # Include only non-zero edges
|
|
49
|
+
edges.append({
|
|
50
|
+
'source': str(node_names[i]),
|
|
51
|
+
'target': str(node_names[j]),
|
|
52
|
+
'weight': float(adj_matrix[i, j])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
nodes_json = json.dumps(nodes)
|
|
56
|
+
edges_json = json.dumps(edges)
|
|
57
|
+
|
|
58
|
+
script_path = os.path.join(os.path.dirname(__file__), "assets", "script.js")
|
|
59
|
+
with open(script_path, 'r') as f:
|
|
60
|
+
script_js = f.read()
|
|
61
|
+
|
|
62
|
+
html_content = f"""
|
|
63
|
+
<div id="lightGraph" style="width: 100%; height: 800px;"></div>
|
|
64
|
+
<script type="application/json" id="nodesData">{nodes_json}</script>
|
|
65
|
+
<script type="application/json" id="edgesData">{edges_json}</script>
|
|
66
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
67
|
+
<script>{script_js}</script>
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if save_as:
|
|
71
|
+
with open(save_as, 'w', encoding='utf-8') as f:
|
|
72
|
+
f.write(html_content)
|
|
73
|
+
|
|
74
|
+
display(HTML(html_content))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lightgraph"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight Python binding for lightGraph network visualization"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Hao Zhu", email = "haozhu233@gmail.com"},
|
|
11
|
+
{name = "Donna Slonim", email = "donna.slonim@tufts.edu"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
homepage = "https://github.com/haozhu233/lightgraph"
|
|
16
|
+
requires-python = ">=3.6"
|
|
17
|
+
dependencies = ["numpy", "IPython"]
|
|
18
|
+
|
|
19
|
+
# [tool.flit.scripts]
|
|
20
|
+
# lightgraph = "lightgraph.visualize:visualize_network_lightGraph"
|
|
21
|
+
|
|
22
|
+
# [tool.flit.metadata]
|
|
23
|
+
# module = "lightgraph"
|
|
24
|
+
# include = ["lightgraph/assets/script.js"]
|