import re
import warnings
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
import networkx as nx
import plotly.graph_objects as go
from npap._logging import LogCategory, log_info
from npap.interfaces import EdgeType
#: Filename used for the exported HTML when no title or filename is available.
DEFAULT_HTML_FILENAME = "npap_network.html"
#: Plotly figure config applied to every rendered/exported figure.
_FIGURE_CONFIG = {"scrollZoom": True}
def _resolve_deprecated_show(show: bool | None, renderer: str | None) -> str | None:
"""
Map the deprecated `show` flag onto the `renderer` argument.
Parameters
----------
show : bool or None
Legacy flag. `None` means the caller did not pass it.
renderer : str or None
Renderer explicitly requested by the caller, which always wins.
Returns
-------
str or None
Effective renderer to use.
"""
if show is None:
return renderer
warnings.warn(
"The 'show' argument of NPAP plotting functions is deprecated and will "
"be removed in a future release. Use renderer='browser' to display the "
"figure in a browser, or renderer=None (the default) to only return it.",
DeprecationWarning,
stacklevel=3,
)
if renderer is not None:
return renderer
return "browser" if show else None
def _in_notebook() -> bool:
"""
Detect whether NPAP is running inside a Jupyter notebook kernel.
Returns
-------
bool
True when executed inside a Jupyter/JupyterLab kernel, False in plain
scripts, IPython terminals and test runners.
"""
try:
from IPython import get_ipython
except ImportError:
return False
shell = get_ipython()
if shell is None:
return False
return type(shell).__name__ == "ZMQInteractiveShell"
def _slugify(text: str) -> str:
"""
Convert a plot title into a filesystem-safe file stem.
Parameters
----------
text : str
Arbitrary title text.
Returns
-------
str
Lowercase slug containing only alphanumerics, dashes and underscores.
"""
slug = re.sub(r"[^\w\-]+", "_", text, flags=re.UNICODE).strip("_").lower()
return slug or Path(DEFAULT_HTML_FILENAME).stem
[docs]
def resolve_html_path(
output_dir: str | Path | None = None,
filename: str | None = None,
title: str | None = None,
) -> Path:
"""
Determine the destination path of the exported HTML figure.
Parameters
----------
output_dir : str or Path or None
Target directory. Defaults to the current working directory.
filename : str or None
Explicit file name. Defaults to a slug of `title`, or
`DEFAULT_HTML_FILENAME` when no title is set.
title : str or None
Plot title used to derive the file name.
Returns
-------
Path
Absolute path the HTML figure will be written to.
"""
directory = Path(output_dir) if output_dir is not None else Path.cwd()
if filename is None:
filename = f"{_slugify(title)}.html" if title else DEFAULT_HTML_FILENAME
elif not filename.lower().endswith(".html"):
filename = f"{filename}.html"
return (directory / filename).resolve()
[docs]
class PlotStyle(Enum):
"""
Define available visualization styles for power networks.
Attributes
----------
SIMPLE : str
All edges rendered uniformly (fastest, minimal visual complexity).
VOLTAGE_AWARE : str
Edges colored by type and voltage level (recommended for power systems).
CLUSTERED : str
Nodes colored by cluster assignment (requires prior partitioning).
"""
SIMPLE = "simple"
VOLTAGE_AWARE = "voltage_aware"
CLUSTERED = "clustered"
[docs]
@dataclass
class PlotConfig:
"""
Configure network visualization parameters.
This centralized configuration allows users to customize every aspect of the
visualization without modifying code. All parameters have sensible defaults
optimized for power system networks.
Attributes
----------
show_lines : bool
Whether to display transmission lines.
show_trafos : bool
Whether to display transformers.
show_dc_links : bool
Whether to display DC links.
show_nodes : bool
Whether to display nodes/buses.
line_voltage_threshold : float
Voltage threshold (kV) for high/low voltage line classification.
line_high_voltage_color : str
Hex color for high voltage lines.
line_low_voltage_color : str
Hex color for low voltage lines.
trafo_color : str
Hex color for transformers.
dc_link_color : str
Hex color for DC links.
edge_color : str
Hex color for edges with an unrecognised type.
node_color : str
Hex color for nodes/buses.
edge_width : float
Width of edge lines in pixels.
node_size : int
Size of node markers in pixels.
map_style : str
MapLibre style for the base map.
map_center_lat : float
Initial map center latitude.
map_center_lon : float
Initial map center longitude.
map_zoom : float
Initial map zoom level.
title : str or None
Figure title.
width : int or None
Figure width in pixels.
height : int or None
Figure height in pixels.
cluster_colorscale : str
Plotly colorscale for cluster coloring.
"""
show_lines: bool = True
show_trafos: bool = True
show_dc_links: bool = True
show_nodes: bool = True
line_voltage_threshold: float = 300.0
line_high_voltage_color: str = "#029E73"
line_low_voltage_color: str = "#CA9161"
trafo_color: str = "#ECE133"
dc_link_color: str = "#CC78BC"
edge_color: str = "#949494"
node_color: str = "#0173B2"
edge_width: float = 1.5
node_size: int = 5
map_style: str = "carto-positron"
map_center_lat: float = 57.5
map_center_lon: float = 14.0
map_zoom: float = 3.7
title: str | None = None
width: int | None = None
height: int | None = None
cluster_colorscale: str = "Viridis"
@dataclass
class EdgeGroup:
"""
Store aggregated edge coordinates by type/voltage category.
This grouping strategy is crucial for performance: instead of creating
separate Plotly traces for each edge (which would be O(n) traces for n edges),
we create one trace per group (typically 4-5 traces total).
Attributes
----------
edge_type : str
Type of edge ('line', 'trafo', or 'dc_link').
voltage_category : str
Voltage category ('high', 'low', 'trafo', or 'dc_link').
lats : list[float or None]
List of latitude coordinates with None separators.
lons : list[float or None]
List of longitude coordinates with None separators.
count : int
Number of edges in this group.
"""
edge_type: str
voltage_category: str
lats: list[float | None] = field(default_factory=list)
lons: list[float | None] = field(default_factory=list)
count: int = 0
class EdgeStyleRegistry:
"""
Provide centralized registry for edge styling rules.
This class eliminates code duplication by providing a single source of truth
for edge colors, display names, and rendering order. Adding new edge categories
only requires updating this registry, not multiple methods.
"""
# Display order determines legend and visual layering
DISPLAY_ORDER = ["line_high", "line_low", "trafo", "dc_link", "edge"]
@staticmethod
def get_color(group_key: str, config: PlotConfig) -> str:
"""
Map group keys to colors from configuration.
Parameters
----------
group_key : str
Identifier like "line_high", "trafo", etc.
config : PlotConfig
Plot configuration with color definitions.
Returns
-------
str
Hex color string.
"""
color_map = {
"line_high": config.line_high_voltage_color,
"line_low": config.line_low_voltage_color,
"trafo": config.trafo_color,
"dc_link": config.dc_link_color,
"edge": config.edge_color,
}
return color_map.get(group_key, config.line_low_voltage_color)
@staticmethod
def get_display_name(group_key: str, config: PlotConfig) -> str:
"""
Map group keys to human-readable display names for the legend.
Parameters
----------
group_key : str
Identifier like "line_high", "trafo", etc.
config : PlotConfig
Plot configuration with threshold values.
Returns
-------
str
Display name string for legend.
"""
threshold = int(config.line_voltage_threshold)
names = {
"line_high": f"High Voltage Lines >{threshold}kV",
"line_low": f"Low Voltage Lines ≤{threshold}kV",
"trafo": "Transformers",
"dc_link": "DC Links",
"edge": "Edges",
}
return names.get(group_key, "Edges")
[docs]
class NetworkPlotter:
"""
Create high-performance interactive visualizations for power networks.
This class handles the complete visualization pipeline:
1. Data preparation (coordinate extraction, edge grouping)
2. Trace generation (edges, nodes)
3. Figure assembly (layout, styling)
Parameters
----------
graph : nx.DiGraph
NetworkX DiGraph with geographical coordinates (lat, lon).
partition_map : dict[int, list[Any]] or None
Optional mapping of cluster_id to node_ids for cluster visualization.
Examples
--------
>>> plotter = NetworkPlotter(graph, partition_map=partition.mapping)
>>> fig = plotter.plot_clustered()
"""
[docs]
def __init__(self, graph: nx.DiGraph, partition_map: dict[int, list[Any]] | None = None):
"""
Initialize plotter with network data.
Parameters
----------
graph : nx.DiGraph
NetworkX DiGraph with geographical coordinates (lat, lon).
partition_map : dict[int, list[Any]] or None
Optional mapping of cluster_id to node_ids for cluster visualization.
"""
self._graph = graph
self._partition_map = partition_map
# Pre-compute reverse mapping for O(1) cluster lookups during node coloring
self._node_to_cluster = self._build_node_to_cluster_map() if partition_map else None
# Cache all coordinates once for performance (avoids repeated dict lookups)
self._node_coords = self._extract_node_coordinates()
def _extract_node_coordinates(self) -> dict[Any, tuple[float, float]]:
"""
Extract and cache all node coordinates for fast lookup.
This caching strategy is critical for performance: with 6000 nodes and
8000 edges, we'd perform 16,000+ coordinate lookups during edge rendering.
By caching, we reduce this to a single O(n) pass at initialization.
Returns
-------
dict[Any, tuple[float, float]]
Dictionary mapping node_id to (lat, lon) tuple.
"""
coords = {}
for node, data in self._graph.nodes(data=True):
lat = data.get("lat")
lon = data.get("lon")
# Only cache nodes with valid coordinates (skip nodes without geolocation)
if lat is not None and lon is not None:
coords[node] = (lat, lon)
return coords
def _build_node_to_cluster_map(self) -> dict[Any, int]:
"""
Create reverse mapping from node_id to cluster_id.
The partition_map is cluster_id -> [node_ids]. For efficient node coloring,
we need the inverse: node_id -> cluster_id. This O(n) conversion at
initialization enables O(1) lookups during visualization.
Returns
-------
dict[Any, int]
Dictionary mapping node_id to cluster_id.
"""
if not self._partition_map:
return {}
node_to_cluster = {}
for cluster_id, nodes in self._partition_map.items():
for node in nodes:
node_to_cluster[node] = cluster_id
return node_to_cluster
@staticmethod
def _should_show_edge_type(edge_type: str, config: PlotConfig) -> bool:
"""
Determine if an edge type should be displayed based on configuration.
Parameters
----------
edge_type : str
Type of edge ('line', 'trafo', 'dc_link').
config : PlotConfig
Plot configuration with display toggles.
Returns
-------
bool
True if this edge type should be displayed.
"""
visibility_map = {
EdgeType.LINE.value: config.show_lines,
EdgeType.TRAFO.value: config.show_trafos,
EdgeType.DC_LINK.value: config.show_dc_links,
}
return visibility_map.get(edge_type, True)
@staticmethod
def _categorize_edge(edge_data: dict, config: PlotConfig) -> tuple[str, str]:
"""
Determine edge type and voltage category for grouping.
This method implements the classification logic:
- Lines are split by voltage threshold (transmission vs distribution)
- Transformers and DC links have their own categories
Parameters
----------
edge_data : dict
Edge attributes dictionary.
config : PlotConfig
Plot configuration with voltage threshold.
Returns
-------
tuple[str, str]
Tuple of (group_key, voltage_category).
"""
edge_type = edge_data.get("type", EdgeType.LINE.value)
if edge_type == EdgeType.LINE.value:
# Classify lines by voltage: high voltage (transmission) vs low voltage
primary_v = edge_data.get("primary_voltage", 0) or 0
voltage_category = "high" if primary_v > config.line_voltage_threshold else "low"
group_key = f"line_{voltage_category}"
elif edge_type == EdgeType.TRAFO.value:
voltage_category = "trafo"
group_key = "trafo"
elif edge_type == EdgeType.DC_LINK.value:
voltage_category = "dc_link"
group_key = "dc_link"
else:
voltage_category = "edge"
group_key = "edge"
return group_key, voltage_category
def _group_edges_by_type(self, config: PlotConfig) -> dict[str, EdgeGroup]:
"""
Group all edges by type and voltage category for efficient rendering.
This is the core performance optimization: instead of creating one Plotly
trace per edge (which would create thousands of traces), we group edges
by category and create one trace per group.
The grouping uses Plotly's None-separation technique: coordinates are
arranged as [x1, x2, None, x3, x4, None, ...] where None breaks the line.
This allows thousands of line segments in a single trace.
Parameters
----------
config : PlotConfig
Plot configuration with display toggles and thresholds.
Returns
-------
dict[str, EdgeGroup]
Dictionary mapping group_key to EdgeGroup with aggregated coordinates.
"""
groups: dict[str, EdgeGroup] = {}
for u, v, data in self._graph.edges(data=True):
# Skip edges with missing node coordinates (shouldn't happen with valid data)
if u not in self._node_coords or v not in self._node_coords:
continue
edge_type = data.get("type", EdgeType.LINE.value)
# Respect display toggles from configuration
if not self._should_show_edge_type(edge_type, config):
continue
# Classify edge into a group
group_key, voltage_category = self._categorize_edge(data, config)
# Create group on first encounter
if group_key not in groups:
groups[group_key] = EdgeGroup(
edge_type=edge_type, voltage_category=voltage_category
)
# Add edge coordinates to the group using None-separation
# Format: [lon1, lon2, None, lon3, lon4, None, ...]
# The None values tell Plotly to break the line, creating separate segments
lat_u, lon_u = self._node_coords[u]
lat_v, lon_v = self._node_coords[v]
group = groups[group_key]
group.lons.extend([lon_u, lon_v, None])
group.lats.extend([lat_u, lat_v, None])
group.count += 1
return groups
def _build_edge_traces_voltage_aware(self, config: PlotConfig) -> list[go.Scattermap]:
"""
Build edge traces grouped by type and voltage category.
This method creates separate Plotly traces for each edge group,
allowing users to toggle edge types in the legend and providing
visual differentiation by color.
Parameters
----------
config : PlotConfig
Plot configuration with colors and styling.
Returns
-------
list[go.Scattermap]
List of Scattermap traces for edges.
"""
groups = self._group_edges_by_type(config)
traces = []
# Process groups in defined order for consistent legend appearance
for group_key in EdgeStyleRegistry.DISPLAY_ORDER:
if group_key not in groups:
continue
group = groups[group_key]
color = EdgeStyleRegistry.get_color(group_key, config)
name = EdgeStyleRegistry.get_display_name(group_key, config)
trace = go.Scattermap(
lon=group.lons,
lat=group.lats,
mode="lines",
line=dict(width=config.edge_width, color=color),
name=f"{name} ({group.count})", # Include count for user insight
hoverinfo="name",
legendgroup=group_key,
)
traces.append(trace)
return traces
def _build_edge_traces_simple(self, config: PlotConfig) -> list[go.Scattermap]:
"""
Build a single edge trace with uniform styling.
This is the fastest rendering option: all edges in one trace with one color.
Useful for initial network overview or when edge differentiation isn't needed.
Parameters
----------
config : PlotConfig
Plot configuration with basic styling.
Returns
-------
list[go.Scattermap]
List containing single edge trace (or empty if no valid edges).
"""
lons = []
lats = []
# Aggregate all edges into a single coordinate list with None-separation
for u, v in self._graph.edges():
if u not in self._node_coords or v not in self._node_coords:
continue
lat_u, lon_u = self._node_coords[u]
lat_v, lon_v = self._node_coords[v]
lons.extend([lon_u, lon_v, None])
lats.extend([lat_u, lat_v, None])
if not lons:
return []
trace = go.Scattermap(
lon=lons,
lat=lats,
mode="lines",
line=dict(width=config.edge_width, color="#888"),
hoverinfo="none",
name="Transmission Lines",
)
return [trace]
def _build_node_trace(
self, config: PlotConfig, color_by_cluster: bool = False
) -> go.Scattermap | None:
"""
Build node trace with optional cluster-based coloring.
Nodes are rendered as markers on the map. When color_by_cluster is True,
each cluster gets a distinct color from the colorscale, making it easy
to visualize the partitioning results.
Parameters
----------
config : PlotConfig
Plot configuration with node styling.
color_by_cluster : bool
If True, color nodes by cluster assignment.
Returns
-------
go.Scattermap or None
Scattermap trace for nodes, or None if nodes are hidden.
"""
if not config.show_nodes:
return None
node_lons = []
node_lats = []
hover_texts = []
colors = []
for node, data in self._graph.nodes(data=True):
if node not in self._node_coords:
continue
lat, lon = self._node_coords[node]
node_lons.append(lon)
node_lats.append(lat)
# Format voltage information for hover text
voltage = data.get("voltage") or data.get("base_voltage", "N/A")
voltage_str = f"{voltage:.2f} kV" if isinstance(voltage, (int, float)) else str(voltage)
hover_texts.append(f"<b>{node}</b><br>Voltage: {voltage_str}")
# Assign color based on cluster or use uniform color
if color_by_cluster and self._node_to_cluster:
cluster_id = self._node_to_cluster.get(node, -1)
colors.append(cluster_id)
else:
colors.append(config.node_color)
if not node_lons:
return None
# Configure marker appearance based on coloring mode
if color_by_cluster and self._node_to_cluster:
# Cluster coloring: use colorscale with legend
marker = dict(
size=config.node_size,
color=colors,
colorscale=config.cluster_colorscale,
showscale=True,
colorbar=dict(
title="Clusters",
title_font=dict(color="#008080", size=16, family="Arial, sans-serif"),
tickfont=dict(color="#008080", size=14, family="Arial, sans-serif"),
x=0.99,
xanchor="right",
y=0.5,
yanchor="middle",
len=0.9,
),
)
else:
# Uniform coloring: single color for all nodes
marker = dict(size=config.node_size, color=config.node_color)
trace = go.Scattermap(
lon=node_lons,
lat=node_lats,
mode="markers",
marker=marker,
hoverinfo="text",
text=hover_texts,
name=f"Buses ({len(node_lons)})",
legendgroup="nodes",
)
return trace
@staticmethod
def _create_figure(traces: list[go.Scattermap], config: PlotConfig) -> go.Figure:
"""
Assemble Plotly figure with all traces and layout configuration.
This method handles the final assembly: combining edge and node traces,
configuring the map layout, and setting up the legend for optimal
user interaction.
Parameters
----------
traces : list[go.Scattermap]
List of all Scattermap traces (edges + nodes).
config : PlotConfig
Plot configuration with layout settings.
Returns
-------
go.Figure
Complete Plotly Figure ready for display.
"""
fig = go.Figure(data=traces)
layout_kwargs = dict(
# Title configuration
title_text=config.title or "Power Network",
title_font=dict(color="white", size=20, family="Arial, sans-serif"),
title_y=0.994,
title_x=0.5,
title_xanchor="center",
paper_bgcolor="#008080",
hovermode="closest",
showlegend=True,
legend=dict(
# Position legend in top-left with semi-transparent background
yanchor="top",
y=0.99,
xanchor="left",
x=0.01,
bgcolor="rgba(255, 255, 255, 0.95)",
bordercolor="#008080",
borderwidth=1,
font=dict(size=11),
itemsizing="constant",
tracegroupgap=5,
),
map=dict(
style=config.map_style,
bearing=0,
center=dict(lat=config.map_center_lat, lon=config.map_center_lon),
pitch=0,
zoom=config.map_zoom,
),
margin=dict(r=0, t=30, l=0, b=0),
)
# Apply optional dimension overrides
if config.width:
layout_kwargs["width"] = config.width
if config.height:
layout_kwargs["height"] = config.height
fig.update_layout(**layout_kwargs)
return fig
def _plot(
self,
style: PlotStyle,
config: PlotConfig | None = None,
*,
renderer: str | None = None,
save_html: bool | None = None,
output_dir: str | Path | None = None,
filename: str | None = None,
show: bool | None = None,
) -> go.Figure:
"""
Execute centralized plotting for all visualization styles.
This method consolidates the logic from plot_simple, plot_voltage_aware,
and plot_clustered into a single implementation, eliminating code duplication
while maintaining the distinct visual styles.
Parameters
----------
style : PlotStyle
Visualization style (SIMPLE, VOLTAGE_AWARE, or CLUSTERED).
config : PlotConfig or None
Optional plot configuration (uses defaults if not provided).
renderer : str or None
Plotly renderer used to display the figure. See :func:`deliver_figure`.
save_html : bool or None
Whether to export a standalone HTML file. `None` exports only
outside Jupyter kernels.
output_dir : str or Path or None
Directory for the exported HTML. Defaults to the working directory.
filename : str or None
File name for the exported HTML.
show : bool or None
Deprecated. Use `renderer` instead.
Returns
-------
go.Figure
Plotly Figure object.
Raises
------
ValueError
If CLUSTERED style requested without partition_map.
"""
renderer = _resolve_deprecated_show(show, renderer)
config = config or PlotConfig()
# Validate prerequisites for clustered visualization
if style == PlotStyle.CLUSTERED and not self._partition_map:
raise ValueError(
"Cannot create clustered plot without partition_map. "
"Provide partition_map during NetworkPlotter initialization."
)
# Build edge traces based on style
if style == PlotStyle.SIMPLE:
edge_traces = self._build_edge_traces_simple(config)
else: # VOLTAGE_AWARE or CLUSTERED both use voltage-aware edges
edge_traces = self._build_edge_traces_voltage_aware(config)
# Build node trace with optional cluster coloring
color_by_cluster = style == PlotStyle.CLUSTERED
node_trace = self._build_node_trace(config, color_by_cluster=color_by_cluster)
# Assemble all traces
traces = edge_traces
if node_trace:
traces.append(node_trace)
# Create figure
fig = self._create_figure(traces, config)
# Export to HTML and/or display, without touching global Plotly state
deliver_figure(
fig,
renderer=renderer,
save_html=save_html,
output_dir=output_dir,
filename=filename,
title=config.title,
)
return fig
# Public API methods - these provide clean interfaces for each style
[docs]
def plot_simple(self, config: PlotConfig | None = None, **output_kwargs: Any) -> go.Figure:
"""
Create simple visualization with uniform edge styling.
Best for: Initial network overview, maximum performance.
Parameters
----------
config : PlotConfig or None
Optional plot configuration.
**output_kwargs : dict
Output options forwarded to :func:`deliver_figure` (`renderer`,
`save_html`, `output_dir`, `filename`).
Returns
-------
go.Figure
Plotly Figure object.
"""
return self._plot(PlotStyle.SIMPLE, config, **output_kwargs)
[docs]
def plot_voltage_aware(
self, config: PlotConfig | None = None, **output_kwargs: Any
) -> go.Figure:
"""
Create voltage-aware visualization with edges colored by type and voltage.
Best for: Understanding network structure, distinguishing transmission levels.
Parameters
----------
config : PlotConfig or None
Optional plot configuration.
**output_kwargs : dict
Output options forwarded to :func:`deliver_figure` (`renderer`,
`save_html`, `output_dir`, `filename`).
Returns
-------
go.Figure
Plotly Figure object.
"""
return self._plot(PlotStyle.VOLTAGE_AWARE, config, **output_kwargs)
[docs]
def plot_clustered(self, config: PlotConfig | None = None, **output_kwargs: Any) -> go.Figure:
"""
Create clustered visualization with nodes colored by cluster assignment.
Best for: Visualizing partitioning results, understanding cluster geography.
Parameters
----------
config : PlotConfig or None
Optional plot configuration.
**output_kwargs : dict
Output options forwarded to :func:`deliver_figure` (`renderer`,
`save_html`, `output_dir`, `filename`).
Returns
-------
go.Figure
Plotly Figure object.
Raises
------
ValueError
If partition_map was not provided during initialization.
"""
return self._plot(PlotStyle.CLUSTERED, config, **output_kwargs)
[docs]
def plot_network(
graph: nx.DiGraph,
style: str = "simple",
partition_map: dict[int, list[Any]] | None = None,
*,
renderer: str | None = None,
save_html: bool | None = None,
output_dir: str | Path | None = None,
filename: str | None = None,
show: bool | None = None,
config: PlotConfig | None = None,
**kwargs,
) -> go.Figure:
"""
Create a quick network visualization.
This function provides a simple interface for one-line plotting without
needing to instantiate the NetworkPlotter class explicitly.
The figure is always returned. Outside Jupyter it is additionally exported
as a standalone HTML file; inside a notebook nothing is written to disk so
the returned figure renders inline. Pass `renderer` to display it.
Parameters
----------
graph : nx.DiGraph
NetworkX DiGraph with geographical coordinates (lat, lon).
style : str
Visualization style ('simple', 'voltage_aware', or 'clustered').
partition_map : dict[int, list[Any]] or None
Optional cluster mapping for 'clustered' style.
renderer : str or None
Plotly renderer used to display the figure. `None` (default) does not
display anything. `"auto"` lets Plotly choose based on the environment.
Any other value (e.g. `"browser"`) is forwarded to Plotly as-is.
save_html : bool or None
Whether to export a standalone HTML file. `None` (default) exports
only when running outside a Jupyter kernel.
output_dir : str or Path or None
Directory for the exported HTML. Defaults to the working directory.
filename : str or None
File name for the exported HTML. Defaults to a slug of the title.
show : bool or None
Deprecated. `show=True` maps to `renderer="browser"` and `show=False`
to `renderer=None`. Use `renderer` directly instead.
config : PlotConfig or None
Optional PlotConfig instance to override defaults. If provided,
kwargs will further override values from this config.
**kwargs : dict
Additional configuration parameters passed to PlotConfig.
These override both defaults and any provided config values.
Returns
-------
go.Figure
Plotly Figure object.
Raises
------
ValueError
If style is unknown or prerequisites are missing.
Examples
--------
>>> from npap.visualization import plot_network
>>> fig = plot_network(graph, style="voltage_aware", title="My Network")
>>> fig = plot_network(graph, style="clustered", partition_map=result.mapping)
>>> fig = plot_network(graph, renderer="browser") # open in a browser tab
"""
if config is not None:
# Start with provided config, then override with kwargs
from dataclasses import asdict
config_dict = asdict(config)
config_dict.update(kwargs)
effective_config = PlotConfig(**config_dict)
else:
effective_config = PlotConfig(**kwargs)
plotter = NetworkPlotter(graph, partition_map=partition_map)
output_kwargs = {
"renderer": renderer,
"save_html": save_html,
"output_dir": output_dir,
"filename": filename,
"show": show,
}
# Support both string and enum style specifications
if style == "simple" or style == PlotStyle.SIMPLE:
return plotter.plot_simple(effective_config, **output_kwargs)
elif style == "voltage_aware" or style == PlotStyle.VOLTAGE_AWARE:
return plotter.plot_voltage_aware(effective_config, **output_kwargs)
elif style == "clustered" or style == PlotStyle.CLUSTERED:
return plotter.plot_clustered(effective_config, **output_kwargs)
else:
raise ValueError(
f"Unknown plot style: {style}. Valid options: 'simple', 'voltage_aware', 'clustered'"
)