networkx_mermaid.builders   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 130
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 60
dl 0
loc 130
rs 10
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
A _edge_label() 0 4 2
A _node_style() 0 6 2
A _graph_title() 0 4 3
A _contrast_color() 0 16 5

3 Methods

Rating   Name   Duplication   Size   Complexity  
A DiagramBuilder.__init__() 0 28 3
A DiagramBuilder._diagram_config() 0 3 1
A DiagramBuilder.build() 0 32 1
1
from functools import lru_cache
2
from typing import Any
3
4
import networkx as nx
5
from mappingtools.collectors import AutoMapper
6
7
from .models import DiagramNodeShape, DiagramOrientation
8
from .typing import MermaidDiagram
9
10
DEFAULT_LAYOUT = "dagre"
11
DEFAULT_LOOK = "neo"
12
DEFAULT_THEME = "neutral"
13
14
15
def _edge_label(data: dict[str, Any]) -> str:
16
    """Generate an edge label string."""
17
    label = data.get("label")
18
    return f"|{label}|" if label else ""
19
20
21
@lru_cache(maxsize=1024)
22
def _contrast_color(color: str) -> str:
23
    """
24
    Return black or white by choosing the best contrast to input color.
25
26
    Args:
27
        color: str - hex color code
28
29
    Returns:
30
        color: str - hex color code
31
    """
32
    if not (isinstance(color, str) and color.startswith("#") and len(color) == 7):
33
        raise ValueError(f"Invalid color format: {color}. Expected a 6-digit hex code.")
34
35
    r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
36
    return "#000000" if (r * 0.299 + g * 0.587 + b * 0.114) > 186 else "#ffffff"
37
38
39
def _node_style(node_id: str, data: dict[str, Any]) -> str:
40
    """Generate a node style string."""
41
    color = data.get("color")
42
    if color:
43
        return f"\nstyle {node_id} fill:{color}, color:{_contrast_color(color)}"
44
    return ""
45
46
47
def _graph_title(graph: nx.Graph, title: str | None = None) -> str:
48
    """Generate a graph title string."""
49
    title = title if title is not None else graph.name
50
    return f"title: {title}\n" if title else ""
51
52
53
class DiagramBuilder:
54
    """
55
    A class to generate Mermaid diagrams from NetworkX graphs.
56
    """
57
58
    def __init__(
59
            self,
60
            orientation: DiagramOrientation = DiagramOrientation.LEFT_RIGHT,
61
            node_shape: DiagramNodeShape = DiagramNodeShape.DEFAULT,
62
            layout: str = DEFAULT_LAYOUT,
63
            look: str = DEFAULT_LOOK,
64
            theme: str = DEFAULT_THEME,
65
    ):
66
        """
67
        Initialize the DiagramBuilder.
68
69
        Args:
70
            orientation: DiagramOrientation - The orientation of the graph (default: LEFT_RIGHT).
71
            node_shape: DiagramNodeShape - The shape of the nodes (default: DiagramNodeShape.DEFAULT).
72
            layout: str - the layout to use (default: 'dagre')
73
            look: str - the look to use (default: 'neo')
74
            theme: str - the theme to use (default: 'neutral')
75
        """
76
        self.orientation = orientation
77
        self.node_shape = node_shape
78
        self.layout = layout
79
        self.look = look
80
        self.theme = theme
81
82
        if not isinstance(orientation, DiagramOrientation):
83
            raise TypeError("orientation must be a valid Orientation enum")
84
        if not isinstance(node_shape, DiagramNodeShape):
85
            raise TypeError("node_shape must be a valid NodeShape enum")
86
87
    def _diagram_config(self, graph, title: str | None = None) -> str:
88
        return (
89
            f"---\n"
90
            f"{_graph_title(graph, title)}"
91
            f"config:\n"
92
            f"  layout: {self.layout}\n"
93
            f"  look: {self.look}\n"
94
            f"  theme: {self.theme}\n"
95
            f"---\n"
96
        )
97
98
    def build(self, graph: nx.Graph, title: str | None = None, with_edge_labels: bool = True) -> MermaidDiagram:
99
        """
100
        Materialize a graph as a Mermaid flowchart.
101
102
        Args:
103
            graph: nx.Graph - The NetworkX graph to convert.
104
            title: str - The title of the graph (default: None).
105
                   If None, the graph name will be used if available.
106
                   Supplying and empty string will remove the title.
107
            with_edge_labels: bool - Whether to include edge labels (default: True).
108
109
        Returns:
110
            A string representation of the graph as a Mermaid graph.
111
        """
112
        config = self._diagram_config(graph, title)
113
114
        bra, ket = self.node_shape.value
115
116
        minifier = AutoMapper()
117
118
        # Pre-calculate node IDs to avoid repeated function calls
119
        node_map = {u: minifier.get(u) for u in graph.nodes()}
120
121
        nodes = "\n".join(
122
            f"{node_map[u]}{bra}{d.get('label', u)}{ket}{_node_style(node_map[u], d)}" for u, d in
123
            graph.nodes.data())
124
125
        _edges = ((node_map[u], node_map[v], d) for u, v, d in graph.edges.data())
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable d does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable u does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable v does not seem to be defined.
Loading history...
126
        edges = "\n".join(f"{u} -->{_edge_label(d) if with_edge_labels else ''} {v}" for u, v, d in _edges)
127
128
        return (
129
            f"{config}"
130
            f"graph {self.orientation.value}\n"
131
            f"{nodes}\n"
132
            f"{edges}"
133
        )
134