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.
@@ -0,0 +1,660 @@
1
+ import json
2
+ import pandas as pd
3
+ from geopy.geocoders import Nominatim
4
+ import random
5
+ import time
6
+
7
+ def latLong(place):
8
+ user_agent = random.randint(10000, 99999)
9
+ geolocator = Nominatim(user_agent=str(user_agent))
10
+ location = geolocator.geocode(place)
11
+ latitude = location.latitude
12
+ longitude = location.longitude
13
+ full_address = location.address
14
+ return latitude, longitude, full_address
15
+
16
+ def netmap(cities_data, connections_data, title="Network Map", maximum_nodes=100, output_html_file="network_map.html", default_size=5):
17
+ """
18
+ cities_data (list): {
19
+ "node": "New York", "lat": 40.7128, "lon": -74.0060, "size": 15,
20
+ "color": "red", "shape": "circle", "node_hover": "The Big Apple, USA"
21
+ }, # shape: circle, triangle, square, star
22
+ connections_data(list): {
23
+ "node1": "New York", "node2": "Paris", "color": "darkblue", "width": 2,
24
+ "style": "dashed", "arrow": False, "curve": True,
25
+ "edge_hover": any text e.g. "Transatlantic flight"
26
+ "default_size": node size 5
27
+ },
28
+ maximum_nodes: maximum nodes to display initially
29
+ """
30
+ df_nodes = pd.DataFrame(cities_data)
31
+ if not df_nodes['lat'].apply(lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)).all():
32
+ raise ValueError("latitude contains non-numeric values.")
33
+ if not df_nodes['lon'].apply(lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)).all():
34
+ raise ValueError("longitude contains non-numeric values.")
35
+ if not df_nodes['size'].apply(lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)).all():
36
+ raise ValueError("node size contains non-numeric values.")
37
+
38
+ df_edges = pd.DataFrame(connections_data)
39
+ if not df_edges['width'].apply(lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)).all():
40
+ raise ValueError("edge width contains non-numeric values.")
41
+
42
+ # Define default values
43
+ defaults_city = {
44
+ "lat": lambda city: latLong(city["node"])[0],
45
+ "lon": lambda city: latLong(city["node"])[1],
46
+ "size": default_size,
47
+ "color": "gray",
48
+ "shape": "circle",
49
+ "node_hover": lambda city: city["node"]
50
+ }
51
+
52
+ for city in cities_data:
53
+ for key, default_value in defaults_city.items():
54
+ if key not in city:
55
+ city[key] = default_value(city) if callable(default_value) else default_value
56
+
57
+ defaults_connection = {
58
+ "color": "gray",
59
+ "width": 1,
60
+ "style": "solid",
61
+ "arrow": False,
62
+ "curve": False,
63
+ "edge_hover": lambda connection: (connection['node1'] + ' to ' + connection['node2'])
64
+ }
65
+
66
+ for connection in connections_data:
67
+ for key, default_value in defaults_connection.items():
68
+ if key not in connection:
69
+ connection[key] = default_value(connection) if callable(default_value) else default_value
70
+
71
+ # Pre-filter based on maximum_nodes for initial load
72
+ sorted_cities = sorted(cities_data, key=lambda x: x['size'], reverse=True)
73
+ selected_cities = sorted_cities[:min(maximum_nodes, len(cities_data))]
74
+ selected_city_nodes = {city['node'] for city in selected_cities}
75
+ filtered_connections = [conn for conn in connections_data if conn['node1'] in selected_city_nodes and conn['node2'] in selected_city_nodes]
76
+
77
+ cities_json = json.dumps(selected_cities, indent=2)
78
+ connections_json = json.dumps(filtered_connections, indent=2)
79
+ all_cities_json = json.dumps(cities_data, indent=2)
80
+ all_connections_json = json.dumps(connections_data, indent=2)
81
+
82
+ total_nodes = len(cities_data)
83
+ total_edges = len(connections_data)
84
+ effective_max_nodes = min(maximum_nodes, len(selected_cities))
85
+
86
+ # Extract unique colors and hover texts from all cities_data
87
+ unique_colors = sorted(set(city['color'] for city in cities_data))
88
+ unique_hovers = sorted(set(city['node_hover'] for city in cities_data))
89
+
90
+ html_content = f"""
91
+ <!DOCTYPE html>
92
+ <html>
93
+ <head>
94
+ <title>Network Map</title>
95
+ <meta charset="utf-8" />
96
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
98
+ <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
99
+ <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
100
+ <style>
101
+ html, body, #map {{
102
+ height: 100%;
103
+ margin: 0;
104
+ }}
105
+ .arrow-label {{
106
+ background: white;
107
+ padding: 2px 6px;
108
+ border-radius: 4px;
109
+ font-size: 12px;
110
+ box-shadow: 0 0 4px rgba(0,0,0,0.3);
111
+ pointer-events: none;
112
+ white-space: nowrap;
113
+ }}
114
+ .leaflet-control-title {{
115
+ background: rgba(255, 255, 255, 0.8);
116
+ padding: 10px 20px;
117
+ border-radius: 5px;
118
+ font-size: 18px;
119
+ font-weight: bold;
120
+ text-align: center;
121
+ box-shadow: 0 0 8px rgba(0,0,0,0.3);
122
+ cursor: move;
123
+ position: absolute;
124
+ top: 10px;
125
+ left: 50%;
126
+ transform: translateX(-50%);
127
+ z-index: 1000;
128
+ user-select: none;
129
+ }}
130
+ .slider-container {{
131
+ position: absolute;
132
+ bottom: 10px;
133
+ left: 10px;
134
+ z-index: 1000;
135
+ background: rgba(255, 255, 255, 0.8);
136
+ padding: 10px;
137
+ border-radius: 5px;
138
+ box-shadow: 0 0 8px rgba(0,0,0,0.3);
139
+ display: none; /* Initially hidden */
140
+ }}
141
+ .slider-label {{
142
+ font-size: 14px;
143
+ margin-bottom: 5px;
144
+ }}
145
+ .dimmed {{
146
+ opacity: 0.3;
147
+ }}
148
+ .dropdown-container {{
149
+ margin-top: 10px;
150
+ }}
151
+ select, input {{
152
+ width: 100%;
153
+ padding: 5px;
154
+ font-size: 14px;
155
+ border-radius: 4px;
156
+ }}
157
+ .toggle-button {{
158
+ position: absolute;
159
+ top: 10px; /* Moved to top to avoid covering controls */
160
+ right: 10px; /* Positioned to the right */
161
+ z-index: 1001;
162
+ background: rgba(255, 255, 255, 0.8);
163
+ padding: 8px 12px;
164
+ border-radius: 5px;
165
+ font-size: 14px;
166
+ cursor: pointer;
167
+ box-shadow: 0 0 8px rgba(0,0,0,0.3);
168
+ border: none;
169
+ }}
170
+ .toggle-button:hover {{
171
+ background: rgba(255, 255, 255, 1);
172
+ }}
173
+ </style>
174
+ </head>
175
+ <body>
176
+ <div id="map"></div>
177
+ <div id="map-title" class="leaflet-control-title">{title}</div>
178
+ <button id="toggle-controls" class="toggle-button">Show Controls</button>
179
+ <div id="controls" class="slider-container">
180
+ <div class="slider-label">Max Nodes: <span id="node-count">{effective_max_nodes}</span></div>
181
+ <input type="range" id="node-slider" min="1" max="{total_nodes}" value="{effective_max_nodes}" step="1">
182
+ <div class="slider-label">Edges: <span id="edge-count">0 / {total_edges}</span></div>
183
+ <div class="dropdown-container">
184
+ <div class="slider-label">Filter by Color:</div>
185
+ <select id="color-filter" multiple>
186
+ <option value="all">All Colors</option>
187
+ {"".join(f'<option value="{color}">{color}</option>' for color in unique_colors)}
188
+ </select>
189
+ </div>
190
+ <div class="dropdown-container">
191
+ <div class="slider-label">Filter by Hover:</div>
192
+ <select id="hover-filter" multiple>
193
+ <option value="all">All Hovers</option>
194
+ {"".join(f'<option value="{hover}">{hover}</option>' for hover in unique_hovers)}
195
+ </select>
196
+ </div>
197
+ <div class="dropdown-container">
198
+ <div class="slider-label">Lat Range (-90 to 90):</div>
199
+ <input type="number" id="lat-min" min="-90" max="90" value="-90" step="0.1">
200
+ <input type="number" id="lat-max" min="-90" max="90" value="90" step="0.1">
201
+ </div>
202
+ <div class="dropdown-container">
203
+ <div class="slider-label">Lon Range (-180 to 180):</div>
204
+ <input type="number" id="lon-min" min="-180" max="180" value="-180" step="0.1">
205
+ <input type="number" id="lon-max" min="-180" max="180" value="180" step="0.1">
206
+ </div>
207
+ </div>
208
+
209
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
210
+ <script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
211
+ <script src="https://rawcdn.githack.com/makinacorpus/Leaflet.TextPath/master/leaflet.textpath.js"></script>
212
+
213
+ <script>
214
+ const cities = {cities_json};
215
+ const connections = {connections_json};
216
+ const all_cities = {all_cities_json};
217
+ const all_connections = {all_connections_json};
218
+ const total_nodes = {total_nodes};
219
+ const total_edges = {total_edges};
220
+ let selectedNode = null;
221
+ let selectedColor = ['all'];
222
+ let selectedHover = ['all'];
223
+ let latMin = -90;
224
+ let latMax = 90;
225
+ let lonMin = -180;
226
+ let lonMax = 180;
227
+
228
+ const map = L.map('map').setView([20, 0], 2);
229
+ L.tileLayer('https://{{s}}.basemaps.cartocdn.com/light_all/{{z}}/{{x}}/{{y}}.png', {{
230
+ maxZoom: 19,
231
+ attribution: '© CartoDB'
232
+ }}).addTo(map);
233
+
234
+ const titleDiv = document.getElementById('map-title');
235
+ const draggable = new L.Draggable(titleDiv);
236
+ draggable.enable();
237
+
238
+ map.getContainer().style.position = 'relative';
239
+
240
+ // Initialize marker cluster group and edges layer
241
+ const markers = L.markerClusterGroup({{
242
+ maxClusterRadius: 50,
243
+ disableClusteringAtZoom: 10,
244
+ spiderfyOnMaxZoom: true
245
+ }});
246
+ const edges = L.layerGroup();
247
+ map.addLayer(markers);
248
+ map.addLayer(edges);
249
+
250
+ // Function to create a marker for a city
251
+ function createMarker(city) {{
252
+ let iconHtml = '';
253
+ const size = city.size * 2;
254
+
255
+ if (city.shape === "circle") {{
256
+ iconHtml = `<div style="width:${{size}}px;height:${{size}}px;background:${{city.color}};border-radius:50%;opacity:0.8;"></div>`;
257
+ }} else if (city.shape === "square") {{
258
+ iconHtml = `<div style="width:${{size}}px;height:${{size}}px;background:${{city.color}};opacity:0.8;"></div>`;
259
+ }} else if (city.shape === "star") {{
260
+ iconHtml = `
261
+ <svg width="${{size}}" height="${{size}}" viewBox="0 0 100 100">
262
+ <polygon points="50,5 61,35 95,35 67,57 76,91 50,70 24,91 33,57 5,35 39,35"
263
+ fill="${{city.color}}" opacity="0.8" />
264
+ </svg>`;
265
+ }}
266
+
267
+ const icon = L.divIcon({{
268
+ html: iconHtml,
269
+ className: "",
270
+ iconSize: [size, size]
271
+ }});
272
+
273
+ const marker = L.marker([city.lat, city.lon], {{
274
+ icon: icon
275
+ }});
276
+
277
+ if (city.node_hover) {{
278
+ marker.bindTooltip(city.node_hover, {{
279
+ direction: 'top',
280
+ permanent: false,
281
+ opacity: 0.9
282
+ }});
283
+ }}
284
+
285
+ marker.bindPopup(city.node);
286
+
287
+ marker.on('click', () => {{
288
+ selectedNode = city.node;
289
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
290
+ }});
291
+
292
+ return marker;
293
+ }}
294
+
295
+ function generateBezierPoints(p0, p1, p2, numPoints) {{
296
+ const points = [p0];
297
+ for (let t = 1 / (numPoints - 1); t < 1; t += 1 / (numPoints - 1)) {{
298
+ const u = 1 - t;
299
+ const tt = t * t;
300
+ const uu = u * u;
301
+ const lat = uu * p0[0] + 2 * u * t * p1[0] + tt * p2[0];
302
+ const lon = uu * p0[1] + 2 * u * t * p1[1] + tt * p2[1];
303
+ points.push([lat, lon]);
304
+ }}
305
+ points.push(p2);
306
+ return points;
307
+ }}
308
+
309
+ // Debounce function to limit update frequency
310
+ function debounce(func, wait) {{
311
+ let timeout;
312
+ return function executedFunction(...args) {{
313
+ const later = () => {{
314
+ clearTimeout(timeout);
315
+ func(...args);
316
+ }};
317
+ clearTimeout(timeout);
318
+ timeout = setTimeout(later, wait);
319
+ }};
320
+ }}
321
+
322
+ function updateNodes(maxNodes) {{
323
+ document.getElementById('node-count').textContent = maxNodes + ' / ' + total_nodes;
324
+ let filteredCities = all_cities;
325
+
326
+ // Apply color filter
327
+ if (selectedColor[0] !== 'all' && selectedColor.length > 0) {{
328
+ filteredCities = filteredCities.filter(city => selectedColor.includes(city.color));
329
+ }}
330
+
331
+ // Apply hover filter
332
+ if (selectedHover[0] !== 'all' && selectedHover.length > 0) {{
333
+ filteredCities = filteredCities.filter(city => selectedHover.includes(city.node_hover));
334
+ }}
335
+
336
+ // Apply lat/lon range filter
337
+ filteredCities = filteredCities.filter(city =>
338
+ city.lat >= latMin && city.lat <= latMax &&
339
+ city.lon >= lonMin && city.lon <= lonMax
340
+ );
341
+
342
+ // Apply max nodes limit
343
+ const sortedCities = filteredCities.sort((a, b) => b.size - a.size).slice(0, maxNodes);
344
+ const selectedCitynodes = new Set(sortedCities.map(city => city.node));
345
+
346
+ // Get visible bounds
347
+ const bounds = map.getBounds();
348
+ const visibleCities = sortedCities.filter(city =>
349
+ bounds.contains([city.lat, city.lon])
350
+ );
351
+ const visibleCitynodes = new Set(visibleCities.map(city => city.node));
352
+
353
+ // Clear existing markers, edges, and hover labels
354
+ markers.clearLayers();
355
+ edges.clearLayers();
356
+ map.eachLayer(layer => {{
357
+ if (layer instanceof L.Marker && layer.options.icon.options.className === 'arrow-label') {{
358
+ map.removeLayer(layer);
359
+ }}
360
+ }});
361
+
362
+ // Add markers for visible cities
363
+ visibleCities.forEach(city => {{
364
+ const marker = createMarker(city);
365
+ if (selectedNode && city.node !== selectedNode && !all_connections.some(link =>
366
+ (link.node1 === selectedNode && link.node2 === city.node) ||
367
+ (link.node2 === selectedNode && link.node1 === city.node))) {{
368
+ marker.setOpacity(0.3);
369
+ }} else {{
370
+ marker.setOpacity(1);
371
+ }}
372
+ markers.addLayer(marker);
373
+ }});
374
+
375
+ // Filter connections to include edges between visible nodes or connected to selectedNode
376
+ const filteredConnections = all_connections.filter(link =>
377
+ (visibleCitynodes.has(link.node1) && visibleCitynodes.has(link.node2)) ||
378
+ (selectedNode && (link.node1 === selectedNode || link.node2 === selectedNode))
379
+ );
380
+
381
+ // Update node count to reflect visible nodes out of filtered total
382
+ document.getElementById('node-count').textContent = visibleCities.length + ' / ' + sortedCities.length;
383
+
384
+ // Update edge count display
385
+ document.getElementById('edge-count').textContent = filteredConnections.length + ' / ' + total_edges;
386
+
387
+ // Detect bidirectional edges
388
+ const bidirectionalEdges = new Set();
389
+ filteredConnections.forEach((link, index) => {{
390
+ const reverseLink = filteredConnections.find((l, i) => i !== index && l.node1 === link.node2 && l.node2 === link.node1);
391
+ if (reverseLink) {{
392
+ bidirectionalEdges.add(`${{link.node1}}-${{link.node2}}`);
393
+ bidirectionalEdges.add(`${{link.node2}}-${{link.node1}}`);
394
+ }}
395
+ }});
396
+
397
+ filteredConnections.forEach(link => {{
398
+ const fromCity = all_cities.find(c => c.node === link.node1);
399
+ const toCity = all_cities.find(c => c.node === link.node2);
400
+ // Skip if either node is missing
401
+ if (!fromCity || !toCity) return;
402
+
403
+ const from = [fromCity.lat, fromCity.lon];
404
+ const to = [toCity.lat, toCity.lon];
405
+
406
+ let latlngs = [from, to];
407
+ const edgeKey = `${{link.node1}}-${{link.node2}}`;
408
+ const isBidirectional = bidirectionalEdges.has(edgeKey);
409
+ const shouldCurve = link.curve || isBidirectional;
410
+
411
+ if (shouldCurve) {{
412
+ const midLat = (from[0] + to[0]) / 2;
413
+ const midLon = (from[1] + to[1]) / 2;
414
+ const latDiff = to[0] - from[0];
415
+ const lonDiff = to[1] - from[1];
416
+ const length = Math.sqrt(latDiff * latDiff + lonDiff * lonDiff);
417
+ const baseOffsetMagnitude = Math.min(length * 0.1, 5);
418
+ const perpLat = -lonDiff / length * baseOffsetMagnitude;
419
+ const perpLon = latDiff / length * baseOffsetMagnitude;
420
+
421
+ let controlLat, controlLon;
422
+ if (isBidirectional) {{
423
+ const isReverse = link.node1 > link.node2;
424
+ const bidirectionalOffset = baseOffsetMagnitude * 2;
425
+ controlLat = midLat + (isReverse ? -perpLat * 2 : perpLat * 2) + (isReverse ? bidirectionalOffset : -bidirectionalOffset);
426
+ controlLon = midLon + (isReverse ? -perpLon * 2 : perpLon * 2) + (isReverse ? bidirectionalOffset : -bidirectionalOffset);
427
+ }} else {{
428
+ controlLat = midLat + perpLat;
429
+ controlLon = midLon + perpLon;
430
+ }}
431
+
432
+ latlngs = generateBezierPoints(from, [controlLat, controlLon], to, 50);
433
+ }}
434
+
435
+ const lineStyle = {{
436
+ color: link.color,
437
+ weight: link.width,
438
+ dashArray:
439
+ link.style === "dashed" ? "5, 10" :
440
+ link.style === "dotted" ? "2, 6" : null,
441
+ opacity: selectedNode && !(link.node1 === selectedNode || link.node2 === selectedNode) ? 0.3 : 1
442
+ }};
443
+
444
+ const polyline = L.polyline(latlngs, {{
445
+ ...lineStyle
446
+ }}).addTo(edges);
447
+
448
+ if (link.arrow) {{
449
+ const arrowSize = Math.max(10, link.width * 4);
450
+ polyline.setText('▶', {{
451
+ repeat: false,
452
+ center: true,
453
+ offset: 0,
454
+ attributes: {{
455
+ fill: link.color,
456
+ "font-size": `${{arrowSize}}`,
457
+ "font-weight": "bold",
458
+ "dy": `${{arrowSize / 3}}`,
459
+ opacity: selectedNode && !(link.node1 === selectedNode || link.node2 === selectedNode) ? 0.3 : 1
460
+ }}
461
+ }});
462
+ }}
463
+
464
+ const hoverLabel = L.marker([0, 0], {{
465
+ icon: L.divIcon({{
466
+ className: 'arrow-label',
467
+ html: link.edge_hover || `${{link.node1}} → ${{link.node2}}`,
468
+ iconSize: null
469
+ }}),
470
+ interactive: false
471
+ }});
472
+
473
+ const hoverLine = L.polyline(latlngs, {{
474
+ color: '#0000',
475
+ weight: Math.max(link.width + 10, 15),
476
+ opacity: 0,
477
+ interactive: true
478
+ }}).addTo(edges);
479
+
480
+ hoverLine.on("mouseover", (e) => {{
481
+ hoverLabel.setLatLng(e.latlng);
482
+ map.addLayer(hoverLabel);
483
+ }});
484
+ hoverLine.on("mouseout", () => {{
485
+ if (map.hasLayer(hoverLabel)) {{
486
+ map.removeLayer(hoverLabel);
487
+ }}
488
+ }});
489
+ hoverLine.on("click", () => {{
490
+ if (map.hasLayer(hoverLabel)) {{
491
+ map.removeLayer(hoverLabel);
492
+ }}
493
+ }});
494
+ }});
495
+ }}
496
+
497
+ // Debounced update function
498
+ const debouncedUpdateNodes = debounce(updateNodes, 300);
499
+
500
+ // Toggle controls visibility
501
+ const toggleButton = document.getElementById('toggle-controls');
502
+ const controlsDiv = document.getElementById('controls');
503
+ toggleButton.addEventListener('click', () => {{
504
+ if (controlsDiv.style.display === 'none') {{
505
+ controlsDiv.style.display = 'block';
506
+ toggleButton.textContent = 'Hide Controls';
507
+ }} else {{
508
+ controlsDiv.style.display = 'none';
509
+ toggleButton.textContent = 'Show Controls';
510
+ }}
511
+ }});
512
+
513
+ // Update selected values for multiple selections
514
+ function updateSelectedValues(selectElement, selectedArray) {{
515
+ const options = Array.from(selectElement.selectedOptions);
516
+ selectedArray.length = 0; // Clear the array
517
+ options.forEach(option => selectedArray.push(option.value));
518
+ if (selectedArray.length === 0) selectedArray.push('all'); // Default to 'all' if nothing selected
519
+ }}
520
+
521
+ // Event listeners for multiple selections
522
+ document.getElementById('color-filter').addEventListener('change', (e) => {{
523
+ updateSelectedValues(e.target, selectedColor);
524
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
525
+ }});
526
+
527
+ document.getElementById('hover-filter').addEventListener('change', (e) => {{
528
+ updateSelectedValues(e.target, selectedHover);
529
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
530
+ }});
531
+
532
+ document.getElementById('lat-min').addEventListener('input', (e) => {{
533
+ latMin = parseFloat(e.target.value);
534
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
535
+ }});
536
+
537
+ document.getElementById('lat-max').addEventListener('input', (e) => {{
538
+ latMax = parseFloat(e.target.value);
539
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
540
+ }});
541
+
542
+ document.getElementById('lon-min').addEventListener('input', (e) => {{
543
+ lonMin = parseFloat(e.target.value);
544
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
545
+ }});
546
+
547
+ document.getElementById('lon-max').addEventListener('input', (e) => {{
548
+ lonMax = parseFloat(e.target.value);
549
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
550
+ }});
551
+
552
+ document.getElementById('node-slider').addEventListener('input', (e) => {{
553
+ debouncedUpdateNodes(parseInt(e.target.value));
554
+ }});
555
+
556
+ map.on('click', (e) => {{
557
+ if (!e.originalEvent.target.closest('.leaflet-marker-icon')) {{
558
+ selectedNode = null;
559
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
560
+ }}
561
+ }});
562
+
563
+ map.on('moveend zoomend', () => {{
564
+ debouncedUpdateNodes(parseInt(document.getElementById('node-slider').value));
565
+ }});
566
+
567
+ // Initial update
568
+ debouncedUpdateNodes({effective_max_nodes});
569
+ </script>
570
+ </body>
571
+ </html>
572
+ """
573
+
574
+ with open(output_html_file, "w", encoding="utf-8") as f:
575
+ f.write(html_content)
576
+ print(f"Generated {output_html_file} successfully!")
577
+ import webbrowser
578
+ webbrowser.open(output_html_file)
579
+
580
+ if __name__ == "__main__":
581
+ cities = [
582
+ {"node": "rovaniemi,finland"},
583
+ {
584
+ 'node': 'eventti.net',
585
+ 'lat': 60.1497055,
586
+ 'lon': 24.7376876,
587
+ 'size': 10,
588
+ 'color': 'blue',
589
+ 'shape': 'circle',
590
+ 'node_hover': 'eventti.net'
591
+ },
592
+ {
593
+ "node": "New York", "lat": 40.7128, "lon": -74.0060, "size": 15,
594
+ "color": "red", "shape": "circle", "node_hover": "The Big Apple, USA"
595
+ },
596
+ {
597
+ "node": "London", "lat": 51.5074, "lon": -0.1278, "size": 12,
598
+ "color": "blue", "shape": "star", "node_hover": "Capital of the UK"
599
+ },
600
+ {
601
+ "node": "Tokyo", "lat": 35.6762, "lon": 139.6503, "size": 10,
602
+ "color": "green", "shape": "square", "node_hover": "Capital of Japan"
603
+ },
604
+ {
605
+ "node": "Sydney", "lat": -33.8688, "lon": 151.2093, "size": 8,
606
+ "color": "purple", "shape": "circle", "node_hover": "Australia’s coastal gem"
607
+ },
608
+ {
609
+ "node": "Paris", "lat": 48.8566, "lon": 2.3522, "size": 11,
610
+ "color": "orange", "shape": "star", "node_hover": "City of Love"
611
+ },
612
+ ]
613
+
614
+ connections = [
615
+ {
616
+ "node1": "rovaniemi,finland", "node2": "eventti.net"
617
+ },
618
+ {
619
+ "node1": "New York", "node2": "London", "color": "black", "width": 2,
620
+ "style": "solid", "arrow": True, "curve": False,
621
+ "edge_hover": "Flight from NY to London"
622
+ },
623
+ {
624
+ "node1": "Tokyo", "node2": "London", "color": "black", "width": 2,
625
+ "style": "solid", "arrow": True, "curve": False,
626
+ "edge_hover": "Flight tokyo to London"
627
+ },
628
+ {
629
+ "node1": "London", "node2": "Tokyo", "color": "blue", "width": 3,
630
+ "style": "solid", "arrow": True, "curve": False,
631
+ "edge_hover": "From London to Tokyo"
632
+ },
633
+ {
634
+ "node1": "Tokyo", "node2": "rovaniemi,finland", "color": "red", "width": 10,
635
+ "style": "dashed", "arrow": True, "curve": True,
636
+ "edge_hover": "From Tokyo to Rovaniemi"
637
+ },
638
+ {
639
+ "node1": "Tokyo", "node2": "Sydney", "color": "blue", "width": 1,
640
+ "style": "solid", "arrow": True, "curve": False,
641
+ "edge_hover": "Trade route Tokyo ↔ Sydney"
642
+ },
643
+ {
644
+ "node1": "Sydney", "node2": "New York", "color": "green", "width": 12,
645
+ "style": "solid", "arrow": True, "curve": True,
646
+ "edge_hover": "Tourist route Sydney–NY"
647
+ },
648
+ {
649
+ "node1": "London", "node2": "Paris", "color": "brown", "width": 4,
650
+ "style": "solid", "arrow": True, "curve": False,
651
+ "edge_hover": "Eurostar connection"
652
+ },
653
+ {
654
+ "node1": "New York", "node2": "Paris", "color": "darkblue", "width": 2,
655
+ "style": "dashed", "arrow": False, "curve": False,
656
+ "edge_hover": "Transatlantic flight"
657
+ },
658
+ ]
659
+
660
+ netmap(cities, connections, title="net map", output_html_file='simple.html', maximum_nodes=2)