Passed
Push — main ( 44846b...a5b16f )
by Eran
01:42
created

networkx_mermaid.builders._node_id()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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