MatplotLibAPI 3.2.21__py3-none-any.whl → 4.0.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.
Files changed (51) hide show
  1. MatplotLibAPI/__init__.py +4 -86
  2. MatplotLibAPI/accessor.py +519 -196
  3. MatplotLibAPI/area.py +177 -0
  4. MatplotLibAPI/bar.py +185 -0
  5. MatplotLibAPI/base_plot.py +88 -0
  6. MatplotLibAPI/box_violin.py +180 -0
  7. MatplotLibAPI/bubble.py +568 -0
  8. MatplotLibAPI/{Composite.py → composite.py} +127 -106
  9. MatplotLibAPI/heatmap.py +223 -0
  10. MatplotLibAPI/histogram.py +170 -0
  11. MatplotLibAPI/mcp/__init__.py +17 -0
  12. MatplotLibAPI/mcp/metadata.py +90 -0
  13. MatplotLibAPI/mcp/renderers.py +45 -0
  14. MatplotLibAPI/mcp_server.py +626 -0
  15. MatplotLibAPI/network/__init__.py +28 -0
  16. MatplotLibAPI/network/constants.py +22 -0
  17. MatplotLibAPI/network/core.py +1360 -0
  18. MatplotLibAPI/network/plot.py +597 -0
  19. MatplotLibAPI/network/scaling.py +56 -0
  20. MatplotLibAPI/pie.py +154 -0
  21. MatplotLibAPI/pivot.py +274 -0
  22. MatplotLibAPI/sankey.py +99 -0
  23. MatplotLibAPI/{StyleTemplate.py → style_template.py} +27 -22
  24. MatplotLibAPI/sunburst.py +139 -0
  25. MatplotLibAPI/{Table.py → table.py} +112 -87
  26. MatplotLibAPI/{Timeserie.py → timeserie.py} +98 -42
  27. MatplotLibAPI/{Treemap.py → treemap.py} +43 -55
  28. MatplotLibAPI/typing.py +12 -0
  29. MatplotLibAPI/{_visualization_utils.py → utils.py} +7 -13
  30. MatplotLibAPI/waffle.py +173 -0
  31. MatplotLibAPI/word_cloud.py +489 -0
  32. {matplotlibapi-3.2.21.dist-info → matplotlibapi-4.0.0.dist-info}/METADATA +98 -9
  33. matplotlibapi-4.0.0.dist-info/RECORD +36 -0
  34. {matplotlibapi-3.2.21.dist-info → matplotlibapi-4.0.0.dist-info}/WHEEL +1 -1
  35. matplotlibapi-4.0.0.dist-info/entry_points.txt +2 -0
  36. MatplotLibAPI/Area.py +0 -80
  37. MatplotLibAPI/Bar.py +0 -83
  38. MatplotLibAPI/BoxViolin.py +0 -75
  39. MatplotLibAPI/Bubble.py +0 -458
  40. MatplotLibAPI/Heatmap.py +0 -121
  41. MatplotLibAPI/Histogram.py +0 -73
  42. MatplotLibAPI/Network.py +0 -989
  43. MatplotLibAPI/Pie.py +0 -70
  44. MatplotLibAPI/Pivot.py +0 -134
  45. MatplotLibAPI/Sankey.py +0 -46
  46. MatplotLibAPI/Sunburst.py +0 -89
  47. MatplotLibAPI/Waffle.py +0 -86
  48. MatplotLibAPI/Wordcloud.py +0 -373
  49. MatplotLibAPI/_typing.py +0 -17
  50. matplotlibapi-3.2.21.dist-info/RECORD +0 -26
  51. {matplotlibapi-3.2.21.dist-info → matplotlibapi-4.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1360 @@
1
+ """Network chart plotting helpers."""
2
+
3
+ from collections import defaultdict
4
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast
5
+ from pandas.api.extensions import register_dataframe_accessor
6
+ import matplotlib.pyplot as plt
7
+ import networkx as nx
8
+ import numpy as np
9
+ import pandas as pd
10
+ import seaborn as sns
11
+ from matplotlib.axes import Axes
12
+ from matplotlib.figure import Figure
13
+
14
+ from ..base_plot import BasePlot
15
+ from .constants import _DEFAULT, _WEIGHT_PERCENTILES
16
+ from .scaling import _scale_weights
17
+ from ..style_template import (
18
+ NETWORK_STYLE_TEMPLATE,
19
+ FIG_SIZE,
20
+ TITLE_SCALE_FACTOR,
21
+ StyleTemplate,
22
+ string_formatter,
23
+ validate_dataframe,
24
+ )
25
+
26
+ __all__ = [
27
+ "NETWORK_STYLE_TEMPLATE",
28
+ "NetworkGraph",
29
+ ]
30
+
31
+
32
+ class NodeView(nx.classes.reportviews.NodeView):
33
+ """Extended node view with convenience helpers."""
34
+
35
+ def __getitem__(self, n: Any) -> Dict[str, Any]:
36
+ return super().__getitem__(n)
37
+
38
+ def sort(self, attribute: str = "weight", reverse: bool = True) -> List[Any]:
39
+ """Return nodes sorted by the specified attribute.
40
+
41
+ Parameters
42
+ ----------
43
+ attribute : str, optional
44
+ Node attribute used for sorting. The default is "weight".
45
+ reverse : bool, optional
46
+ Sort order. The default is `True`.
47
+
48
+ Returns
49
+ -------
50
+ list[Any]
51
+ Sorted nodes.
52
+ """
53
+ sorted_nodes = sorted(
54
+ self, key=lambda node: self[node].get(attribute, 1), reverse=reverse
55
+ )
56
+ return sorted_nodes
57
+
58
+ def filter(self, attribute: str, value: str) -> List[Any]:
59
+ """Return nodes where ``attribute`` equals ``value``.
60
+
61
+ Parameters
62
+ ----------
63
+ attribute : str
64
+ Node attribute to compare.
65
+ value : str
66
+ Desired attribute value.
67
+
68
+ Returns
69
+ -------
70
+ list
71
+ Nodes matching the condition.
72
+ """
73
+ filtered_nodes = [node for node in self if self[node].get(attribute) == value]
74
+ return filtered_nodes
75
+
76
+ def to_dataframe(
77
+ self, node_col: str = "node", weight_col: str = "weight"
78
+ ) -> pd.DataFrame:
79
+ """Convert the node view to a DataFrame.
80
+
81
+ Parameters
82
+ ----------
83
+ node_col : str, optional
84
+ Column name for node identifiers. The default is "node".
85
+ weight_col : str, optional
86
+ Column name for node weights. The default is "weight".
87
+
88
+ Returns
89
+ -------
90
+ pd.DataFrame
91
+ DataFrame with columns for nodes and their weights.
92
+ """
93
+ data = [
94
+ {node_col: node, weight_col: data.get(weight_col, 1)}
95
+ for node, data in self(data=True)
96
+ ]
97
+ return pd.DataFrame(data)
98
+
99
+
100
+ class AdjacencyView(nx.classes.coreviews.AdjacencyView):
101
+ """Adjacency view with sorting and filtering helpers."""
102
+
103
+ def sort(self, attribute: str = "weight", reverse: bool = True) -> List[Any]:
104
+ """Return adjacent nodes sorted by the given attribute.
105
+
106
+ Parameters
107
+ ----------
108
+ attribute : str, optional
109
+ Attribute used for sorting. The default is "weight".
110
+ reverse : bool, optional
111
+ Sort order. The default is `True`.
112
+
113
+ Returns
114
+ -------
115
+ list[Any]
116
+ Sorted adjacent nodes.
117
+ """
118
+ sorted_nodes = sorted(
119
+ self, key=lambda node: self[node].get(attribute, 1), reverse=reverse
120
+ )
121
+ return sorted_nodes
122
+
123
+ def filter(self, attribute: str, value: str) -> List[Any]:
124
+ """Return adjacent nodes where ``attribute`` equals ``value``.
125
+
126
+ Parameters
127
+ ----------
128
+ attribute : str
129
+ Node attribute to compare.
130
+ value : str
131
+ Desired attribute value.
132
+
133
+ Returns
134
+ -------
135
+ list
136
+ Adjacent nodes matching the value.
137
+ """
138
+ filtered_nodes = [node for node in self if self[node].get(attribute) == value]
139
+ return filtered_nodes
140
+
141
+ def to_dataframe(
142
+ self, node_col: str = "node", weight_col: str = "weight"
143
+ ) -> pd.DataFrame:
144
+ """Convert the adjacency view to a DataFrame.
145
+
146
+ Parameters
147
+ ----------
148
+ node_col : str, optional
149
+ Column name for node identifiers. The default is "node".
150
+ weight_col : str, optional
151
+ Column name for node weights. The default is "weight".
152
+
153
+ Returns
154
+ -------
155
+ pd.DataFrame
156
+ DataFrame with columns for adjacent nodes and their weights.
157
+ """
158
+ data = {node: self[node].get(weight_col, 1) for node in self}
159
+ return pd.DataFrame(data)
160
+
161
+
162
+ class EdgeView(nx.classes.reportviews.EdgeView):
163
+ """Edge view with sorting and filtering helpers."""
164
+
165
+ def __getitem__(self, e: Tuple) -> Dict[str, Any]:
166
+ return super().__getitem__(e)
167
+
168
+ def sort(
169
+ self, attribute: str = "weight", reverse: bool = True
170
+ ) -> Dict[Tuple[Any, Any], Dict[str, Any]]:
171
+ """Return edges sorted by the given attribute.
172
+
173
+ Parameters
174
+ ----------
175
+ attribute : str, optional
176
+ Edge attribute used for sorting. The default is "weight".
177
+ reverse : bool, optional
178
+ Sort order. The default is `True`.
179
+
180
+ Returns
181
+ -------
182
+ dict[tuple[Any, Any], dict[str, Any]]
183
+ Mapping of edge tuples to their attributes.
184
+ """
185
+ sorted_edges = sorted(
186
+ self(data=True), key=lambda t: t[2].get(attribute, 1), reverse=reverse
187
+ )
188
+ return {(u, v): data for u, v, data in sorted_edges}
189
+
190
+ def filter(self, attribute: str, value: str) -> List[Tuple[Any, Any]]:
191
+ """Return edges where ``attribute`` equals ``value``.
192
+
193
+ Parameters
194
+ ----------
195
+ attribute : str
196
+ Edge attribute to compare.
197
+ value : str
198
+ Desired attribute value.
199
+
200
+ Returns
201
+ -------
202
+ list[tuple[Any, Any]]
203
+ Edges matching the condition.
204
+ """
205
+ return [
206
+ (edge[0], edge[1]) for edge in self if self[edge].get(attribute) == value
207
+ ]
208
+
209
+ def to_dataframe(
210
+ self,
211
+ source_col: str = "source",
212
+ target_col: str = "target",
213
+ weight_col: str = "weight",
214
+ ) -> pd.DataFrame:
215
+ """Convert the edge view to a DataFrame.
216
+
217
+ Parameters
218
+ ----------
219
+ source_col : str, optional
220
+ Column name for source nodes. The default is "source".
221
+ target_col : str, optional
222
+ Column name for target nodes. The default is "target".
223
+ weight_col : str, optional
224
+ Column name for edge weights. The default is "weight".
225
+
226
+ Returns
227
+ -------
228
+ pd.DataFrame
229
+ DataFrame with columns for source, target and weight.
230
+ """
231
+ data = [
232
+ {source_col: u, target_col: v, weight_col: data.get(weight_col, 1)}
233
+ for u, v, data in self(data=True)
234
+ ]
235
+ return pd.DataFrame(data)
236
+
237
+
238
+ @register_dataframe_accessor("network")
239
+ class NetworkGraph(BasePlot):
240
+ """Custom graph class based on NetworkX's ``Graph``.
241
+
242
+ Methods
243
+ -------
244
+ compute_positions
245
+ Return node positions computed with a spring layout.
246
+ layout
247
+ Return scaled node sizes, edge widths, and grouped font sizes.
248
+ aplot
249
+ Plot the graph on a provided axis.
250
+ fplot
251
+ Plot the graph and return a new figure.
252
+ aplot_connected_components
253
+ Plot each connected component on a shared axis.
254
+ fplot_connected_components
255
+ Plot each connected component on a new figure.
256
+ get_component_subgraph
257
+ Return the subgraph containing the specified node.
258
+ k_core
259
+ Return the k-core of the graph.
260
+ get_core_subgraph
261
+ Return the 2-core of the graph.
262
+ top_k_edges
263
+ Return the top edges for each node based on an attribute.
264
+ calculate_node_weights_from_edges
265
+ Populate node weights by summing top edge weights.
266
+ trim_edges
267
+ Create a subgraph that retains the top edges per node.
268
+ set_node_attributes
269
+ Set multiple node attributes from a mapping.
270
+ from_pandas_edgelist
271
+ Build a graph from a pandas edge list.
272
+ build_from_dataframes
273
+ Construct a graph from node and edge DataFrames with validation.
274
+ """
275
+
276
+ _nx_graph: nx.Graph
277
+
278
+ def __init__(
279
+ self,
280
+ pd_df: Optional[pd.DataFrame] = None,
281
+ nx_graph: Optional[nx.Graph] = None,
282
+ source: str = "source",
283
+ target: str = "target",
284
+ weight: str = "weight",
285
+ ):
286
+ """Initialize with an existing NetworkX graph.
287
+
288
+ Parameters
289
+ ----------
290
+ nx_graph : nx.Graph
291
+ Graph to wrap.
292
+ """
293
+ self._weight_column = weight
294
+ if isinstance(pd_df, nx.Graph) and nx_graph is None:
295
+ nx_graph = pd_df
296
+ pd_df = None
297
+
298
+ if nx_graph is not None:
299
+ self._nx_graph = nx_graph
300
+ elif pd_df is not None:
301
+ self._nx_graph = NetworkGraph.from_pandas_edgelist(
302
+ pd_df, source=source, target=target, edge_weight_col=weight
303
+ )._nx_graph
304
+ else:
305
+ self._nx_graph = nx.Graph()
306
+
307
+ self._scale = 1.0
308
+
309
+ @property
310
+ def scale(self) -> float:
311
+ """Return scaling factor for plotting sizes."""
312
+ return self._scale
313
+
314
+ @scale.setter
315
+ def scale(self, value: float):
316
+ """Set scaling factor for plotting sizes.
317
+
318
+ Parameters
319
+ ----------
320
+ value : float
321
+ Scaling factor.
322
+ """
323
+ self._scale = value
324
+
325
+ @property
326
+ def node_view(self) -> NodeView:
327
+ """Return a ``NodeView`` over the graph."""
328
+ return NodeView(self._nx_graph)
329
+
330
+ @node_view.setter
331
+ def node_view(self, node: Any, attributes: Dict[str, Any]):
332
+ """Set attributes for a specific node.
333
+
334
+ Parameters
335
+ ----------
336
+ node : Any
337
+ Node identifier.
338
+ attributes : dict
339
+ Attributes to set for the node.
340
+ """
341
+ self._nx_graph.nodes[node].update(attributes)
342
+
343
+ @property
344
+ def edge_view(self) -> EdgeView:
345
+ """Return an ``EdgeView`` over the graph."""
346
+ return EdgeView(self._nx_graph)
347
+
348
+ @edge_view.setter
349
+ def edge_view(self, edge: Tuple[Any, Any], attributes: Dict[str, Any]):
350
+ """Set attributes for a specific edge.
351
+
352
+ Parameters
353
+ ----------
354
+ edge : tuple[Any, Any]
355
+ Edge defined by source and target node identifiers.
356
+ attributes : dict
357
+ Attributes to set for the edge.
358
+ """
359
+ u, v = edge
360
+ self._nx_graph.edges[u, v].update(attributes)
361
+
362
+ @property
363
+ def adjacency_view(self) -> AdjacencyView:
364
+ """Return an ``AdjacencyView`` of the graph."""
365
+ return AdjacencyView(self._nx_graph.adj)
366
+
367
+ @adjacency_view.setter
368
+ def adjacency_view(self, node: Any, attributes: Dict[str, Any]):
369
+ """Set attributes for a specific node.
370
+
371
+ Parameters
372
+ ----------
373
+ node : Any
374
+ Node identifier.
375
+ attributes : dict
376
+ Attributes to set for the node.
377
+ """
378
+ self._nx_graph.adj[node].update(attributes)
379
+
380
+ @property
381
+ def connected_components(self) -> List[set]:
382
+ """Return the connected components of the graph."""
383
+ return list(nx.connected_components(self._nx_graph))
384
+
385
+ @property
386
+ def number_of_nodes(self) -> int:
387
+ """Return the number of nodes in the graph."""
388
+ return self._nx_graph.number_of_nodes()
389
+
390
+ @property
391
+ def number_of_edges(self) -> int:
392
+ """Return the number of edges in the graph."""
393
+ return self._nx_graph.number_of_edges()
394
+
395
+ @property
396
+ def density(self) -> float:
397
+ """Return the density of the graph."""
398
+ return nx.density(self._nx_graph)
399
+
400
+ @property
401
+ def is_connected(self) -> bool:
402
+ """Return whether the graph is connected."""
403
+ return nx.is_connected(self._nx_graph)
404
+
405
+ @property
406
+ def average_clustering(self) -> float:
407
+ """Return the average clustering coefficient of the graph."""
408
+ return nx.average_clustering(self._nx_graph)
409
+
410
+ @property
411
+ def diameter(self) -> int:
412
+ """Return the diameter of the graph."""
413
+ return nx.diameter(self._nx_graph)
414
+
415
+ @property
416
+ def radius(self) -> int:
417
+ """Return the radius of the graph."""
418
+ return nx.radius(self._nx_graph)
419
+
420
+ @property
421
+ def center(self) -> List[Any]:
422
+ """Return the center nodes of the graph."""
423
+ return nx.center(self._nx_graph)
424
+
425
+ @property
426
+ def periphery(self) -> List[Any]:
427
+ """Return the periphery nodes of the graph."""
428
+ return nx.periphery(self._nx_graph)
429
+
430
+ @property
431
+ def average_shortest_path_length(self) -> float:
432
+ """Return the average shortest path length of the graph."""
433
+ return nx.average_shortest_path_length(self._nx_graph)
434
+
435
+ @property
436
+ def transitivity(self) -> float:
437
+ """Return the transitivity of the graph."""
438
+ return nx.transitivity(self._nx_graph)
439
+
440
+ @property
441
+ def clustering_coefficients(self) -> Dict[Any, float]:
442
+ """Return the clustering coefficients of the graph."""
443
+ return nx.clustering(self._nx_graph) # pyright: ignore[reportReturnType]
444
+
445
+ @property
446
+ def degree_assortativity_coefficient(self) -> float:
447
+ """Return the degree assortativity coefficient of the graph."""
448
+ return nx.degree_assortativity_coefficient(self._nx_graph)
449
+
450
+ def add_node(self, node: Any, **attributes: Any):
451
+ """Add a node with optional attributes.
452
+
453
+ Parameters
454
+ ----------
455
+ node : Any
456
+ Node identifier.
457
+ **attributes : dict
458
+ Arbitrary node attributes as keyword arguments.
459
+ """
460
+ self._nx_graph.add_node(node, **attributes)
461
+
462
+ def add_nodes_from(self, nodes: Iterable, **attributes: Any):
463
+ """Add multiple nodes with optional attributes.
464
+
465
+ Parameters
466
+ ----------
467
+ nodes : Iterable
468
+ Node identifiers to add.
469
+ **attributes : dict
470
+ Arbitrary node attributes as keyword arguments.
471
+ """
472
+ self._nx_graph.add_nodes_from(nodes, **attributes)
473
+
474
+ def add_edge(self, source: Any, target: Any, **attributes: Any):
475
+ """Add an edge with optional attributes.
476
+
477
+ Parameters
478
+ ----------
479
+ source : Any
480
+ Source node identifier.
481
+ target : Any
482
+ Target node identifier.
483
+ **attributes : dict
484
+ Arbitrary edge attributes as keyword arguments.
485
+ """
486
+ self._nx_graph.add_edge(source, target, **attributes)
487
+
488
+ def add_edges_from(self, edges: Iterable[Tuple[Any, Any]], **attributes: Any):
489
+ """Add multiple edges with optional attributes.
490
+
491
+ Parameters
492
+ ----------
493
+ edges : Iterable[tuple[Any, Any]]
494
+ Edge tuples defined by source and target node identifiers.
495
+ **attributes : dict
496
+ Arbitrary edge attributes as keyword arguments.
497
+ """
498
+ self._nx_graph.add_edges_from(edges, **attributes)
499
+
500
+ def subgraph_edges(self, edges: Iterable) -> "NetworkGraph":
501
+ """Return a subgraph containing only the specified edges.
502
+
503
+ Parameters
504
+ ----------
505
+ edges : Iterable
506
+ Edges to include.
507
+
508
+ Returns
509
+ -------
510
+ NetworkGraph
511
+ Subgraph with only ``edges``.
512
+ """
513
+ return NetworkGraph(nx.edge_subgraph(self._nx_graph, edges).copy())
514
+
515
+ def layout(
516
+ self,
517
+ max_node_size: int = _DEFAULT["MAX_NODE_SIZE"],
518
+ min_node_size: int = _DEFAULT["MIN_NODE_SIZE"],
519
+ max_edge_width: int = _DEFAULT["MAX_EDGE_WIDTH"],
520
+ max_font_size: int = _DEFAULT["MAX_FONT_SIZE"],
521
+ min_font_size: int = _DEFAULT["MIN_FONT_SIZE"],
522
+ edge_weight_col: str = "weight",
523
+ ) -> Tuple[List[float], List[float], Dict[int, List[str]]]:
524
+ """Calculate node, edge and font sizes based on weights.
525
+
526
+ Parameters
527
+ ----------
528
+ max_node_size : int, optional
529
+ Upper bound for node size. The default is `_DEFAULT["MAX_NODE_SIZE"]`.
530
+ min_node_size : int, optional
531
+ Lower bound for node size. The default is `_DEFAULT["MIN_NODE_SIZE"]`.
532
+ max_edge_width : int, optional
533
+ Upper bound for edge width. The default is `_DEFAULT["MAX_EDGE_WIDTH"]`.
534
+ max_font_size : int, optional
535
+ Upper bound for font size. The default is `_DEFAULT["MAX_FONT_SIZE"]`.
536
+ min_font_size : int, optional
537
+ Lower bound for font size. The default is `_DEFAULT["MIN_FONT_SIZE"]`.
538
+ edge_weight_col : str, optional
539
+ Node attribute used for weighting. The default is "weight".
540
+
541
+ Returns
542
+ -------
543
+ tuple[list[float], list[float], dict[int, list[str]]]
544
+ Node sizes, edge widths and nodes grouped by font size.
545
+ """
546
+ # Normalize and scale nodes' weights within the desired range of edge widths
547
+ node_weights = [
548
+ data.get(edge_weight_col, 1) for node, data in self.node_view(data=True)
549
+ ]
550
+ node_deciles = (
551
+ np.percentile(np.array(node_weights), _WEIGHT_PERCENTILES)
552
+ if node_weights
553
+ else None
554
+ )
555
+ node_size = _scale_weights(
556
+ weights=node_weights,
557
+ scale_max=max_node_size,
558
+ scale_min=min_node_size,
559
+ deciles=node_deciles,
560
+ )
561
+
562
+ # Normalize and scale edges' weights within the desired range of edge widths
563
+ edge_weights = [
564
+ data.get(edge_weight_col, 1) for _, _, data in self.edge_view(data=True)
565
+ ]
566
+ edges_width = _scale_weights(weights=edge_weights, scale_max=max_edge_width)
567
+
568
+ # Scale the normalized node weights within the desired range of font sizes
569
+ node_size_dict = dict(
570
+ zip(
571
+ self.node_view,
572
+ _scale_weights(
573
+ weights=node_weights,
574
+ scale_max=max_font_size,
575
+ scale_min=min_font_size,
576
+ deciles=node_deciles,
577
+ ),
578
+ )
579
+ )
580
+ fonts_size = defaultdict(list)
581
+ for node, width in node_size_dict.items():
582
+ fonts_size[int(width)].append(node)
583
+ fonts_size = dict(fonts_size)
584
+
585
+ return node_size, edges_width, fonts_size
586
+
587
+ def subgraph(
588
+ self,
589
+ node_list: Optional[List[str]] = None,
590
+ max_edges: int = _DEFAULT["MAX_EDGES"],
591
+ min_degree: int = 2,
592
+ top_k_edges_per_node: int = 5,
593
+ ) -> "NetworkGraph":
594
+ """Return a trimmed subgraph limited by nodes and edges.
595
+
596
+ Parameters
597
+ ----------
598
+ node_list : list[str], optional
599
+ Nodes to include.
600
+ max_edges : int, optional
601
+ Maximum edges to retain. The default is `_DEFAULT["MAX_EDGES"]`.
602
+ min_degree : int, optional
603
+ Minimum degree for nodes in the core subgraph. The default is 2.
604
+ top_k_edges_per_node : int, optional
605
+ Number of top edges to keep per node. The default is 5.
606
+
607
+ Returns
608
+ -------
609
+ NetworkGraph
610
+ Trimmed subgraph.
611
+ """
612
+ if node_list is None:
613
+ node_list = self.node_view.sort("weight")[: _DEFAULT["MAX_NODES"]]
614
+ core_subgraph_nodes = list(self.k_core(k=min_degree).node_view)
615
+ node_list = [node for node in node_list if node in core_subgraph_nodes]
616
+ _subgraph = nx.subgraph(self._nx_graph, node_list)
617
+ subgraph = NetworkGraph(_subgraph)
618
+ if top_k_edges_per_node > 0:
619
+ edges = subgraph.top_k_edges(
620
+ attribute="weight", k=top_k_edges_per_node
621
+ ).keys()
622
+ subgraph = subgraph.subgraph_edges(list(edges)[:max_edges])
623
+ return subgraph
624
+
625
+ def compute_positions(
626
+ self,
627
+ k: Optional[float] = None,
628
+ seed: Optional[int] = _DEFAULT["SPRING_LAYOUT_SEED"],
629
+ ) -> Dict[Any, np.ndarray]:
630
+ """Return spring layout positions for the graph.
631
+
632
+ Parameters
633
+ ----------
634
+ k : float, optional
635
+ Optimal distance between nodes. The default is ``_DEFAULT["SPRING_LAYOUT_K"]``.
636
+ seed : int, optional
637
+ Seed for reproducible layouts. The default is ``_DEFAULT["SPRING_LAYOUT_SEED"]``.
638
+
639
+ Returns
640
+ -------
641
+ dict[Any, np.ndarray]
642
+ Mapping of nodes to their layout coordinates.
643
+ """
644
+ layout_k = _DEFAULT["SPRING_LAYOUT_K"] if k is None else k
645
+ return nx.spring_layout(self._nx_graph, k=layout_k, seed=seed)
646
+
647
+ def subgraph_component(self, node: Any) -> "NetworkGraph":
648
+ """Return the connected component containing ``node``.
649
+
650
+ Parameters
651
+ ----------
652
+ node : Any
653
+ Node identifier to anchor the component selection.
654
+
655
+ Returns
656
+ -------
657
+ NetworkGraph
658
+ Subgraph made of the nodes in the same connected component as
659
+ ``node``.
660
+
661
+ Raises
662
+ ------
663
+ ValueError
664
+ If ``node`` is not present in the graph.
665
+ """
666
+ if node not in self._nx_graph:
667
+ raise ValueError(f"Node {node!r} is not present in the graph.")
668
+
669
+ component_nodes = next(
670
+ (
671
+ component
672
+ for component in nx.connected_components(self._nx_graph)
673
+ if node in component
674
+ ),
675
+ None,
676
+ )
677
+
678
+ if component_nodes is None:
679
+ return NetworkGraph()
680
+
681
+ return NetworkGraph(nx.subgraph(self._nx_graph, component_nodes).copy())
682
+
683
+ def aplot(
684
+ self,
685
+ title: Optional[str] = None,
686
+ style: StyleTemplate = NETWORK_STYLE_TEMPLATE,
687
+ edge_weight_col: str = "weight",
688
+ layout_seed: Optional[int] = _DEFAULT["SPRING_LAYOUT_SEED"],
689
+ ax: Optional[Axes] = None,
690
+ **kwargs: Any,
691
+ ) -> Axes:
692
+ """Plot the graph using node and edge weights.
693
+
694
+ Parameters
695
+ ----------
696
+ title : str, optional
697
+ Plot title.
698
+ style : StyleTemplate, optional
699
+ Style configuration. The default is `NETWORK_STYLE_TEMPLATE`.
700
+ edge_weight_col : str, optional
701
+ Edge attribute used for weighting. The default is "weight".
702
+ layout_seed : int, optional
703
+ Seed for the spring layout used to place nodes. The default is ``_DEFAULT["SPRING_LAYOUT_SEED"]``.
704
+ ax : Axes, optional
705
+ Axes to draw on.
706
+
707
+ Returns
708
+ -------
709
+ Axes
710
+ Matplotlib axes with the plotted network.
711
+ """
712
+ sns.set_palette(style.palette)
713
+ if ax is None:
714
+ ax = cast(Axes, plt.gca())
715
+
716
+ isolated_nodes = list(nx.isolates(self._nx_graph))
717
+ graph_nx = self._nx_graph
718
+ if isolated_nodes:
719
+ graph_nx = graph_nx.copy()
720
+ graph_nx.remove_nodes_from(isolated_nodes)
721
+
722
+ graph = self if graph_nx is self._nx_graph else NetworkGraph(nx_graph=graph_nx)
723
+
724
+ if graph._nx_graph.number_of_nodes() == 0:
725
+ ax.set_axis_off()
726
+ if title:
727
+ ax.set_title(
728
+ title,
729
+ color=style.font_color,
730
+ fontsize=style.font_size * TITLE_SCALE_FACTOR,
731
+ )
732
+ return ax
733
+
734
+ mapped_min_font_size = style.font_mapping.get(0)
735
+ mapped_max_font_size = style.font_mapping.get(4)
736
+
737
+ node_sizes, edge_widths, font_sizes = graph.layout(
738
+ min_node_size=_DEFAULT["MIN_NODE_SIZE"],
739
+ max_node_size=_DEFAULT["MAX_NODE_SIZE"],
740
+ max_edge_width=_DEFAULT["MAX_EDGE_WIDTH"],
741
+ min_font_size=(
742
+ mapped_min_font_size
743
+ if mapped_min_font_size is not None
744
+ else _DEFAULT["MIN_FONT_SIZE"]
745
+ ),
746
+ max_font_size=(
747
+ mapped_max_font_size
748
+ if mapped_max_font_size is not None
749
+ else _DEFAULT["MAX_FONT_SIZE"]
750
+ ),
751
+ edge_weight_col=edge_weight_col,
752
+ )
753
+ pos = graph.compute_positions(seed=layout_seed)
754
+ # nodes
755
+ node_sizes_int = [int(size) for size in node_sizes]
756
+ nx.draw_networkx_nodes(
757
+ graph._nx_graph,
758
+ pos,
759
+ ax=ax,
760
+ node_size=cast(Any, node_sizes_int),
761
+ node_color=cast(Any, node_sizes),
762
+ cmap=plt.get_cmap(style.palette),
763
+ )
764
+ # edges
765
+ nx.draw_networkx_edges(
766
+ graph._nx_graph,
767
+ pos,
768
+ ax=ax,
769
+ edge_color=style.font_color,
770
+ edge_cmap=plt.get_cmap(style.palette),
771
+ width=cast(Any, edge_widths),
772
+ )
773
+ # labels
774
+ for font_size, nodes in font_sizes.items():
775
+ nx.draw_networkx_labels(
776
+ graph._nx_graph,
777
+ pos,
778
+ ax=ax,
779
+ font_size=font_size,
780
+ font_color=style.font_color,
781
+ labels={n: string_formatter(n) for n in nodes},
782
+ )
783
+ ax.set_facecolor(style.background_color)
784
+ if title:
785
+ ax.set_title(
786
+ title,
787
+ color=style.font_color,
788
+ fontsize=style.font_size * TITLE_SCALE_FACTOR,
789
+ )
790
+ ax.set_axis_off()
791
+
792
+ return ax
793
+
794
+ def fplot(
795
+ self,
796
+ title: Optional[str] = None,
797
+ style: StyleTemplate = NETWORK_STYLE_TEMPLATE,
798
+ layout_seed: Optional[int] = _DEFAULT["SPRING_LAYOUT_SEED"],
799
+ figsize: Tuple[float, float] = FIG_SIZE,
800
+ ) -> Figure:
801
+ """Plot the graph using node and edge weights.
802
+
803
+ Parameters
804
+ ----------
805
+ title : str, optional
806
+ Plot title.
807
+ style : StyleTemplate, optional
808
+ Style configuration. The default is `NETWORK_STYLE_TEMPLATE`.
809
+ edge_weight_col : str, optional
810
+ Edge attribute used for weighting. The default is "weight".
811
+ layout_seed : int, optional
812
+ Seed for the spring layout used to place nodes. The default is ``_DEFAULT["SPRING_LAYOUT_SEED"]``.
813
+
814
+ Returns
815
+ -------
816
+ Figure
817
+ Matplotlib figure with the plotted network.
818
+ """
819
+ fig = Figure(
820
+ figsize=figsize,
821
+ facecolor=style.background_color,
822
+ edgecolor=style.background_color,
823
+ )
824
+ ax = Axes(fig=fig, facecolor=style.background_color)
825
+ self.aplot(
826
+ title=title,
827
+ style=style,
828
+ edge_weight_col="",
829
+ layout_seed=layout_seed,
830
+ ax=ax,
831
+ )
832
+ return fig
833
+
834
+ def aplot_connected_components(
835
+ self,
836
+ title: Optional[str] = None,
837
+ style: StyleTemplate = NETWORK_STYLE_TEMPLATE,
838
+ edge_weight_col: str = "weight",
839
+ layout_seed: Optional[int] = _DEFAULT["SPRING_LAYOUT_SEED"],
840
+ axes: Optional[np.ndarray] = None,
841
+ ) -> Union[Axes, np.ndarray]:
842
+ """Plot all connected components of the graph.
843
+
844
+ Parameters
845
+ ----------
846
+ title : str, optional
847
+ Base title for component subplots. When provided, each axis title is
848
+ suffixed with the component index.
849
+ style : StyleTemplate, optional
850
+ Style configuration. The default is `NETWORK_STYLE_TEMPLATE`.
851
+ edge_weight_col : str, optional
852
+ Edge attribute used for weighting. The default is "weight".
853
+ layout_seed : int, optional
854
+ Seed for the spring layout used to place nodes. The default is ``_DEFAULT["SPRING_LAYOUT_SEED"]``.
855
+ axes : np.ndarray, optional
856
+ Existing axes to draw each component on. If None, a grid is created
857
+ based on the number of components.
858
+
859
+ Returns
860
+ -------
861
+ Union[Axes, np.ndarray]
862
+ Matplotlib axes with the plotted network. When ``axes`` is provided
863
+ or created, the flattened array of axes is returned; otherwise, a
864
+ single Axes is returned.
865
+ """
866
+ sns.set_palette(style.palette)
867
+
868
+ graph = self
869
+ isolated_nodes = list(nx.isolates(self._nx_graph))
870
+ if isolated_nodes:
871
+ graph = NetworkGraph(nx_graph=self._nx_graph.copy())
872
+ graph._nx_graph.remove_nodes_from(isolated_nodes)
873
+
874
+ connected_components = list(nx.connected_components(graph._nx_graph))
875
+
876
+ local_axes = axes
877
+ created_axes = False
878
+
879
+ if not connected_components:
880
+ if local_axes is None:
881
+ local_axes = np.array([cast(Axes, plt.gca())])
882
+ for axis in local_axes.flatten():
883
+ axis.set_facecolor(style.background_color)
884
+ axis.set_axis_off()
885
+ return local_axes
886
+
887
+ if local_axes is None:
888
+ _, local_axes = _compute_network_grid(connected_components, style)
889
+ created_axes = True
890
+
891
+ for i, component in enumerate(connected_components):
892
+ if i >= len(local_axes):
893
+ break
894
+ component_graph = NetworkGraph(
895
+ nx.subgraph(graph._nx_graph, component).copy()
896
+ )
897
+ component_graph.aplot(
898
+ title=f"{title}::{i}" if title else str(i),
899
+ style=style,
900
+ edge_weight_col=edge_weight_col,
901
+ layout_seed=layout_seed,
902
+ ax=cast(Axes, local_axes[i]),
903
+ )
904
+ cast(Axes, local_axes[i]).set_axis_on()
905
+
906
+ for axis in local_axes[len(connected_components) :]:
907
+ axis.set_axis_off()
908
+
909
+ return local_axes if created_axes or len(local_axes) > 1 else local_axes[0]
910
+
911
+ def fplot_connected_components(
912
+ self,
913
+ title: Optional[str] = None,
914
+ style: StyleTemplate = NETWORK_STYLE_TEMPLATE,
915
+ edge_weight_col: str = "weight",
916
+ layout_seed: Optional[int] = _DEFAULT["SPRING_LAYOUT_SEED"],
917
+ ) -> Figure:
918
+ """Plot all connected components of the graph.
919
+
920
+ Parameters
921
+ ----------
922
+ title : str, optional
923
+ Plot title to apply to the first component axis.
924
+ style : StyleTemplate, optional
925
+ Style configuration. The default is `NETWORK_STYLE_TEMPLATE`.
926
+ edge_weight_col : str, optional
927
+ Edge attribute used for weighting. The default is "weight".
928
+ layout_seed : int, optional
929
+ Seed for the spring layout used to place nodes. The default is ``_DEFAULT["SPRING_LAYOUT_SEED"]``.
930
+
931
+ Returns
932
+ -------
933
+ Figure
934
+ Matplotlib figure with the plotted network.
935
+ """
936
+ fig, ax = plt.subplots(figsize=FIG_SIZE)
937
+ fig = cast(Figure, fig)
938
+ fig.set_facecolor(style.background_color)
939
+ self.aplot_connected_components(
940
+ title=title,
941
+ style=style,
942
+ edge_weight_col=edge_weight_col,
943
+ layout_seed=layout_seed,
944
+ axes=np.array([ax]),
945
+ )
946
+ return fig
947
+
948
+ def k_core(self, k: int = 2) -> "NetworkGraph":
949
+ """Return the k-core of the graph.
950
+
951
+ The k-core is a subgraph containing only nodes with degree >= k.
952
+
953
+ Parameters
954
+ ----------
955
+ k : int, optional
956
+ The minimum degree for nodes in the core. The default is 2.
957
+
958
+ Returns
959
+ -------
960
+ NetworkGraph
961
+ The k-core subgraph.
962
+ """
963
+ core_graph = nx.k_core(self._nx_graph, k=k)
964
+ return NetworkGraph(core_graph)
965
+
966
+ def subgraph_core(self) -> "NetworkGraph":
967
+ """Return the 2-core of the graph.
968
+
969
+ Returns
970
+ -------
971
+ NetworkGraph
972
+ The k-core subgraph with minimum degree 2.
973
+ """
974
+ return self.k_core(k=2)
975
+
976
+ def top_k_edges(
977
+ self, attribute: str, reverse: bool = True, k: int = 5
978
+ ) -> Dict[Any, List[Tuple[Any, Dict]]]:
979
+ """Return the top ``k`` edges based on a given attribute.
980
+
981
+ Parameters
982
+ ----------
983
+ attribute : str
984
+ Attribute name used for sorting.
985
+ reverse : bool, optional
986
+ Whether to sort in descending order. The default is `True`.
987
+ k : int, optional
988
+ Number of top edges to return. The default is 5.
989
+
990
+ Returns
991
+ -------
992
+ dict[Any, list[tuple[Any, dict]]]
993
+ Mapping of edge tuples to attribute values.
994
+ """
995
+ top_list = {}
996
+ for node in self.node_view:
997
+ edges = self.edge_view(node, data=True)
998
+ edges_sorted = sorted(
999
+ edges, key=lambda x: x[2].get(attribute, 0), reverse=reverse
1000
+ )
1001
+ top_k_edges = edges_sorted[:k]
1002
+ for u, v, data in top_k_edges:
1003
+ edge_key = (u, v)
1004
+ top_list[edge_key] = data[attribute]
1005
+ return top_list
1006
+
1007
+ def calculate_nodes(
1008
+ self,
1009
+ edge_weight_col: str = "weight",
1010
+ k: int = 10,
1011
+ ):
1012
+ """Calculate node weights by summing weights of top k edges.
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ edge_weight_col : str, optional
1017
+ Edge attribute to use for weighting. The default is "weight".
1018
+ k : int, optional
1019
+ Number of top edges to consider for each node. The default is 10.
1020
+ """
1021
+ top_edges = self.top_k_edges(attribute=edge_weight_col, k=k)
1022
+ attributes: dict[Any, dict[str, Any]] = {}
1023
+ for (u, v), weight_value in top_edges.items():
1024
+ for node in [u, v]:
1025
+ if node not in attributes:
1026
+ attributes[node] = {edge_weight_col: 0, "edges": 0}
1027
+ attributes[node][edge_weight_col] += weight_value
1028
+ attributes[node]["edges"] += 1
1029
+
1030
+ self.set_node_attributes(attributes=attributes)
1031
+
1032
+ def trim_edges(
1033
+ self, edge_weight_col: str = "weight", k: int = 10
1034
+ ) -> "NetworkGraph":
1035
+ """Trim the graph to keep only the top k edges per node.
1036
+
1037
+ Parameters
1038
+ ----------
1039
+ edge_weight_col : str, optional
1040
+ Edge attribute to use for sorting. The default is "weight".
1041
+ top_k_per_node : int, optional
1042
+ Number of top edges to keep per node. The default is 5.
1043
+
1044
+ Returns
1045
+ -------
1046
+ NetworkGraph
1047
+ A new graph containing only the top edges.
1048
+ """
1049
+ edges_to_keep = self.top_k_edges(attribute=edge_weight_col, k=k)
1050
+ network_X = self.subgraph_edges(edges=edges_to_keep)
1051
+ network_X.sanitize_network()
1052
+ return network_X
1053
+
1054
+ def set_node_attributes(self, attributes: Dict[Any, Dict[str, Any]]):
1055
+ """Set multiple node attributes from a dictionary.
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ attributes : Dict[Any, Dict[str, Any]]
1060
+ Mapping of node identifiers to their attribute dictionaries.
1061
+ """
1062
+ for node, attrs in attributes.items():
1063
+ nx.set_node_attributes(self._nx_graph, {node: attrs})
1064
+
1065
+ @staticmethod
1066
+ def from_pandas_edgelist(
1067
+ edges_df: pd.DataFrame,
1068
+ source: str = "source",
1069
+ target: str = "target",
1070
+ edge_weight_col: str = "weight",
1071
+ k: int = 10,
1072
+ ) -> "NetworkGraph":
1073
+ """Initialize a NetworkGraph from a simple DataFrame.
1074
+
1075
+ Parameters
1076
+ ----------
1077
+ edges_df : pd.DataFrame
1078
+ DataFrame containing edge data.
1079
+ source : str, optional
1080
+ Column name for source nodes. The default is "source".
1081
+ target : str, optional
1082
+ Column name for target nodes. The default is "target".
1083
+ edge_weight_col : str, optional
1084
+ Column name for edge weights. The default is "weight".
1085
+
1086
+ Returns
1087
+ -------
1088
+ NetworkGraph
1089
+ Initialized network graph.
1090
+ """
1091
+ validate_dataframe(edges_df, cols=[source, target, edge_weight_col])
1092
+ network_Z = NetworkGraph()
1093
+ for src, dst, weight in edges_df[[source, target, edge_weight_col]].itertuples(
1094
+ index=False, name=None
1095
+ ):
1096
+ network_Z.add_edge(src, dst, **{edge_weight_col: weight})
1097
+
1098
+ network_Z.calculate_nodes(edge_weight_col=edge_weight_col, k=k)
1099
+ network_Z.sanitize_network()
1100
+ return network_Z
1101
+
1102
+ def sanitize_network(
1103
+ self,
1104
+ ):
1105
+ """Remove inconsistent nodes and edges to ensure graph consistency."""
1106
+ if len(self.node_view) == 0:
1107
+ return
1108
+
1109
+ nodes = set(self.node_view)
1110
+ edges = list(self.edge_view())
1111
+ nodes_in_edges = {endpoint for edge in edges for endpoint in edge}
1112
+
1113
+ for node in list(nodes):
1114
+ if node not in nodes_in_edges:
1115
+ self._nx_graph.remove_node(node)
1116
+
1117
+ for u, v in list(self.edge_view()):
1118
+ if u not in self._nx_graph or v not in self._nx_graph:
1119
+ self._nx_graph.remove_edge(u, v)
1120
+
1121
+ @staticmethod
1122
+ def sanitize_node_df(
1123
+ node_df: pd.DataFrame,
1124
+ edge_df: pd.DataFrame,
1125
+ node_col: str = "node",
1126
+ node_weight_col: str = "weight",
1127
+ edge_source_col: str = "source",
1128
+ edge_target_col: str = "target",
1129
+ edge_weight_col: str = "weight",
1130
+ ) -> pd.DataFrame:
1131
+ """Private helper returning ``node_df`` rows present in the edge list.
1132
+
1133
+ This method supports internal builders and is not part of the public API.
1134
+
1135
+ Parameters
1136
+ ----------
1137
+ node_df : pd.DataFrame
1138
+ DataFrame containing ``node`` and ``weight`` columns.
1139
+ edge_df : pd.DataFrame
1140
+ Edge DataFrame containing source and target columns.
1141
+ node_col : str, optional
1142
+ Column name for node identifiers. The default is "node".
1143
+ node_weight_col : str, optional
1144
+ Column name for node weights. The default is "weight".
1145
+ edge_source_col : str, optional
1146
+ Column name for source edges. The default is "source".
1147
+ edge_target_col : str, optional
1148
+ Column name for target edges. The default is "target".
1149
+ edge_weight_col : str, optional
1150
+ Column name for edge weights. The default is "weight". Included for
1151
+ signature parity with other sanitization helpers.
1152
+
1153
+ Returns
1154
+ -------
1155
+ pd.DataFrame
1156
+ Filtered ``node_df`` with only nodes that appear as sources or targets.
1157
+ """
1158
+ validate_dataframe(node_df, cols=[node_col, node_weight_col])
1159
+ filtered_node_df = node_df.copy()
1160
+ nodes_in_edges = list(
1161
+ set(edge_df[edge_source_col]).union(edge_df[edge_target_col])
1162
+ )
1163
+ return filtered_node_df.loc[filtered_node_df[node_col].isin(nodes_in_edges)]
1164
+
1165
+ @staticmethod
1166
+ def sanitize_edge_df(
1167
+ node_df: pd.DataFrame,
1168
+ edge_df: pd.DataFrame,
1169
+ node_col: str = "node",
1170
+ node_weight_col: str = "weight",
1171
+ edge_source_col: str = "source",
1172
+ edge_target_col: str = "target",
1173
+ edge_weight_col: str = "weight",
1174
+ ) -> pd.DataFrame:
1175
+ """Private helper returning a sanitized copy of the edge DataFrame.
1176
+
1177
+ Intended for internal validation when building graphs from dataframes.
1178
+
1179
+ Parameters
1180
+ ----------
1181
+ node_df : pd.DataFrame
1182
+ DataFrame containing node identifiers and weights.
1183
+ edge_df : pd.DataFrame
1184
+ Edge DataFrame containing source and target columns.
1185
+ node_col : str, optional
1186
+ Column name for node identifiers. The default is "node".
1187
+ node_weight_col : str, optional
1188
+ Column name for node weights. The default is "weight".
1189
+ edge_source_col : str, optional
1190
+ Column name for source nodes. The default is "source".
1191
+ edge_target_col : str, optional
1192
+ Column name for target nodes. The default is "target".
1193
+ edge_weight_col : str, optional
1194
+ Column name for edge weights. The default is "weight".
1195
+
1196
+ Returns
1197
+ -------
1198
+ pd.DataFrame
1199
+ Sanitized edge DataFrame containing only edges whose nodes appear
1200
+ in ``node_df``.
1201
+ """
1202
+ validate_dataframe(
1203
+ edge_df, cols=[edge_source_col, edge_target_col, edge_weight_col]
1204
+ )
1205
+ validate_dataframe(node_df, cols=[node_col, node_weight_col])
1206
+ allowed_nodes = node_df[node_col].tolist()
1207
+ edge_df = edge_df.loc[
1208
+ edge_df[edge_source_col].isin(allowed_nodes)
1209
+ & edge_df[edge_target_col].isin(allowed_nodes)
1210
+ ]
1211
+ return edge_df
1212
+
1213
+ @staticmethod
1214
+ def sanitize_dfs(
1215
+ node_df: pd.DataFrame,
1216
+ edge_df: pd.DataFrame,
1217
+ node_col: str = "node",
1218
+ node_weight_col: str = "weight",
1219
+ edge_source_col: str = "source",
1220
+ edge_target_col: str = "target",
1221
+ edge_weight_col: str = "weight",
1222
+ ) -> Tuple[pd.DataFrame, pd.DataFrame]:
1223
+ """Return sanitized node and edge DataFrames.
1224
+
1225
+ Parameters
1226
+ ----------
1227
+ node_df : pd.DataFrame
1228
+ DataFrame containing node identifiers and weights.
1229
+ edge_df : pd.DataFrame
1230
+ Edge DataFrame containing source, target, and weights.
1231
+ node_col : str, optional
1232
+ Column name for node identifiers. The default is "node".
1233
+ node_weight_col : str, optional
1234
+ Column name for node weights. The default is "weight".
1235
+ edge_source_col : str, optional
1236
+ Column name for source nodes. The default is "source".
1237
+ edge_target_col : str, optional
1238
+ Column name for target nodes. The default is "target".
1239
+ edge_weight_col : str, optional
1240
+ Column name for edge weights. The default is "weight".
1241
+
1242
+ Returns
1243
+ -------
1244
+ tuple[pd.DataFrame, pd.DataFrame]
1245
+ A tuple containing the sanitized node and edge DataFrames.
1246
+ """
1247
+ node_df = NetworkGraph.sanitize_node_df(
1248
+ node_df,
1249
+ edge_df=edge_df,
1250
+ node_col=node_col,
1251
+ node_weight_col=node_weight_col,
1252
+ edge_source_col=edge_source_col,
1253
+ edge_target_col=edge_target_col,
1254
+ edge_weight_col=edge_weight_col,
1255
+ )
1256
+ edge_df = NetworkGraph.sanitize_edge_df(
1257
+ node_df,
1258
+ edge_df=edge_df,
1259
+ node_col=node_col,
1260
+ node_weight_col=node_weight_col,
1261
+ edge_source_col=edge_source_col,
1262
+ edge_target_col=edge_target_col,
1263
+ edge_weight_col=edge_weight_col,
1264
+ )
1265
+ return node_df, edge_df
1266
+
1267
+ @staticmethod
1268
+ def from_pandas(
1269
+ node_df: pd.DataFrame,
1270
+ edge_df: pd.DataFrame,
1271
+ node_col: str = "node",
1272
+ node_weight_col: str = "weight",
1273
+ edge_source_col: str = "source",
1274
+ edge_target_col: str = "target",
1275
+ edge_weight_col: str = "weight",
1276
+ ) -> "NetworkGraph":
1277
+ """Build a NetworkGraph from node and edge DataFrames.
1278
+
1279
+ Parameters
1280
+ ----------
1281
+ node_df : pd.DataFrame
1282
+ DataFrame containing node identifiers and weights.
1283
+ edge_df : pd.DataFrame
1284
+ DataFrame containing the edge list.
1285
+ node_col : str, optional
1286
+ Column name for node identifiers. The default is "node".
1287
+ node_weight_col : str, optional
1288
+ Column name for node weights. The default is "weight".
1289
+ edge_source_col : str, optional
1290
+ Column name for source nodes. The default is "source".
1291
+ edge_target_col : str, optional
1292
+ Column name for target nodes. The default is "target".
1293
+ edge_weight_col : str, optional
1294
+ Column name for edge weights. The default is "weight".
1295
+
1296
+ Returns
1297
+ -------
1298
+ NetworkGraph
1299
+ Prepared ``NetworkGraph`` instance with node weights set and edges
1300
+ filtered to nodes present in ``node_df``.
1301
+ """
1302
+ if node_df is not None:
1303
+ node_df, edge_df = NetworkGraph.sanitize_dfs(
1304
+ node_df,
1305
+ edge_df,
1306
+ node_col=node_col,
1307
+ node_weight_col=node_weight_col,
1308
+ edge_source_col=edge_source_col,
1309
+ edge_target_col=edge_target_col,
1310
+ edge_weight_col=edge_weight_col,
1311
+ )
1312
+ graph = NetworkGraph.from_pandas_edgelist(
1313
+ edge_df,
1314
+ source=edge_source_col,
1315
+ target=edge_target_col,
1316
+ edge_weight_col=edge_weight_col,
1317
+ )
1318
+ if node_df is None or node_df.empty:
1319
+ graph.calculate_nodes(edge_weight_col=edge_weight_col)
1320
+ else:
1321
+ node_weights = {
1322
+ node: {node_weight_col: weight_value}
1323
+ for node, weight_value in node_df.set_index(node_col)[
1324
+ node_weight_col
1325
+ ].items()
1326
+ if node in graph._nx_graph.nodes
1327
+ }
1328
+ graph.set_node_attributes(node_weights)
1329
+
1330
+ return graph
1331
+
1332
+
1333
+ def _compute_network_grid(
1334
+ connected_components: List[set], style: StyleTemplate
1335
+ ) -> Tuple[Figure, np.ndarray]:
1336
+ """Compute the grid layout for network component subplots.
1337
+
1338
+ Parameters
1339
+ ----------
1340
+ connected_components : list[set]
1341
+ A list of sets, where each set contains the nodes of a connected component.
1342
+ style : StyleTemplate
1343
+ The style template used for plotting.
1344
+
1345
+ Returns
1346
+ -------
1347
+ Tuple[Figure, np.ndarray]
1348
+ A tuple containing the Matplotlib figure and the grid of axes.
1349
+ """
1350
+ n_components = len(connected_components)
1351
+ n_cols = int(np.ceil(np.sqrt(n_components)))
1352
+ n_rows = int(np.ceil(n_components / n_cols))
1353
+ fig, axes_grid = plt.subplots(n_rows, n_cols, figsize=FIG_SIZE)
1354
+ fig = cast(Figure, fig)
1355
+ fig.set_facecolor(style.background_color)
1356
+ if not isinstance(axes_grid, np.ndarray):
1357
+ axes = np.array([axes_grid])
1358
+ else:
1359
+ axes = axes_grid.flatten()
1360
+ return fig, axes