"""Network topology visualization for infrastructure systems."""
import folium
import networkx as nx
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from folium import plugins
[docs]
class NetworkVisualizer:
"""Visualize infrastructure networks and topologies."""
[docs]
def __init__(self):
self.default_colors = px.colors.qualitative.Set1
self.state_colors = {
0: "red", # Failed
1: "orange", # Critical
2: "orange",
3: "yellow", # Poor
4: "yellow",
5: "lightgreen", # Fair
6: "lightgreen",
7: "green", # Good
8: "green",
9: "darkgreen", # Excellent
10: "darkgreen",
}
[docs]
def create_network_graph(
self,
components: list[dict],
connections: list[tuple] = None,
layout: str = "spring",
) -> go.Figure:
"""Create network graph visualization of infrastructure components."""
G = nx.Graph()
# Add nodes
for comp in components:
comp_id = comp.get("id", 0)
G.add_node(comp_id, **comp)
# Add edges if provided
if connections:
G.add_edges_from(connections)
# Calculate layout
if layout == "spring":
pos = nx.spring_layout(G, k=1, iterations=50)
elif layout == "circular":
pos = nx.circular_layout(G)
elif layout == "random":
pos = nx.random_layout(G)
else:
pos = nx.spring_layout(G)
# Extract node positions
node_x = [pos[node][0] for node in G.nodes()]
node_y = [pos[node][1] for node in G.nodes()]
# Node colors based on state
node_colors = []
node_text = []
for node in G.nodes():
state = G.nodes[node].get("state", 10)
node_colors.append(self.state_colors.get(state, "gray"))
# Create hover text
node_info = G.nodes[node]
text = f"ID: {node}<br>"
text += f"State: {state}<br>"
if "name" in node_info:
text += f"Name: {node_info['name']}<br>"
if "type" in node_info:
text += f"Type: {node_info['type']}<br>"
if "importance" in node_info:
text += f"Importance: {node_info['importance']:.2f}<br>"
node_text.append(text)
# Create edge traces
edge_x = []
edge_y = []
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.extend([x0, x1, None])
edge_y.extend([y0, y1, None])
# Create figure
fig = go.Figure()
# Add edges
fig.add_trace(
go.Scatter(
x=edge_x,
y=edge_y,
mode="lines",
line=dict(width=2, color="lightgray"),
hoverinfo="none",
showlegend=False,
)
)
# Add nodes
fig.add_trace(
go.Scatter(
x=node_x,
y=node_y,
mode="markers+text",
marker=dict(
size=20,
color=node_colors,
line=dict(width=2, color="black"),
opacity=0.8,
),
text=[str(node) for node in G.nodes()],
textposition="middle center",
hovertext=node_text,
hoverinfo="text",
showlegend=False,
)
)
fig.update_layout(
title="Infrastructure Network Topology",
showlegend=False,
hovermode="closest",
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
template="plotly_white",
)
return fig
[docs]
def create_geographic_map(
self,
components: list[dict],
center_lat: float = 52.5,
center_lon: float = 13.4,
zoom: int = 10,
) -> folium.Map:
"""Create geographic map with infrastructure components."""
# Create base map
m = folium.Map(
location=[center_lat, center_lon], zoom_start=zoom, tiles="OpenStreetMap"
)
# Add components as markers
for comp in components:
if "coordinates" in comp and comp["coordinates"]:
lat, lon = comp["coordinates"][:2]
# Determine marker color based on state
state = comp.get("state", 10)
color = self._get_folium_color(state)
# Create popup text
comp_id = comp.get("id", "?")
comp_name = comp.get("name", f"Component {comp_id}")
popup_text = f"<b>{comp_name}</b><br>"
popup_text += f"State: {state}/10<br>"
popup_text += f"Type: {comp.get('type', 'Unknown')}<br>"
if "importance" in comp:
popup_text += f"Importance: {comp['importance']:.2f}<br>"
if "criticality" in comp:
popup_text += f"Criticality: {comp['criticality']}<br>"
# Add marker
folium.CircleMarker(
location=[lat, lon],
radius=8,
popup=folium.Popup(popup_text, max_width=300),
color="black",
fillColor=color,
fillOpacity=0.8,
weight=2,
).add_to(m)
# Add legend
legend_html = self._create_map_legend()
m.get_root().html.add_child(folium.Element(legend_html))
return m
[docs]
def create_cascade_visualization(
self,
components: list[dict],
cascade_steps: list[dict],
connections: list[tuple] = None,
) -> list[go.Figure]:
"""Create animation frames showing cascade failure propagation."""
frames = []
for step_idx, step_data in enumerate(cascade_steps):
# Update component states for this step
updated_components = []
for comp in components:
comp_copy = comp.copy()
comp_id = comp["id"]
if comp_id in step_data.get("failed_components", []):
comp_copy["state"] = 0 # Failed
elif comp_id in step_data.get("degraded_components", []):
comp_copy["state"] = max(1, comp_copy.get("state", 10) - 2)
updated_components.append(comp_copy)
# Create network graph for this step
fig = self.create_network_graph(
updated_components, connections, layout="spring"
)
fig.update_layout(
title=f"Cascade Step {step_idx + 1}: {step_data.get('description', '')}"
)
frames.append(fig)
return frames
[docs]
def create_traffic_flow_map(
self, road_network: dict, traffic_data: dict = None
) -> folium.Map:
"""Create map showing traffic flow on road network."""
if not road_network.get("nodes") or not road_network.get("edges"):
return folium.Map(location=[52.5, 13.4], zoom_start=10)
# Calculate map center
lats = [node["y"] for node in road_network["nodes"].values()]
lons = [node["x"] for node in road_network["nodes"].values()]
center_lat = np.mean(lats)
center_lon = np.mean(lons)
m = folium.Map(
location=[center_lat, center_lon], zoom_start=12, tiles="OpenStreetMap"
)
# Add road segments
for edge_id, edge_data in road_network["edges"].items():
start_node = road_network["nodes"][edge_data["start"]]
end_node = road_network["nodes"][edge_data["end"]]
# Determine road color and width based on traffic
if traffic_data and edge_id in traffic_data:
traffic_level = traffic_data[edge_id].get("volume", 0)
color = self._get_traffic_color(traffic_level)
weight = max(2, min(8, traffic_level / 1000))
else:
color = "blue"
weight = 3
# Add road segment
folium.PolyLine(
locations=[
[start_node["y"], start_node["x"]],
[end_node["y"], end_node["x"]],
],
color=color,
weight=weight,
opacity=0.8,
popup=f"Road: {edge_data.get('name', edge_id)}<br>Traffic: {traffic_data.get(edge_id, {}).get('volume', 'Unknown')}",
).add_to(m)
# Add traffic legend
traffic_legend = self._create_traffic_legend()
m.get_root().html.add_child(folium.Element(traffic_legend))
return m
[docs]
def create_heatmap(
self,
components: list[dict],
value_field: str = "state",
center_lat: float = 52.5,
center_lon: float = 13.4,
) -> folium.Map:
"""Create heatmap of component values."""
m = folium.Map(
location=[center_lat, center_lon], zoom_start=10, tiles="OpenStreetMap"
)
# Prepare heatmap data
heat_data = []
for comp in components:
if "coordinates" in comp and comp["coordinates"]:
lat, lon = comp["coordinates"][:2]
value = comp.get(value_field, 0)
heat_data.append([lat, lon, value])
if heat_data:
# Add heatmap layer
plugins.HeatMap(
heat_data,
min_opacity=0.2,
max_val=10,
radius=20,
blur=15,
gradient={0.2: "blue", 0.4: "lime", 0.6: "orange", 1: "red"},
).add_to(m)
return m
def _get_folium_color(self, state: int) -> str:
"""Get folium marker color based on component state."""
if state == 0:
return "red"
elif state <= 2:
return "orange"
elif state <= 4:
return "yellow"
elif state <= 6:
return "lightgreen"
else:
return "green"
def _get_traffic_color(self, volume: int) -> str:
"""Get color based on traffic volume."""
if volume >= 5000:
return "red"
elif volume >= 3000:
return "orange"
elif volume >= 1000:
return "yellow"
else:
return "green"
def _create_map_legend(self) -> str:
"""Create HTML legend for component state map."""
return """
<div style="position: fixed;
bottom: 50px; left: 50px; width: 150px; height: 120px;
background-color: white; border:2px solid grey; z-index:9999;
font-size:14px; padding: 10px">
<h4>Component State</h4>
<p><i class="fa fa-circle" style="color:green"></i> Excellent (7-10)</p>
<p><i class="fa fa-circle" style="color:lightgreen"></i> Good (5-6)</p>
<p><i class="fa fa-circle" style="color:yellow"></i> Poor (3-4)</p>
<p><i class="fa fa-circle" style="color:orange"></i> Critical (1-2)</p>
<p><i class="fa fa-circle" style="color:red"></i> Failed (0)</p>
</div>
"""
def _create_traffic_legend(self) -> str:
"""Create HTML legend for traffic flow map."""
return """
<div style="position: fixed;
bottom: 50px; left: 50px; width: 150px; height: 100px;
background-color: white; border:2px solid grey; z-index:9999;
font-size:14px; padding: 10px">
<h4>Traffic Volume</h4>
<p><i class="fa fa-minus" style="color:red"></i> Heavy (5000+)</p>
<p><i class="fa fa-minus" style="color:orange"></i> Moderate (3000+)</p>
<p><i class="fa fa-minus" style="color:yellow"></i> Light (1000+)</p>
<p><i class="fa fa-minus" style="color:green"></i> Low (<1000)</p>
</div>
"""