|
cd8f2f9…
|
leo
|
1 |
"""Mermaid rendering and chart reproduction utilities.""" |
|
cd8f2f9…
|
leo
|
2 |
|
|
cd8f2f9…
|
leo
|
3 |
import logging |
|
cd8f2f9…
|
leo
|
4 |
from pathlib import Path |
|
829e24a…
|
leo
|
5 |
from typing import Dict |
|
cd8f2f9…
|
leo
|
6 |
|
|
cd8f2f9…
|
leo
|
7 |
logger = logging.getLogger(__name__) |
|
cd8f2f9…
|
leo
|
8 |
|
|
cd8f2f9…
|
leo
|
9 |
|
|
cd8f2f9…
|
leo
|
10 |
def render_mermaid(mermaid_code: str, output_dir: str | Path, name: str) -> Dict[str, Path]: |
|
cd8f2f9…
|
leo
|
11 |
""" |
|
cd8f2f9…
|
leo
|
12 |
Render mermaid code to SVG and PNG files. |
|
cd8f2f9…
|
leo
|
13 |
|
|
cd8f2f9…
|
leo
|
14 |
Writes {name}.mermaid (source), {name}.svg, and {name}.png. |
|
cd8f2f9…
|
leo
|
15 |
Uses mermaid-py if available, falls back gracefully. |
|
cd8f2f9…
|
leo
|
16 |
|
|
cd8f2f9…
|
leo
|
17 |
Returns dict with keys: mermaid, svg, png (Paths to generated files). |
|
cd8f2f9…
|
leo
|
18 |
""" |
|
cd8f2f9…
|
leo
|
19 |
output_dir = Path(output_dir) |
|
cd8f2f9…
|
leo
|
20 |
output_dir.mkdir(parents=True, exist_ok=True) |
|
cd8f2f9…
|
leo
|
21 |
result: Dict[str, Path] = {} |
|
cd8f2f9…
|
leo
|
22 |
|
|
cd8f2f9…
|
leo
|
23 |
# Always write source |
|
cd8f2f9…
|
leo
|
24 |
mermaid_path = output_dir / f"{name}.mermaid" |
|
cd8f2f9…
|
leo
|
25 |
mermaid_path.write_text(mermaid_code) |
|
cd8f2f9…
|
leo
|
26 |
result["mermaid"] = mermaid_path |
|
cd8f2f9…
|
leo
|
27 |
|
|
cd8f2f9…
|
leo
|
28 |
try: |
|
cd8f2f9…
|
leo
|
29 |
import mermaid as mmd |
|
cd8f2f9…
|
leo
|
30 |
from mermaid.graph import Graph |
|
cd8f2f9…
|
leo
|
31 |
|
|
cd8f2f9…
|
leo
|
32 |
graph = Graph("diagram", mermaid_code) |
|
cd8f2f9…
|
leo
|
33 |
rendered = mmd.Mermaid(graph) |
|
cd8f2f9…
|
leo
|
34 |
|
|
cd8f2f9…
|
leo
|
35 |
# SVG |
|
cd8f2f9…
|
leo
|
36 |
svg_path = output_dir / f"{name}.svg" |
|
cd8f2f9…
|
leo
|
37 |
svg_content = rendered.svg_response |
|
cd8f2f9…
|
leo
|
38 |
if svg_content: |
|
cd8f2f9…
|
leo
|
39 |
if isinstance(svg_content, bytes): |
|
cd8f2f9…
|
leo
|
40 |
svg_path.write_bytes(svg_content) |
|
cd8f2f9…
|
leo
|
41 |
else: |
|
cd8f2f9…
|
leo
|
42 |
svg_path.write_text(svg_content) |
|
cd8f2f9…
|
leo
|
43 |
result["svg"] = svg_path |
|
cd8f2f9…
|
leo
|
44 |
|
|
cd8f2f9…
|
leo
|
45 |
# PNG |
|
cd8f2f9…
|
leo
|
46 |
png_path = output_dir / f"{name}.png" |
|
cd8f2f9…
|
leo
|
47 |
png_content = rendered.img_response |
|
cd8f2f9…
|
leo
|
48 |
if png_content: |
|
cd8f2f9…
|
leo
|
49 |
if isinstance(png_content, bytes): |
|
cd8f2f9…
|
leo
|
50 |
png_path.write_bytes(png_content) |
|
cd8f2f9…
|
leo
|
51 |
else: |
|
829e24a…
|
leo
|
52 |
png_path.write_bytes( |
|
829e24a…
|
leo
|
53 |
png_content.encode() if isinstance(png_content, str) else png_content |
|
829e24a…
|
leo
|
54 |
) |
|
cd8f2f9…
|
leo
|
55 |
result["png"] = png_path |
|
cd8f2f9…
|
leo
|
56 |
|
|
cd8f2f9…
|
leo
|
57 |
except ImportError: |
|
829e24a…
|
leo
|
58 |
logger.warning( |
|
829e24a…
|
leo
|
59 |
"mermaid-py not installed, skipping SVG/PNG rendering. " |
|
829e24a…
|
leo
|
60 |
"Install with: pip install mermaid-py" |
|
829e24a…
|
leo
|
61 |
) |
|
cd8f2f9…
|
leo
|
62 |
except Exception as e: |
|
cd8f2f9…
|
leo
|
63 |
logger.warning(f"Mermaid rendering failed for '{name}': {e}") |
|
cd8f2f9…
|
leo
|
64 |
|
|
cd8f2f9…
|
leo
|
65 |
return result |
|
cd8f2f9…
|
leo
|
66 |
|
|
cd8f2f9…
|
leo
|
67 |
|
|
cd8f2f9…
|
leo
|
68 |
def reproduce_chart( |
|
cd8f2f9…
|
leo
|
69 |
chart_data: dict, |
|
cd8f2f9…
|
leo
|
70 |
output_dir: str | Path, |
|
cd8f2f9…
|
leo
|
71 |
name: str, |
|
cd8f2f9…
|
leo
|
72 |
) -> Dict[str, Path]: |
|
cd8f2f9…
|
leo
|
73 |
""" |
|
cd8f2f9…
|
leo
|
74 |
Reproduce a chart from extracted data using matplotlib. |
|
cd8f2f9…
|
leo
|
75 |
|
|
cd8f2f9…
|
leo
|
76 |
chart_data should contain: labels, values, chart_type (bar/line/pie/scatter). |
|
cd8f2f9…
|
leo
|
77 |
Returns dict with keys: svg, png (Paths to generated files). |
|
cd8f2f9…
|
leo
|
78 |
""" |
|
cd8f2f9…
|
leo
|
79 |
output_dir = Path(output_dir) |
|
cd8f2f9…
|
leo
|
80 |
output_dir.mkdir(parents=True, exist_ok=True) |
|
cd8f2f9…
|
leo
|
81 |
result: Dict[str, Path] = {} |
|
cd8f2f9…
|
leo
|
82 |
|
|
cd8f2f9…
|
leo
|
83 |
labels = chart_data.get("labels", []) |
|
cd8f2f9…
|
leo
|
84 |
values = chart_data.get("values", []) |
|
cd8f2f9…
|
leo
|
85 |
chart_type = chart_data.get("chart_type", "bar") |
|
cd8f2f9…
|
leo
|
86 |
|
|
cd8f2f9…
|
leo
|
87 |
if not labels or not values: |
|
cd8f2f9…
|
leo
|
88 |
logger.warning(f"Insufficient chart data for '{name}': missing labels or values") |
|
cd8f2f9…
|
leo
|
89 |
return result |
|
cd8f2f9…
|
leo
|
90 |
|
|
cd8f2f9…
|
leo
|
91 |
try: |
|
cd8f2f9…
|
leo
|
92 |
import matplotlib |
|
cd8f2f9…
|
leo
|
93 |
|
|
cd8f2f9…
|
leo
|
94 |
matplotlib.use("Agg") # Non-interactive backend |
|
cd8f2f9…
|
leo
|
95 |
import matplotlib.pyplot as plt |
|
cd8f2f9…
|
leo
|
96 |
|
|
cd8f2f9…
|
leo
|
97 |
fig, ax = plt.subplots(figsize=(10, 6)) |
|
cd8f2f9…
|
leo
|
98 |
|
|
cd8f2f9…
|
leo
|
99 |
if chart_type == "bar": |
|
cd8f2f9…
|
leo
|
100 |
ax.bar(labels, values) |
|
cd8f2f9…
|
leo
|
101 |
elif chart_type == "line": |
|
cd8f2f9…
|
leo
|
102 |
ax.plot(labels, values, marker="o") |
|
cd8f2f9…
|
leo
|
103 |
elif chart_type == "pie": |
|
cd8f2f9…
|
leo
|
104 |
ax.pie(values, labels=labels, autopct="%1.1f%%") |
|
cd8f2f9…
|
leo
|
105 |
elif chart_type == "scatter": |
|
cd8f2f9…
|
leo
|
106 |
ax.scatter(range(len(values)), values) |
|
cd8f2f9…
|
leo
|
107 |
if labels: |
|
cd8f2f9…
|
leo
|
108 |
ax.set_xticks(range(len(labels))) |
|
cd8f2f9…
|
leo
|
109 |
ax.set_xticklabels(labels, rotation=45, ha="right") |
|
cd8f2f9…
|
leo
|
110 |
else: |
|
cd8f2f9…
|
leo
|
111 |
ax.bar(labels, values) |
|
cd8f2f9…
|
leo
|
112 |
|
|
cd8f2f9…
|
leo
|
113 |
if chart_type != "pie": |
|
cd8f2f9…
|
leo
|
114 |
ax.set_xlabel("") |
|
cd8f2f9…
|
leo
|
115 |
ax.set_ylabel("") |
|
cd8f2f9…
|
leo
|
116 |
plt.xticks(rotation=45, ha="right") |
|
cd8f2f9…
|
leo
|
117 |
|
|
cd8f2f9…
|
leo
|
118 |
plt.tight_layout() |
|
cd8f2f9…
|
leo
|
119 |
|
|
cd8f2f9…
|
leo
|
120 |
# SVG |
|
cd8f2f9…
|
leo
|
121 |
svg_path = output_dir / f"{name}_chart.svg" |
|
cd8f2f9…
|
leo
|
122 |
fig.savefig(svg_path, format="svg") |
|
cd8f2f9…
|
leo
|
123 |
result["svg"] = svg_path |
|
cd8f2f9…
|
leo
|
124 |
|
|
cd8f2f9…
|
leo
|
125 |
# PNG |
|
cd8f2f9…
|
leo
|
126 |
png_path = output_dir / f"{name}_chart.png" |
|
cd8f2f9…
|
leo
|
127 |
fig.savefig(png_path, format="png", dpi=150) |
|
cd8f2f9…
|
leo
|
128 |
result["png"] = png_path |
|
cd8f2f9…
|
leo
|
129 |
|
|
cd8f2f9…
|
leo
|
130 |
plt.close(fig) |
|
cd8f2f9…
|
leo
|
131 |
|
|
cd8f2f9…
|
leo
|
132 |
except ImportError: |
|
cd8f2f9…
|
leo
|
133 |
logger.warning("matplotlib not installed, skipping chart reproduction") |
|
cd8f2f9…
|
leo
|
134 |
except Exception as e: |
|
cd8f2f9…
|
leo
|
135 |
logger.warning(f"Chart reproduction failed for '{name}': {e}") |
|
cd8f2f9…
|
leo
|
136 |
|
|
cd8f2f9…
|
leo
|
137 |
return result |