visnetmap 0.1.0__py3-none-any.whl
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.
- visnetmap-0.1.0.dist-info/METADATA +18 -0
- visnetmap-0.1.0.dist-info/RECORD +10 -0
- visnetmap-0.1.0.dist-info/WHEEL +5 -0
- visnetmap-0.1.0.dist-info/licenses/LICENSE +21 -0
- visnetmap-0.1.0.dist-info/top_level.txt +1 -0
- vistNETmap/__init__.py +0 -0
- vistNETmap/base_modes/__init__.py +1 -0
- vistNETmap/base_modes/netmap_base.py +660 -0
- vistNETmap/base_modes/network_visual_save_base.py +1014 -0
- vistNETmap/visNM.py +257 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import base64
|
|
3
|
+
import json, os
|
|
4
|
+
from IPython.display import HTML, display
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import webbrowser
|
|
7
|
+
import random
|
|
8
|
+
|
|
9
|
+
def base64_from_url(image_url):
|
|
10
|
+
"""
|
|
11
|
+
Downloads an image from a URL and returns its Base64 encoded string.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
image_url (str): The URL of the image.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
str: The Base64 encoded string of the image, prefixed with data URI,
|
|
18
|
+
or None if an error occurs.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
response = requests.get(image_url, stream=True)
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
|
|
24
|
+
content_type = response.headers['Content-Type']
|
|
25
|
+
if not content_type.startswith('image/'):
|
|
26
|
+
print(f"Warning: URL does not point to an image. Content-Type: {content_type}")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
image_bytes = response.content
|
|
30
|
+
encoded_string = base64.b64encode(image_bytes).decode('utf-8')
|
|
31
|
+
return f"data:{content_type};base64,{encoded_string}"
|
|
32
|
+
|
|
33
|
+
except requests.exceptions.RequestException as e:
|
|
34
|
+
print(f"Error downloading image from {image_url}: {e}")
|
|
35
|
+
return None
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"An unexpected error occurred: {e}")
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def base64_from_loc(image_local):
|
|
41
|
+
""" turn local image to base64 for vis.js diagram for an image node
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
image_local (_type_): local image address
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
with open(image_local, "rb") as image_file:
|
|
48
|
+
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
|
49
|
+
base64_image_local = f"data:image/png;base64,{encoded_string}"
|
|
50
|
+
return base64_image_local
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
print("Error: Image file not found. Using placeholder image.")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def visnet(nodes, edges, network_title='Atsaniik', description_df=None, description_title="Introduce your network", writeHTML="network_visualization.html", browserView=False, min_default_node_size=0, min_default_edge_width=0, maximum_display=100):
|
|
56
|
+
""" visualize your network with a save button to export selected nodes and edges to JSON files
|
|
57
|
+
Args:
|
|
58
|
+
nodes(list): list of nodes dict e.g. {"id": 1, "label": "Start", "size": 30, "color": "red-spain", "shape": "dot-america", "title": "starting the point"}
|
|
59
|
+
edges(list): list of edges dict e.g. {"from": 1, "to": 3, "width": 4, "color": {"color": "#C70039-europe"}, "arrows": "to", "title": "from 1 to 3"}
|
|
60
|
+
description_df(dataframe): a dataframe with columns "Name", "Description"
|
|
61
|
+
description_title(str): describe your network in short
|
|
62
|
+
writeHTML(str): output HTML file name
|
|
63
|
+
browserView(bool): whether to open the HTML in a browser
|
|
64
|
+
min_default_node_size(float): minimum node size for initial display
|
|
65
|
+
min_default_edge_width(float): minimum edge width for initial display
|
|
66
|
+
maximum_display(int): maximum number of nodes to display at any time
|
|
67
|
+
"""
|
|
68
|
+
defaults_node = {"label": lambda node: str(node['id']), "size": 1, "color": "gray", "shape": "dot", "title": lambda node: str(node['id'])}
|
|
69
|
+
|
|
70
|
+
nodeIDchecks = []
|
|
71
|
+
for node in nodes:
|
|
72
|
+
nodeIDcheck = node['id']
|
|
73
|
+
nodeIDchecks.append(nodeIDcheck)
|
|
74
|
+
for key, default_value in defaults_node.items():
|
|
75
|
+
if key not in node:
|
|
76
|
+
# If the key is lon or node_hover, use the lambda to compute the value
|
|
77
|
+
node[key] = default_value(node) if callable(default_value) else default_value
|
|
78
|
+
#print(nodes)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
defaults_edge = {"width": 1, "color": {"color": "gray"}, "title": lambda edge: str(edge['from'])+ ' - ' + str(edge['to'] )}
|
|
82
|
+
edgeIDchecks = []
|
|
83
|
+
for edge in edges:
|
|
84
|
+
edgeIDcheck = [edge['from'], edge['to']]
|
|
85
|
+
missingNodes = [x for x in edgeIDcheck if x not in nodeIDchecks]
|
|
86
|
+
if len(missingNodes)>0:
|
|
87
|
+
print(f'edge {edge['from']}-{edge['to']} missing nodes description: {missingNodes}')
|
|
88
|
+
edgeIDchecks.extend(edgeIDcheck)
|
|
89
|
+
for key, default_value in defaults_edge.items():
|
|
90
|
+
if key not in edge:
|
|
91
|
+
# If the key is lon or node_hover, use the lambda to compute the value
|
|
92
|
+
edge[key] = default_value(edge) if callable(default_value) else default_value
|
|
93
|
+
#print(edges)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_base_value(full_value, is_shape=False):
|
|
98
|
+
if "-" in full_value:
|
|
99
|
+
return full_value.split("-")[0]
|
|
100
|
+
return full_value
|
|
101
|
+
|
|
102
|
+
node_colors = [
|
|
103
|
+
node["color"]["background"] if isinstance(node["color"], dict) else node["color"]
|
|
104
|
+
for node in nodes if "color" in node
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
edge_colors = [edge["color"]["color"] for edge in edges if "color" in edge and "color" in edge["color"]]
|
|
109
|
+
|
|
110
|
+
color_bases = {}
|
|
111
|
+
for color in node_colors + edge_colors:
|
|
112
|
+
base = extract_base_value(color)
|
|
113
|
+
if base in color_bases:
|
|
114
|
+
color_bases[base].add(str(color))
|
|
115
|
+
else:
|
|
116
|
+
color_bases[base] = {str(color)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
for base, variants in color_bases.items():
|
|
120
|
+
if len(variants) > 1:
|
|
121
|
+
print(f"Error: Multiple extra labels detected for base color '{base}': {', '.join(variants)}. Execution stopped.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
shape_bases = {}
|
|
125
|
+
icon_shapes = {}
|
|
126
|
+
image_shapes = {}
|
|
127
|
+
for node in nodes:
|
|
128
|
+
if "shape" in node:
|
|
129
|
+
full_shape = node["shape"]
|
|
130
|
+
base = extract_base_value(full_shape, is_shape=True)
|
|
131
|
+
if base in shape_bases:
|
|
132
|
+
shape_bases[base].add(full_shape)
|
|
133
|
+
else:
|
|
134
|
+
shape_bases[base] = {full_shape}
|
|
135
|
+
# Track icon and image consistency
|
|
136
|
+
if base == "icon" and "icon" in node and "code" in node["icon"]:
|
|
137
|
+
shape_label = full_shape.split("-")[1] if "-" in full_shape else full_shape
|
|
138
|
+
if node["icon"]["code"] in icon_shapes:
|
|
139
|
+
if shape_label not in icon_shapes[node["icon"]["code"]]:
|
|
140
|
+
icon_shapes[node["icon"]["code"]].add(shape_label)
|
|
141
|
+
else:
|
|
142
|
+
continue
|
|
143
|
+
else:
|
|
144
|
+
icon_shapes[node["icon"]["code"]] = {shape_label}
|
|
145
|
+
elif base == "image" and "image" in node:
|
|
146
|
+
shape_label = full_shape.split("-")[1] if "-" in full_shape else full_shape
|
|
147
|
+
image_value = str(node["image"])
|
|
148
|
+
if image_value in image_shapes:
|
|
149
|
+
if shape_label not in image_shapes[image_value]:
|
|
150
|
+
image_shapes[image_value].add(shape_label)
|
|
151
|
+
else:
|
|
152
|
+
continue
|
|
153
|
+
else:
|
|
154
|
+
image_shapes[image_value] = {shape_label}
|
|
155
|
+
|
|
156
|
+
for code, shapes in icon_shapes.items():
|
|
157
|
+
if len(shapes) > 1:
|
|
158
|
+
print(f"Error: Inconsistent icon code '{code}' used with different shape names: {', '.join(shapes)}. Execution stopped.")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
for image_value, shapes in image_shapes.items():
|
|
162
|
+
if len(shapes) > 1:
|
|
163
|
+
print(f"Error: Inconsistent image name '{image_value}' used with different shape names: {', '.join(shapes)}. Execution stopped.")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
for base, variants in shape_bases.items():
|
|
167
|
+
if len(variants) > 1 and base not in ["icon", "image"]:
|
|
168
|
+
print(f"Error: Multiple extra labels detected for base shape '{base}': {', '.join(variants)}. Execution stopped.")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
if description_df is None:
|
|
172
|
+
df = pd.DataFrame({
|
|
173
|
+
"Name": ["Nodes amount", "Edges amount"],
|
|
174
|
+
"Description": [len(nodes), len(edges)]
|
|
175
|
+
})
|
|
176
|
+
else:
|
|
177
|
+
df = description_df
|
|
178
|
+
|
|
179
|
+
table_rows = "".join([f'<tr><td>{row["Name"]}</td><td>{row["Description"]}</td></tr>' for _, row in df.iterrows()])
|
|
180
|
+
|
|
181
|
+
all_nodes = sorted(nodes, key=lambda x: x["size"], reverse=True)
|
|
182
|
+
unique_sizes = len(set(node["size"] for node in all_nodes))
|
|
183
|
+
if unique_sizes == 1:
|
|
184
|
+
random.shuffle(all_nodes)
|
|
185
|
+
initial_nodes = all_nodes[:maximum_display]
|
|
186
|
+
initial_node_ids = set(node["id"] for node in initial_nodes)
|
|
187
|
+
|
|
188
|
+
initial_edges = [
|
|
189
|
+
edge for edge in edges
|
|
190
|
+
if edge["from"] in initial_node_ids and edge["to"] in initial_node_ids and
|
|
191
|
+
(min_default_edge_width is None or edge["width"] >= min_default_edge_width)
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
min_size = round(min(node["size"] for node in all_nodes), 2) if all_nodes else 0.00
|
|
195
|
+
max_size = round(max(node["size"] for node in all_nodes), 2) if all_nodes else 0.00
|
|
196
|
+
min_width = round(min(edge["width"] for edge in edges), 2) if edges else 0.00
|
|
197
|
+
max_width = round(max(edge["width"] for edge in edges), 2) if edges else 0.00
|
|
198
|
+
|
|
199
|
+
def extract_base_value(full_value, is_shape=False):
|
|
200
|
+
if isinstance(full_value, dict):
|
|
201
|
+
full_value = full_value.get('background', '') if is_shape else full_value.get('color', '') if not is_shape else ''
|
|
202
|
+
if "-" in full_value:
|
|
203
|
+
return full_value.split("-")[0]
|
|
204
|
+
return full_value
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
colors = sorted(set(
|
|
210
|
+
node["color"]["background"] if isinstance(node["color"], dict) else node["color"]
|
|
211
|
+
for node in all_nodes
|
|
212
|
+
))
|
|
213
|
+
shapes = sorted(set(node["shape"] for node in nodes))
|
|
214
|
+
labels = sorted(set(node["label"] for node in all_nodes))
|
|
215
|
+
titles = sorted(set(node.get("title", "") for node in all_nodes if "title" in node))
|
|
216
|
+
edge_colors = sorted(set(edge["color"]["color"] for edge in edges))
|
|
217
|
+
|
|
218
|
+
for node in all_nodes:
|
|
219
|
+
full_shape = node["shape"]
|
|
220
|
+
base_shape = extract_base_value(full_shape, is_shape=True)
|
|
221
|
+
node["shape_label"] = full_shape
|
|
222
|
+
node["shape"] = base_shape
|
|
223
|
+
if isinstance(node["color"], dict):
|
|
224
|
+
color_base = extract_base_value(node["color"].get('background', ''))
|
|
225
|
+
else:
|
|
226
|
+
color_base = extract_base_value(node["color"])
|
|
227
|
+
node["color"] = {"background": color_base, "border": node["color"].get('border', 'gray') if isinstance(node["color"], dict) else 'gray'}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
for edge in edges:
|
|
231
|
+
edge["color"]["color"] = extract_base_value(edge["color"]["color"])
|
|
232
|
+
|
|
233
|
+
color_options = "\n".join([f' <option value="{color}">{color}</option>' for color in colors])
|
|
234
|
+
shape_options = "\n".join([f' <option value="{shape}">{shape}</option>' for shape in shapes])
|
|
235
|
+
title_options = "\n".join([f' <option value="{title}">{title}</option>' for title in titles if title])
|
|
236
|
+
edge_color_options = "\n".join([f' <option value="{color}">{color}</option>' for color in edge_colors])
|
|
237
|
+
max_display_options = "\n".join([f' <option value="{val}"{" selected" if val == maximum_display else ""}>{val}</option>' for val in [1,5,10,20,30,50,60,70,80,90,100,108,150,200,300,400,500,1000,2000,5000,10000,20000]])
|
|
238
|
+
|
|
239
|
+
nodes_json = json.dumps(initial_nodes, ensure_ascii=False)
|
|
240
|
+
all_nodes_json = json.dumps(all_nodes, ensure_ascii=False)
|
|
241
|
+
all_edges_json = json.dumps(edges, ensure_ascii=False)
|
|
242
|
+
edges_json = json.dumps(initial_edges, ensure_ascii=False)
|
|
243
|
+
|
|
244
|
+
drag_script = """
|
|
245
|
+
<script type="text/javascript">
|
|
246
|
+
var isDragging = false;
|
|
247
|
+
var currentX;
|
|
248
|
+
var currentY;
|
|
249
|
+
var initialX;
|
|
250
|
+
var initialY;
|
|
251
|
+
var titleElement = document.getElementById('diagram-title');
|
|
252
|
+
|
|
253
|
+
function startDragging(e) {
|
|
254
|
+
initialX = e.clientX - currentX;
|
|
255
|
+
initialY = e.clientY - currentY;
|
|
256
|
+
isDragging = true;
|
|
257
|
+
titleElement.style.cursor = 'grabbing';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stopDragging() {
|
|
261
|
+
isDragging = false;
|
|
262
|
+
titleElement.style.cursor = 'move';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function dragTitle(e) {
|
|
266
|
+
if (isDragging) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
currentX = e.clientX - initialX;
|
|
269
|
+
currentY = e.clientY - initialY;
|
|
270
|
+
titleElement.style.left = currentX + 'px';
|
|
271
|
+
titleElement.style.top = currentY + 'px';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (titleElement) {
|
|
276
|
+
currentX = parseInt(window.getComputedStyle(titleElement).left) || 50;
|
|
277
|
+
currentY = parseInt(window.getComputedStyle(titleElement).top) || 10;
|
|
278
|
+
titleElement.addEventListener('mousedown', startDragging);
|
|
279
|
+
document.addEventListener('mousemove', dragTitle);
|
|
280
|
+
document.addEventListener('mouseup', stopDragging);
|
|
281
|
+
titleElement.style.userSelect = 'none';
|
|
282
|
+
}
|
|
283
|
+
</script>
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
script_code = """
|
|
287
|
+
<script type="text/javascript">
|
|
288
|
+
var allNodes = {all_nodes_json};
|
|
289
|
+
var allEdges = {all_edges_json};
|
|
290
|
+
var originalNodes = {nodes_json};
|
|
291
|
+
var originalEdges = {edges_json};
|
|
292
|
+
var nodes = new vis.DataSet(originalNodes);
|
|
293
|
+
var edges = new vis.DataSet(originalEdges);
|
|
294
|
+
var lastZoomCenter = {{ x: 0, y: 0 }};
|
|
295
|
+
var lastScale = 1;
|
|
296
|
+
var uniqueLabels = [...new Set(allNodes.map(node => node.label).filter(label => label))].sort();
|
|
297
|
+
var previousNodes = originalNodes;
|
|
298
|
+
var previousEdges = originalEdges;
|
|
299
|
+
|
|
300
|
+
var container = document.getElementById('mynetwork');
|
|
301
|
+
if (!container) {{
|
|
302
|
+
console.error("Container #mynetwork not found!");
|
|
303
|
+
}}
|
|
304
|
+
var loadingBar = document.getElementById('loading-bar');
|
|
305
|
+
var loadingProgress = document.getElementById('loading-progress');
|
|
306
|
+
var loadingPercentage = document.getElementById('loading-percentage');
|
|
307
|
+
|
|
308
|
+
var data = {{
|
|
309
|
+
nodes: nodes,
|
|
310
|
+
edges: edges
|
|
311
|
+
}};
|
|
312
|
+
var options = {{
|
|
313
|
+
nodes: {{
|
|
314
|
+
shape: 'dot',
|
|
315
|
+
size: 20,
|
|
316
|
+
font: {{ size: 14, color: '#333333' }},
|
|
317
|
+
borderWidth: 2,
|
|
318
|
+
color: {{
|
|
319
|
+
background: '#97C2E6',
|
|
320
|
+
border: '#2B7CE9',
|
|
321
|
+
highlight: {{ background: '#D2E5FF', border: '#2B7CE9' }},
|
|
322
|
+
hover: {{ background: '#D2E5FF', border: '#2B7CE9' }}
|
|
323
|
+
}}
|
|
324
|
+
}},
|
|
325
|
+
edges: {{
|
|
326
|
+
width: 1,
|
|
327
|
+
color: {{ color: '#848484', highlight: '#848484', hover: '#848484', inherit: 'from', opacity: 0.8 }},
|
|
328
|
+
smooth: {{ enabled: true, type: "dynamic" }},
|
|
329
|
+
font: {{ size: 12 }}
|
|
330
|
+
}},
|
|
331
|
+
physics: {{
|
|
332
|
+
enabled: true,
|
|
333
|
+
stabilization: {{
|
|
334
|
+
enabled: true,
|
|
335
|
+
iterations: 1000,
|
|
336
|
+
updateInterval: 50
|
|
337
|
+
}},
|
|
338
|
+
barnesHut: {{
|
|
339
|
+
gravitationalConstant: -2000,
|
|
340
|
+
centralGravity: 0.3,
|
|
341
|
+
springLength: 95,
|
|
342
|
+
springConstant: 0.04,
|
|
343
|
+
damping: 0.09,
|
|
344
|
+
avoidOverlap: 0
|
|
345
|
+
}},
|
|
346
|
+
solver: 'barnesHut'
|
|
347
|
+
}},
|
|
348
|
+
configure: {{
|
|
349
|
+
enabled: true,
|
|
350
|
+
filter: 'physics',
|
|
351
|
+
showButton: true,
|
|
352
|
+
container: document.getElementById('physics-config')
|
|
353
|
+
}}
|
|
354
|
+
}};
|
|
355
|
+
|
|
356
|
+
container.style.opacity = '0';
|
|
357
|
+
|
|
358
|
+
var network = new vis.Network(container, data, options);
|
|
359
|
+
if (!network) {{
|
|
360
|
+
console.error("Failed to initialize vis.Network!");
|
|
361
|
+
}}
|
|
362
|
+
|
|
363
|
+
network.on("stabilizationProgress", function(params) {{
|
|
364
|
+
var progress = (params.iterations / params.total) * 100;
|
|
365
|
+
if (loadingProgress && loadingPercentage) {{
|
|
366
|
+
loadingProgress.style.width = progress + '%';
|
|
367
|
+
loadingPercentage.textContent = Math.round(progress) + '%';
|
|
368
|
+
}}
|
|
369
|
+
}});
|
|
370
|
+
|
|
371
|
+
network.on("stabilizationIterationsDone", function() {{
|
|
372
|
+
if (loadingBar) {{
|
|
373
|
+
loadingBar.style.display = 'none';
|
|
374
|
+
}}
|
|
375
|
+
container.style.opacity = '1';
|
|
376
|
+
network.setOptions({{ physics: false }});
|
|
377
|
+
updateNodes();
|
|
378
|
+
}});
|
|
379
|
+
|
|
380
|
+
function getSelectedOptions(selectElement) {{
|
|
381
|
+
return Array.from(selectElement.selectedOptions).map(option => option.value);
|
|
382
|
+
}}
|
|
383
|
+
|
|
384
|
+
function updateSuggestions() {{
|
|
385
|
+
var input = document.getElementById('label-search').value;
|
|
386
|
+
var suggestionsDiv = document.getElementById('label-suggestions');
|
|
387
|
+
suggestionsDiv.innerHTML = '';
|
|
388
|
+
suggestionsDiv.style.display = 'none';
|
|
389
|
+
|
|
390
|
+
if (input.trim() !== '') {{
|
|
391
|
+
var terms = input.split(',').map(term => term.trim().toLowerCase());
|
|
392
|
+
var lastTerm = terms[terms.length - 1];
|
|
393
|
+
if (lastTerm) {{
|
|
394
|
+
var matches = uniqueLabels.filter(label =>
|
|
395
|
+
label.toLowerCase().includes(lastTerm)
|
|
396
|
+
);
|
|
397
|
+
if (matches.length > 0) {{
|
|
398
|
+
suggestionsDiv.style.display = 'block';
|
|
399
|
+
matches.forEach(label => {{
|
|
400
|
+
var div = document.createElement('div');
|
|
401
|
+
div.className = 'suggestion-item';
|
|
402
|
+
div.textContent = label;
|
|
403
|
+
div.onclick = function() {{
|
|
404
|
+
var currentTerms = input.split(',').slice(0, -1);
|
|
405
|
+
currentTerms.push(label);
|
|
406
|
+
document.getElementById('label-search').value = currentTerms.join(',') + (currentTerms.length > 0 ? ',' : '');
|
|
407
|
+
suggestionsDiv.innerHTML = '';
|
|
408
|
+
suggestionsDiv.style.display = 'none';
|
|
409
|
+
updateNodes();
|
|
410
|
+
}};
|
|
411
|
+
suggestionsDiv.appendChild(div);
|
|
412
|
+
}});
|
|
413
|
+
}}
|
|
414
|
+
}}
|
|
415
|
+
}} else {{
|
|
416
|
+
updateNodes();
|
|
417
|
+
}}
|
|
418
|
+
}}
|
|
419
|
+
|
|
420
|
+
function saveSelection() {{
|
|
421
|
+
var currentNodes = nodes.get();
|
|
422
|
+
var currentEdges = edges.get();
|
|
423
|
+
|
|
424
|
+
// Create JSON strings
|
|
425
|
+
var nodesJson = JSON.stringify(currentNodes, null, 2);
|
|
426
|
+
var edgesJson = JSON.stringify(currentEdges, null, 2);
|
|
427
|
+
|
|
428
|
+
// Create Blob objects for download
|
|
429
|
+
var nodesBlob = new Blob([nodesJson], {{ type: 'application/json' }});
|
|
430
|
+
var edgesBlob = new Blob([edgesJson], {{ type: 'application/json' }});
|
|
431
|
+
|
|
432
|
+
// Create temporary URLs for the Blobs
|
|
433
|
+
var nodesUrl = URL.createObjectURL(nodesBlob);
|
|
434
|
+
var edgesUrl = URL.createObjectURL(edgesBlob);
|
|
435
|
+
|
|
436
|
+
// Create download links
|
|
437
|
+
var nodesLink = document.createElement('a');
|
|
438
|
+
nodesLink.href = nodesUrl;
|
|
439
|
+
nodesLink.download = 'nodes.json';
|
|
440
|
+
document.body.appendChild(nodesLink);
|
|
441
|
+
nodesLink.click();
|
|
442
|
+
document.body.removeChild(nodesLink);
|
|
443
|
+
|
|
444
|
+
var edgesLink = document.createElement('a');
|
|
445
|
+
edgesLink.href = edgesUrl;
|
|
446
|
+
edgesLink.download = 'edges.json';
|
|
447
|
+
document.body.appendChild(edgesLink);
|
|
448
|
+
edgesLink.click();
|
|
449
|
+
document.body.removeChild(edgesLink);
|
|
450
|
+
|
|
451
|
+
// Clean up URLs
|
|
452
|
+
URL.revokeObjectURL(nodesUrl);
|
|
453
|
+
URL.revokeObjectURL(edgesUrl);
|
|
454
|
+
|
|
455
|
+
// Log saved locations (simulating file paths for console output)
|
|
456
|
+
console.log("Nodes saved to: nodes.json");
|
|
457
|
+
console.log("Edges saved to: edges.json");
|
|
458
|
+
}}
|
|
459
|
+
|
|
460
|
+
function updateNodes(zoomCenter = null, zoomDirection = null) {{
|
|
461
|
+
console.log("Updating nodes...");
|
|
462
|
+
var maxDisplay = parseInt(document.getElementById('max-display-select').value) || {maximum_display};
|
|
463
|
+
var colorSelect = getSelectedOptions(document.getElementById('color-select'));
|
|
464
|
+
var shapeSelect = getSelectedOptions(document.getElementById('shape-select'));
|
|
465
|
+
var sizeMin = parseFloat(document.getElementById('size-min').value) || 0;
|
|
466
|
+
var sizeMax = parseFloat(document.getElementById('size-max').value) || Infinity;
|
|
467
|
+
var labelSearch = document.getElementById('label-search').value;
|
|
468
|
+
var titleSelect = getSelectedOptions(document.getElementById('title-select'));
|
|
469
|
+
var widthMin = document.getElementById('width-min').value ? parseFloat(document.getElementById('width-min').value) : null;
|
|
470
|
+
var widthMax = document.getElementById('width-max').value ? parseFloat(document.getElementById('width-max').value) : null;
|
|
471
|
+
var edgeColorSelect = getSelectedOptions(document.getElementById('edge-color-select'));
|
|
472
|
+
|
|
473
|
+
var filteredNodes = allNodes;
|
|
474
|
+
var filteredEdges = allEdges;
|
|
475
|
+
|
|
476
|
+
if (labelSearch.trim() !== '') {{
|
|
477
|
+
previousNodes = nodes.get();
|
|
478
|
+
previousEdges = edges.get();
|
|
479
|
+
console.log("Saving previous state: ", previousNodes.length, "nodes,", previousEdges.length, "edges");
|
|
480
|
+
|
|
481
|
+
var searchTerms = labelSearch.split(',').map(term => term.trim().toLowerCase()).filter(term => term !== '');
|
|
482
|
+
console.log("Search terms:", searchTerms);
|
|
483
|
+
var selectedNodes = allNodes.filter(function(node) {{
|
|
484
|
+
return node.label && searchTerms.some(term => node.label.toLowerCase() === term);
|
|
485
|
+
}});
|
|
486
|
+
console.log("Selected nodes:", selectedNodes.map(node => node.label));
|
|
487
|
+
var neighborIds = new Set();
|
|
488
|
+
selectedNodes.forEach(function(node) {{
|
|
489
|
+
neighborIds.add(node.id);
|
|
490
|
+
allEdges.forEach(function(edge) {{
|
|
491
|
+
if (edge.from === node.id) {{
|
|
492
|
+
neighborIds.add(edge.to);
|
|
493
|
+
}}
|
|
494
|
+
if (edge.to === node.id) {{
|
|
495
|
+
neighborIds.add(edge.from);
|
|
496
|
+
}}
|
|
497
|
+
}});
|
|
498
|
+
}});
|
|
499
|
+
console.log("Neighbor IDs:", Array.from(neighborIds));
|
|
500
|
+
filteredNodes = allNodes.filter(function(node) {{
|
|
501
|
+
return neighborIds.has(node.id);
|
|
502
|
+
}});
|
|
503
|
+
console.log("Filtered nodes:", filteredNodes.map(node => node.label));
|
|
504
|
+
}}
|
|
505
|
+
|
|
506
|
+
var validNodeIds = new Set();
|
|
507
|
+
if (widthMin !== null || widthMax !== null) {{
|
|
508
|
+
filteredEdges = allEdges.filter(function(edge) {{
|
|
509
|
+
var widthMatch = true;
|
|
510
|
+
if (widthMin !== null) {{
|
|
511
|
+
widthMatch = widthMatch && edge.width >= widthMin;
|
|
512
|
+
}}
|
|
513
|
+
if (widthMax !== null) {{
|
|
514
|
+
widthMatch = widthMatch && edge.width <= widthMax;
|
|
515
|
+
}}
|
|
516
|
+
return widthMatch;
|
|
517
|
+
}});
|
|
518
|
+
if (widthMin !== null && widthMin > 0) {{
|
|
519
|
+
filteredEdges.forEach(function(edge) {{
|
|
520
|
+
validNodeIds.add(edge.from);
|
|
521
|
+
validNodeIds.add(edge.to);
|
|
522
|
+
}});
|
|
523
|
+
filteredNodes = filteredNodes.filter(function(node) {{
|
|
524
|
+
return validNodeIds.has(node.id) || filteredEdges.some(edge => edge.from === node.id || edge.to === node.id);
|
|
525
|
+
}});
|
|
526
|
+
}}
|
|
527
|
+
}}
|
|
528
|
+
|
|
529
|
+
filteredNodes = filteredNodes.filter(function(node) {{
|
|
530
|
+
var fullColor = typeof node.color === 'string' ? node.color : node.color.background;
|
|
531
|
+
var fullShape = node.shape_label || node.shape;
|
|
532
|
+
var colorMatch = colorSelect.includes('all') || colorSelect.some(cs => fullColor === cs || fullColor.startsWith(cs.split('-')[0]));
|
|
533
|
+
var shapeMatch = shapeSelect.includes('all') || (shapeSelect.length === 1 && shapeSelect[0] !== 'all' && fullShape === shapeSelect[0]);
|
|
534
|
+
var sizeMatch = node.size >= sizeMin && node.size <= sizeMax;
|
|
535
|
+
var titleMatch = titleSelect.includes('all') || (node.title && titleSelect.includes(node.title));
|
|
536
|
+
return colorMatch && shapeMatch && sizeMatch && titleMatch;
|
|
537
|
+
}});
|
|
538
|
+
|
|
539
|
+
filteredNodes.sort((a, b) => b.size - a.size);
|
|
540
|
+
|
|
541
|
+
filteredNodes = filteredNodes.slice(0, maxDisplay);
|
|
542
|
+
|
|
543
|
+
filteredEdges = allEdges.filter(function(edge) {{
|
|
544
|
+
var fullEdgeColor = edge.color.color;
|
|
545
|
+
var colorMatch = edgeColorSelect.includes('all') || edgeColorSelect.some(function(ecs) {{
|
|
546
|
+
return fullEdgeColor === ecs || fullEdgeColor.startsWith(ecs.split('-')[0]);
|
|
547
|
+
}});
|
|
548
|
+
|
|
549
|
+
var widthMatch = true;
|
|
550
|
+
if (widthMin !== null) {{
|
|
551
|
+
widthMatch = widthMatch && edge.width >= widthMin;
|
|
552
|
+
}}
|
|
553
|
+
if (widthMax !== null) {{
|
|
554
|
+
widthMatch = widthMatch && edge.width <= widthMax;
|
|
555
|
+
}}
|
|
556
|
+
|
|
557
|
+
var nodeMatch = filteredNodes.some(function(n) {{
|
|
558
|
+
return n.id === edge.from;
|
|
559
|
+
}}) && filteredNodes.some(function(n) {{
|
|
560
|
+
return n.id === edge.to;
|
|
561
|
+
}});
|
|
562
|
+
return colorMatch && widthMatch && nodeMatch;
|
|
563
|
+
}});
|
|
564
|
+
|
|
565
|
+
document.getElementById('node-count').textContent = filteredNodes.length;
|
|
566
|
+
document.getElementById('edge-count').textContent = filteredEdges.length;
|
|
567
|
+
|
|
568
|
+
nodes.clear();
|
|
569
|
+
nodes.add(filteredNodes);
|
|
570
|
+
edges.clear();
|
|
571
|
+
edges.add(filteredEdges);
|
|
572
|
+
}}
|
|
573
|
+
|
|
574
|
+
function togglePanel(panelId) {{
|
|
575
|
+
console.log("Toggling panel: " + panelId);
|
|
576
|
+
var panel = document.getElementById(panelId);
|
|
577
|
+
if (panel) {{
|
|
578
|
+
panel.classList.toggle('panel-hidden');
|
|
579
|
+
}} else {{
|
|
580
|
+
console.error("Panel " + panelId + " not found!");
|
|
581
|
+
}}
|
|
582
|
+
}}
|
|
583
|
+
|
|
584
|
+
network.on("zoom", function(params) {{
|
|
585
|
+
var scale = params.scale;
|
|
586
|
+
if (scale > lastScale) {{
|
|
587
|
+
var pointerDOM = params.pointer.DOM;
|
|
588
|
+
var pointerCanvas = network.DOMtoCanvas(pointerDOM);
|
|
589
|
+
lastZoomCenter = pointerCanvas;
|
|
590
|
+
updateNodes(lastZoomCenter, 'in');
|
|
591
|
+
}} else if (scale < lastScale) {{
|
|
592
|
+
updateNodes(null, 'out');
|
|
593
|
+
}}
|
|
594
|
+
lastScale = scale;
|
|
595
|
+
}});
|
|
596
|
+
|
|
597
|
+
// Add Enter key handler for label-search input
|
|
598
|
+
document.getElementById('label-search').addEventListener('keydown', function(event) {{
|
|
599
|
+
if (event.key === 'Enter') {{
|
|
600
|
+
document.getElementById('label-suggestions').style.display = 'none';
|
|
601
|
+
updateNodes();
|
|
602
|
+
}}
|
|
603
|
+
}});
|
|
604
|
+
|
|
605
|
+
updateNodes();
|
|
606
|
+
</script>
|
|
607
|
+
""".format(
|
|
608
|
+
all_nodes_json=all_nodes_json,
|
|
609
|
+
all_edges_json=all_edges_json,
|
|
610
|
+
nodes_json=nodes_json,
|
|
611
|
+
edges_json=edges_json,
|
|
612
|
+
maximum_display=maximum_display
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
html_code = f"""
|
|
616
|
+
<html>
|
|
617
|
+
<head>
|
|
618
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
619
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
620
|
+
<style type="text/css">
|
|
621
|
+
body {{
|
|
622
|
+
font-family: 'Inter', sans-serif;
|
|
623
|
+
display: flex;
|
|
624
|
+
flex-direction: column;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
align-items: center;
|
|
627
|
+
min-height: 100vh;
|
|
628
|
+
margin: 0;
|
|
629
|
+
background-color: #e0e0e0;
|
|
630
|
+
position: relative;
|
|
631
|
+
}}
|
|
632
|
+
#mynetwork {{
|
|
633
|
+
width: 100vw;
|
|
634
|
+
height: 100vh;
|
|
635
|
+
border: 1px solid lightgray;
|
|
636
|
+
background-color: #f9f9f9;
|
|
637
|
+
border-radius: 8px;
|
|
638
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
639
|
+
transition: opacity 0.5s ease;
|
|
640
|
+
}}
|
|
641
|
+
#diagram-title {{
|
|
642
|
+
position: absolute;
|
|
643
|
+
top: 10px;
|
|
644
|
+
left: 50px;
|
|
645
|
+
z-index: 100;
|
|
646
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
647
|
+
padding: 10px 15px;
|
|
648
|
+
border-radius: 5px;
|
|
649
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
650
|
+
font-size: 24px;
|
|
651
|
+
color: #333;
|
|
652
|
+
cursor: move;
|
|
653
|
+
user-select: none;
|
|
654
|
+
}}
|
|
655
|
+
#loading-bar {{
|
|
656
|
+
position: absolute;
|
|
657
|
+
top: 50%;
|
|
658
|
+
left: 50%;
|
|
659
|
+
transform: translate(-50%, -50%);
|
|
660
|
+
width: 300px;
|
|
661
|
+
height: 30px;
|
|
662
|
+
background-color: #ddd;
|
|
663
|
+
border-radius: 15px;
|
|
664
|
+
overflow: hidden;
|
|
665
|
+
z-index: 150;
|
|
666
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
667
|
+
}}
|
|
668
|
+
#loading-progress {{
|
|
669
|
+
width: 0%;
|
|
670
|
+
height: 100%;
|
|
671
|
+
background-color: #4CAF50;
|
|
672
|
+
transition: width 0.3s ease;
|
|
673
|
+
}}
|
|
674
|
+
#loading-percentage {{
|
|
675
|
+
position: absolute;
|
|
676
|
+
top: 50%;
|
|
677
|
+
left: 50%;
|
|
678
|
+
transform: translate(-50%, -50%);
|
|
679
|
+
font-size: 16px;
|
|
680
|
+
font-weight: bold;
|
|
681
|
+
color: #333;
|
|
682
|
+
z-index: 151;
|
|
683
|
+
}}
|
|
684
|
+
#config-container {{
|
|
685
|
+
position: absolute;
|
|
686
|
+
bottom: 20px;
|
|
687
|
+
right: 20px;
|
|
688
|
+
z-index: 100;
|
|
689
|
+
display: flex;
|
|
690
|
+
flex-direction: column;
|
|
691
|
+
gap: 10px;
|
|
692
|
+
max-height: calc(100vh - 40px);
|
|
693
|
+
max-width: 20vw;
|
|
694
|
+
overflow-y: auto;
|
|
695
|
+
padding: 10px;
|
|
696
|
+
}}
|
|
697
|
+
#description-container {{
|
|
698
|
+
position: absolute;
|
|
699
|
+
bottom: 20px;
|
|
700
|
+
left: 20px;
|
|
701
|
+
z-index: 100;
|
|
702
|
+
display: flex;
|
|
703
|
+
flex-direction: column;
|
|
704
|
+
gap: 10px;
|
|
705
|
+
max-height: calc(100vh - 40px);
|
|
706
|
+
max-width: 20vw;
|
|
707
|
+
overflow-y: auto;
|
|
708
|
+
padding: 10px;
|
|
709
|
+
}}
|
|
710
|
+
#save-button-container {{
|
|
711
|
+
position: absolute;
|
|
712
|
+
bottom: 20px;
|
|
713
|
+
left: 50%;
|
|
714
|
+
transform: translateX(-50%);
|
|
715
|
+
z-index: 100;
|
|
716
|
+
display: flex;
|
|
717
|
+
justify-content: center;
|
|
718
|
+
}}
|
|
719
|
+
#save-selection {{
|
|
720
|
+
background-color: white;
|
|
721
|
+
color: #333333;
|
|
722
|
+
padding: 10px 20px;
|
|
723
|
+
border: none;
|
|
724
|
+
border-radius: 5px;
|
|
725
|
+
cursor: pointer;
|
|
726
|
+
font-size: 16px;
|
|
727
|
+
font-weight: bold;
|
|
728
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
729
|
+
transition: background-color 0.3s ease;
|
|
730
|
+
}}
|
|
731
|
+
#save-selection:hover {{
|
|
732
|
+
background-color: #45a049;
|
|
733
|
+
}}
|
|
734
|
+
#node-config, #physics-config, #description-table {{
|
|
735
|
+
background-color: white;
|
|
736
|
+
padding: 10px;
|
|
737
|
+
border-radius: 5px;
|
|
738
|
+
border: 1px solid #ccc;
|
|
739
|
+
max-width: 100%;
|
|
740
|
+
overflow-x: auto;
|
|
741
|
+
}}
|
|
742
|
+
#node-config .filter-group, #description-table .table-group {{
|
|
743
|
+
margin-bottom: 10px;
|
|
744
|
+
}}
|
|
745
|
+
#node-config .filter-group label, #description-table .table-group label {{
|
|
746
|
+
display: block;
|
|
747
|
+
font-weight: bold;
|
|
748
|
+
margin-bottom: 5px;
|
|
749
|
+
|
|
750
|
+
}}
|
|
751
|
+
.label-search {{
|
|
752
|
+
color: #0066cc; /* Added color for Node Label Search label */
|
|
753
|
+
}}
|
|
754
|
+
#node-config select, #node-config input[type="number"], #node-config input[type="text"], #description-table table {{
|
|
755
|
+
width: 100%;
|
|
756
|
+
padding: 5px;
|
|
757
|
+
border-radius: 3px;
|
|
758
|
+
border: 1px solid #ccc;
|
|
759
|
+
box-sizing: border-box;
|
|
760
|
+
}}
|
|
761
|
+
#node-config .size-inputs {{
|
|
762
|
+
display: flex;
|
|
763
|
+
gap: 5px;
|
|
764
|
+
}}
|
|
765
|
+
#node-config .size-inputs input[type="number"] {{
|
|
766
|
+
flex: 1;
|
|
767
|
+
}}
|
|
768
|
+
.legend {{
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
771
|
+
padding: 8px 12px;
|
|
772
|
+
border-radius: 5px;
|
|
773
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
774
|
+
font-size: 14px;
|
|
775
|
+
font-weight: bold;
|
|
776
|
+
color: #333;
|
|
777
|
+
text-align: center;
|
|
778
|
+
}}
|
|
779
|
+
.panel-hidden {{
|
|
780
|
+
display: none;
|
|
781
|
+
}}
|
|
782
|
+
#node-config p {{
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
color: #555;
|
|
785
|
+
margin: 5px 0;
|
|
786
|
+
}}
|
|
787
|
+
.vis-icon {{
|
|
788
|
+
font-family: "Font Awesome 5 Free" !important;
|
|
789
|
+
font-weight: 900;
|
|
790
|
+
}}
|
|
791
|
+
#description-table table {{
|
|
792
|
+
border-collapse: collapse;
|
|
793
|
+
width: 100%;
|
|
794
|
+
}}
|
|
795
|
+
#description-table th, #description-table td {{
|
|
796
|
+
border: 1px solid #ddd;
|
|
797
|
+
padding: 8px;
|
|
798
|
+
text-align: left;
|
|
799
|
+
}}
|
|
800
|
+
#description-table th {{
|
|
801
|
+
background-color: #f2f2f2;
|
|
802
|
+
}}
|
|
803
|
+
#color-select, #shape-select, #title-select, #edge-color-select {{
|
|
804
|
+
height: 100px;
|
|
805
|
+
}}
|
|
806
|
+
#node-edge-count {{
|
|
807
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
808
|
+
padding: 8px 12px;
|
|
809
|
+
border-radius: 5px;
|
|
810
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
811
|
+
font-size: 14px;
|
|
812
|
+
font-weight: bold;
|
|
813
|
+
color: #333;
|
|
814
|
+
text-align: center;
|
|
815
|
+
}}
|
|
816
|
+
.suggestions {{
|
|
817
|
+
position: absolute;
|
|
818
|
+
background-color: white;
|
|
819
|
+
border: 1px solid #ccc;
|
|
820
|
+
border-radius: 3px;
|
|
821
|
+
max-height: 150px;
|
|
822
|
+
overflow-y: auto;
|
|
823
|
+
width: 100%;
|
|
824
|
+
z-index: 200;
|
|
825
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
826
|
+
display: none;
|
|
827
|
+
}}
|
|
828
|
+
.suggestion-item {{
|
|
829
|
+
padding: 5px 10px;
|
|
830
|
+
cursor: pointer;
|
|
831
|
+
}}
|
|
832
|
+
.suggestion-item:hover {{
|
|
833
|
+
background-color: #f0f0f0;
|
|
834
|
+
}}
|
|
835
|
+
</style>
|
|
836
|
+
</head>
|
|
837
|
+
<body>
|
|
838
|
+
<h2 id="diagram-title">{network_title}</h2>
|
|
839
|
+
<div id="mynetwork"></div>
|
|
840
|
+
<div id="loading-bar">
|
|
841
|
+
<div id="loading-progress"></div>
|
|
842
|
+
<span id="loading-percentage">0%</span>
|
|
843
|
+
</div>
|
|
844
|
+
<div id="config-container">
|
|
845
|
+
<div class="legend" onclick="togglePanel('node-config')">Node & Edge Selection</div>
|
|
846
|
+
<div id="node-config" class="panel-hidden">
|
|
847
|
+
<div class="filter-group">
|
|
848
|
+
<label>Maximum Nodes to Display</label>
|
|
849
|
+
<select id="max-display-select" onchange="updateNodes()">
|
|
850
|
+
{max_display_options}
|
|
851
|
+
</select>
|
|
852
|
+
</div>
|
|
853
|
+
<div class="filter-group">
|
|
854
|
+
<label>Node Color</label>
|
|
855
|
+
<select id="color-select" multiple onchange="updateNodes()">
|
|
856
|
+
<option value="all" selected>All</option>
|
|
857
|
+
{color_options}
|
|
858
|
+
</select>
|
|
859
|
+
</div>
|
|
860
|
+
<div class="filter-group">
|
|
861
|
+
<label>Node Shape</label>
|
|
862
|
+
<select id="shape-select" multiple onchange="updateNodes()">
|
|
863
|
+
<option value="all" selected>All</option>
|
|
864
|
+
{shape_options}
|
|
865
|
+
</select>
|
|
866
|
+
</div>
|
|
867
|
+
<div class="filter-group">
|
|
868
|
+
<label>Node Size ({min_size}-{max_size})</label>
|
|
869
|
+
<div class="size-inputs">
|
|
870
|
+
<input type="number" id="size-min" value="{min_default_node_size}" placeholder="Min" min="0" oninput="updateNodes()">
|
|
871
|
+
<input type="number" id="size-max" placeholder="Max" min="0" oninput="updateNodes()">
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="filter-group">
|
|
875
|
+
<label class="label-search">Node Label Search</label>
|
|
876
|
+
<input type="text" id="label-search" placeholder="Enter labels, e.g., Node1,Node2" oninput="updateSuggestions()">
|
|
877
|
+
<div id="label-suggestions" class="suggestions"></div>
|
|
878
|
+
</div>
|
|
879
|
+
<div class="filter-group">
|
|
880
|
+
<label>Node Title (Class)</label>
|
|
881
|
+
<select id="title-select" multiple onchange="updateNodes()">
|
|
882
|
+
<option value="all" selected>All</option>
|
|
883
|
+
{title_options}
|
|
884
|
+
</select>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="filter-group">
|
|
887
|
+
<label>Edge Width ({min_width}-{max_width})</label>
|
|
888
|
+
<div class="size-inputs">
|
|
889
|
+
<input type="number" id="width-min" value="{min_default_edge_width}" placeholder="min" min="0" oninput="updateNodes()">
|
|
890
|
+
<input type="number" id="width-max" placeholder="Max" min="0" oninput="updateNodes()">
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
<div class="filter-group">
|
|
894
|
+
<label>Edge Color</label>
|
|
895
|
+
<select id="edge-color-select" multiple onchange="updateNodes()">
|
|
896
|
+
<option value="all" selected>All</option>
|
|
897
|
+
{edge_color_options}
|
|
898
|
+
</select>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
<div class="legend" onclick="togglePanel('physics-config')">Physics</div>
|
|
902
|
+
<div id="physics-config" class="panel-hidden"></div>
|
|
903
|
+
</div>
|
|
904
|
+
<div id="description-container">
|
|
905
|
+
<div id="node-edge-count" class="legend">
|
|
906
|
+
Nodes: <span id="node-count">0</span>, Edges: <span id="edge-count">0</span>
|
|
907
|
+
</div>
|
|
908
|
+
<div class="legend" onclick="togglePanel('description-table')">Diagram Description</div>
|
|
909
|
+
<div id="description-table" class="panel-hidden">
|
|
910
|
+
<p>{description_title}</p>
|
|
911
|
+
<div class="table-group">
|
|
912
|
+
<table>
|
|
913
|
+
<thead>
|
|
914
|
+
<tr>
|
|
915
|
+
<th>Name</th>
|
|
916
|
+
<th>Description</th>
|
|
917
|
+
</tr>
|
|
918
|
+
</thead>
|
|
919
|
+
<tbody>
|
|
920
|
+
{table_rows}
|
|
921
|
+
</tbody>
|
|
922
|
+
</table>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
<div id="save-button-container">
|
|
927
|
+
<button id="save-selection" onclick="saveSelection()">Save Selection</button>
|
|
928
|
+
</div>
|
|
929
|
+
{drag_script}
|
|
930
|
+
{script_code}
|
|
931
|
+
</body>
|
|
932
|
+
</html>
|
|
933
|
+
"""
|
|
934
|
+
|
|
935
|
+
if writeHTML is None:
|
|
936
|
+
html_add = "network_visualization.html"
|
|
937
|
+
else:
|
|
938
|
+
html_add = writeHTML
|
|
939
|
+
|
|
940
|
+
with open(html_add, "w", encoding="utf-8") as file:
|
|
941
|
+
file.write(html_code)
|
|
942
|
+
base_path = os.path.dirname(os.path.abspath(__file__))
|
|
943
|
+
|
|
944
|
+
if browserView:
|
|
945
|
+
webbrowser.open(html_add)
|
|
946
|
+
|
|
947
|
+
print(f"HTML file has been created: {base_path}/{writeHTML}")
|
|
948
|
+
return None
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
if __name__ == "__main__":
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
gipuzkoa_path = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Escudo_de_Donostia.svg/800px-Escudo_de_Donostia.svg.png"
|
|
955
|
+
turku_path = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Turku.vaakuna.svg/800px-Turku.vaakuna.svg.png"
|
|
956
|
+
|
|
957
|
+
turku_path2 = r"C:\PhDDocs\pythonPhD\pyBase4All\hyperlinks_scrape\scrapper\network_visual\network_visual\Turku.vaakuna.png"
|
|
958
|
+
gipuzkoa_path2 = r"C:\PhDDocs\pythonPhD\pyBase4All\hyperlinks_scrape\scrapper\network_visual\network_visual\Escudo_de_Donostia.png"
|
|
959
|
+
|
|
960
|
+
turku_img64 = base64_from_loc(turku_path2)
|
|
961
|
+
gipuzkoa_img64 = base64_from_loc(gipuzkoa_path2 )
|
|
962
|
+
|
|
963
|
+
nodes = [
|
|
964
|
+
{"id": 1, "label": "Start", "size": 9, "color": "red-spain", "shape": "dot", "title": "starting the point"},
|
|
965
|
+
{"id": 2, "label": "Process A", "size": 25, "color": "#33FF57-france", "shape": "triangle", "title": "process one"},
|
|
966
|
+
{"id": 3, "label": "Process B", "size": 40, "color": "#3357FF-france", "shape": "box-canada", "title": "process two"},
|
|
967
|
+
{"id": 4, "label": "Critical", "size": 35, "color": "#FFBD33-japan", "shape": "star-asia", "title": "critical point"},
|
|
968
|
+
{"id": 5, "label": "User", "size": 45, "color": "#8D33FF-germany", "shape": "icon-africa",
|
|
969
|
+
"icon": {"face": '"Font Awesome 5 Free"', "code": "\uf007", "size": 50, "color": "#8D33FF-germany"}},
|
|
970
|
+
{"id": 6, "label": "Info A", "size": 48, "color": "#33B5E5-canada", "shape": "icon-africa",
|
|
971
|
+
"icon": {"face": "Font Awesome 5 Brands", "code": "\uf007", "size": 40, "color": "#33B5E5-canada"}},
|
|
972
|
+
{"id": 7, "label": "Image Node A ", "size": 15, "shape": "image-gipuzkoa", "image": gipuzkoa_img64, "color": "#33B5E7-gipuzkoa"},
|
|
973
|
+
{"id": 8, "label": "Gogo", "size": 30, "color": "#FF5733-italy", "shape": "dot", "title": "I am so Alone"},
|
|
974
|
+
{"id": 9, "label": "comecome", "size": 8, "color": "#FF5733-italy", "shape": "dot", "title": "I am so Alone"},
|
|
975
|
+
{"id": 10, "label": "Image Node B", "size": 15, "shape": "image-gipuzkoa", "image": gipuzkoa_img64, "color": "#33B5E7-gipuzkoa"},
|
|
976
|
+
{"id": 11, "label": "Info B", "size": 48, "color": "#33B5E5-canada", "shape": "icon-australia",
|
|
977
|
+
"icon": {"face": "Font Awesome 5 Brands", "code": "\uf05a", "size": 40, "color": "#33B5E5-canada"}},
|
|
978
|
+
{"id": 12, "label": "turku", "size": 15, "shape": "image-turku", "image": turku_img64, "color": "#33B5E6-turku"},
|
|
979
|
+
{"id": 13, "label": "dict", "size": 9, "color": {"background": "yellow-Finland", "border": "green"}, "shape": "dot", "title": "starting the point"},
|
|
980
|
+
{"id":'abcdefg',"label":"abcdefg-label",'shape':'square','size':50},
|
|
981
|
+
|
|
982
|
+
]
|
|
983
|
+
|
|
984
|
+
edges = [
|
|
985
|
+
{"from": 1, "to": 3, "width": 1, "color": {"color": "#C70039-europe"}, "arrows": "to", "title": "from 1 to 3"},
|
|
986
|
+
{"from": 1, "to": 2, "width": 2, "color": {"color": "#C70038-africa"}},
|
|
987
|
+
{"from": 2, "to": 4, "width": 3, "color": {"color": "#581845-africa"}, "arrows": "to", "dashes": True},
|
|
988
|
+
{"from": 2, "to": 5, "width": 4, "color": {"color": "#FFC300-australia"}, "arrows": "to", "smooth": {"type": "curvedCW"}},
|
|
989
|
+
{"from": 3, "to": 3, "width": 5, "color": {"color": "#DAF7A6-america"}, "arrows": "to"},
|
|
990
|
+
{"from": 4, "to": 6, "width": 6, "color": {"color": "#4CAF50-europe"}, "arrows": "to", "smooth": {"type": "curvedCCW"}},
|
|
991
|
+
{"from": 5, "to": 1, "width": 7, "color": {"color": "#2196F3-asia"}, "arrows": "to", "dashes": True},
|
|
992
|
+
{"from": 7, "to": 1, "width": 8, "color": {"color": "#FF00FF-africa"}, "arrows": "to"},
|
|
993
|
+
{"from":14,"to":13}
|
|
994
|
+
]
|
|
995
|
+
visnet(nodes, edges, network_title='small Network', browserView=True, writeHTML='test_small.html', min_default_node_size=1, min_default_edge_width=0, maximum_display=10)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
import random
|
|
1000
|
+
nodes2 = []
|
|
1001
|
+
edges2 = []
|
|
1002
|
+
|
|
1003
|
+
for i in range(1, 10001): # Changed to 10,000 nodes
|
|
1004
|
+
if i<20:
|
|
1005
|
+
size = random.randint(10,20)
|
|
1006
|
+
else:
|
|
1007
|
+
size = random.randint(1,5)
|
|
1008
|
+
width = random.randint(1, 4)
|
|
1009
|
+
node = {"id": i, "label": "Node " + str(i), "size": size, "color": random.choice(['red','blue','yellow']), "shape": "dot", "title": "Node " + str(i)}
|
|
1010
|
+
nodes2.append(node)
|
|
1011
|
+
edge = {"from": random.randint(1, 20), "to": i , "width": width, "color": {"color": "#131112"}, "arrows": "to", "title": f"to {i} width {width}"}
|
|
1012
|
+
edges2.append(edge)
|
|
1013
|
+
|
|
1014
|
+
visnet(nodes2, edges2, network_title='Large Network', browserView=True, writeHTML='test_large.html', min_default_node_size=1, min_default_edge_width=1, maximum_display=100)
|