Passed
Push — main ( bcf228...55c602 )
by Eran
01:46
created

DiagramBuilder._diagram_config()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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