Skip to content

Commit aa33681

Browse files
committed
Fix graph visualizer by downgrading Mermaid and improving subgraph generation logic
1 parent 0f08691 commit aa33681

14 files changed

Lines changed: 1322 additions & 517 deletions

output/graph_debug.png

13.2 KB
Loading
16.6 KB
Loading
Lines changed: 253 additions & 245 deletions
Large diffs are not rendered by default.

output/graph_visualizer_test.html

Lines changed: 574 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"playwright": "^1.58.2"
4+
}
5+
}

scripts/check_mermaid_integrity.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import re
2+
import os
3+
4+
def check_integrity(file_path):
5+
with open(file_path, 'r', encoding='utf-8') as f:
6+
content = f.read()
7+
8+
# Extract mermaid section
9+
match = re.search(r'<div class="mermaid">\s*(.*?)\s*</div>', content, re.DOTALL)
10+
if not match:
11+
print("Could not find mermaid section in HTML.")
12+
return
13+
14+
mermaid_code = match.group(1)
15+
lines = mermaid_code.split('\n')
16+
17+
defined_nodes = set()
18+
referenced_nodes = set()
19+
20+
# Simple regex for node definition: id["Label"]:::style or id["Label"] or id
21+
# Note: id can be anything alphanumeric + underscores in this project
22+
node_def_re = re.compile(r'^(\w+)\[".*?"\](:::(\w+))?')
23+
# Simple regex for edges: id1 -- edge -- id2
24+
edge_re = re.compile(r'(\w+)\s*([-=.][->ox]+|--o|--x|==>|-.->)\s*(\|".*?"\|)?\s*(\w+)')
25+
26+
for line in lines:
27+
line = line.strip()
28+
if not line or line.startswith('graph') or line.startswith('classDef') or line.startswith('subgraph') or line.startswith('end'):
29+
continue
30+
31+
m = node_def_re.match(line)
32+
if m:
33+
defined_nodes.add(m.group(1))
34+
continue
35+
36+
m = edge_re.search(line)
37+
if m:
38+
referenced_nodes.add(m.group(1))
39+
referenced_nodes.add(m.group(4))
40+
41+
missing = referenced_nodes - defined_nodes
42+
if missing:
43+
print(f"Found {len(missing)} referenced nodes that are NOT defined:")
44+
for m in sorted(missing):
45+
# Some nodes might be "stat_*" which are defined later or implicitly
46+
# Let's check if they are defined anywhere in the file
47+
if f'{m}["' in mermaid_code or f'{m}:::' in mermaid_code:
48+
continue
49+
print(f" - {m}")
50+
else:
51+
print("All referenced nodes are defined.")
52+
53+
if __name__ == "__main__":
54+
check_integrity("output/graph_visualizer_test.html")

scripts/export_graph_mermaid.py

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import json
22
import os
33
import sys
4+
import re
45

56
GRAPH_FILE = "Mythril.Blazor/wwwroot/data/content_graph.json"
67

78
def load_graph():
89
with open(GRAPH_FILE, 'r', encoding='utf-8') as f:
910
return json.load(f)
1011

12+
def clean_label(label):
13+
if not label: return "Untitled"
14+
# Rigorous cleaning
15+
label = label.replace('"', "'").replace(":", "-").replace("[", "(").replace("]", ")")
16+
label = "".join(c for c in label if c.isprintable() or c == " ")
17+
label = " ".join(label.split()).strip()
18+
return label if label else "Untitled"
19+
1120
def generate_mermaid():
1221
nodes = load_graph()
1322

@@ -32,114 +41,127 @@ def generate_mermaid():
3241
cadence_map[edge["targetId"]] = node["name"]
3342

3443
# 2. Group nodes
35-
by_location = {} # LocName -> [NodeLines]
36-
by_cadence = {} # CadName -> [NodeLines]
37-
globals = []
44+
by_location = {} # LocName -> [NodeIDs]
45+
by_cadence = {} # CadName -> [NodeIDs]
3846

39-
lines = ["graph TD"]
47+
lines = ["flowchart TD"]
4048

4149
# Styles
42-
lines.append("classDef quest fill:#4b0082,stroke:#f9f,stroke-width:2px,color:#fff;")
43-
lines.append("classDef location fill:#00008b,stroke:#ccf,stroke-width:2px,color:#fff;")
44-
lines.append("classDef cadence fill:#006400,stroke:#cfc,stroke-width:2px,color:#fff;")
45-
lines.append("classDef item fill:#444,stroke:#fff,stroke-width:1px,color:#fff;")
46-
lines.append("classDef stat fill:#8b4513,stroke:#ff8c00,stroke-width:1px,color:#fff;")
47-
lines.append("classDef ability fill:#2f4f4f,stroke:#00ced1,stroke-width:1px,color:#fff;")
48-
lines.append("classDef refinement fill:#ff4500,stroke:#fff,stroke-width:1px,color:#fff;")
49-
lines.append("classDef root fill:#8b8b00,stroke:#ff9,stroke-width:4px,color:#fff;")
50+
lines.append("classDef quest fill:#4b0082,stroke:#f9f,stroke-width:2px,color:#fff")
51+
lines.append("classDef location fill:#00008b,stroke:#ccf,stroke-width:2px,color:#fff")
52+
lines.append("classDef cadence fill:#006400,stroke:#cfc,stroke-width:2px,color:#fff")
53+
lines.append("classDef item fill:#444,stroke:#fff,stroke-width:1px,color:#fff")
54+
lines.append("classDef stat fill:#8b4513,stroke:#ff8c00,stroke-width:1px,color:#fff")
55+
lines.append("classDef ability fill:#2f4f4f,stroke:#00ced1,stroke-width:1px,color:#fff")
56+
lines.append("classDef refinement fill:#ff4500,stroke:#fff,stroke-width:1px,color:#fff")
57+
lines.append("classDef root fill:#8b8b00,stroke:#ff9,stroke-width:4px,color:#fff")
5058

5159
edge_lines = []
60+
processed_ids = set()
61+
node_defs = []
62+
63+
# Pre-define stat nodes
64+
for stat in ["Strength", "Vitality", "Magic", "Speed"]:
65+
stat_id = f"stat_{stat.lower()}"
66+
node_defs.append(f'{stat_id}["{stat}"]:::stat')
67+
processed_ids.add(stat_id)
5268

5369
for node in nodes:
5470
nid = node["id"].strip()
5571
ntype = node["type"]
56-
# Rigorous cleaning
57-
raw_name = node["name"].replace('"', "'").replace(":", "-")
58-
name = "".join(c for c in raw_name if c.isprintable() or c == " ")
59-
name = " ".join(name.split()).strip()
6072

6173
# IGNORE GOLD
6274
if nid == "item_gold":
6375
continue
6476

77+
name = clean_label(node["name"])
6578
style = ntype.lower()
6679
if nid == "quest_prologue": style = "root"
6780

6881
node_def = f'{nid}["{name}"]:::{style}'
82+
node_defs.append(node_def)
83+
processed_ids.add(nid)
6984

7085
# Assign to group
7186
if ntype == "Quest" and nid in location_map:
72-
raw_loc = location_map[nid].replace(":", "-")
73-
loc = "".join(c for c in raw_loc if c.isprintable() or c == " ")
74-
loc = " ".join(loc.split()).strip()
87+
loc = clean_label(location_map[nid])
7588
if loc not in by_location: by_location[loc] = []
76-
by_location[loc].append(node_def)
89+
by_location[loc].append(nid)
7790
elif ntype == "Ability" and nid in cadence_map:
78-
raw_cad = cadence_map[nid].replace(":", "-")
79-
cad = "".join(c for c in raw_cad if c.isprintable() or c == " ")
80-
cad = " ".join(cad.split()).strip()
91+
cad = clean_label(cadence_map[nid])
8192
if cad not in by_cadence: by_cadence[cad] = []
82-
by_cadence[cad].append(node_def)
83-
else:
84-
globals.append(node_def)
93+
by_cadence[cad].append(nid)
8594

86-
# Edges
95+
# Edges (same as before)
8796
in_edges = node.get("in_edges", {})
8897
out_edges = node.get("out_edges", {})
8998

9099
if "requires_quest" in in_edges:
91100
for req_id in in_edges["requires_quest"]:
92101
label = "Enables" if ntype == "Location" else "Requires"
93-
edge_lines.append(f"{req_id} -->|{label}| {nid}")
102+
edge_lines.append(f'{req_id} -->|"{label}"| {nid}')
94103

95104
if "requires_ability" in in_edges:
96105
for req_id in in_edges["requires_ability"]:
97-
edge_lines.append(f"{req_id} -.->|Allows| {nid}")
106+
edge_lines.append(f'{req_id} -->|"Allows"| {nid}')
98107

99108
if "requires_stat" in in_edges:
100109
for stat_name, val in in_edges["requires_stat"].items():
101110
stat_id = f"stat_{stat_name.lower()}"
102-
edge_lines.append(f"{stat_id} -.->|Req {val}| {nid}")
111+
edge_lines.append(f'{stat_id} -->|"Req {val}"| {nid}')
103112

104113
for unlock_type in ["unlocks_cadence", "unlocks_ability", "provides_ability", "unlocks_location"]:
105114
if unlock_type in out_edges:
106115
for edge in out_edges[unlock_type]:
107-
edge_lines.append(f"{nid} ==>|Unlocks| {edge['targetId']}")
116+
edge_lines.append(f'{nid} -->|"Unlocks"| {edge["targetId"]}')
108117

109118
for reward_type in ["rewards", "produces", "rewards_item"]:
110119
if reward_type in out_edges:
111120
for edge in out_edges[reward_type]:
112121
target_id = edge["targetId"]
113-
# IGNORE GOLD EDGES
114122
if target_id == "item_gold": continue
115-
116123
qty = edge.get("quantity", 1)
117-
edge_lines.append(f"{nid} --o|Gives {qty}| {target_id}")
124+
edge_lines.append(f'{nid} --o|"Gives {qty}"| {target_id}')
118125

119126
if "consumes" in out_edges:
120127
for edge in out_edges["consumes"]:
121128
target_id = edge["targetId"]
122-
# IGNORE GOLD EDGES
123129
if target_id == "item_gold": continue
124-
125130
qty = edge.get("quantity", 1)
126-
edge_lines.append(f"{target_id} --x|Consumes {qty}| {nid}")
131+
edge_lines.append(f'{target_id} --x|"Consumes {qty}"| {nid}')
127132

128133
# Build final output
129-
for node_def in globals:
130-
lines.append(node_def)
134+
lines.extend(node_defs)
131135

132136
for loc, nodes_in_loc in by_location.items():
133-
lines.append(f'subgraph "Location - {loc}"')
137+
# Sanitize loc for ID
138+
loc_id = "loc_" + re.sub(r'\W+', '_', loc.lower())
139+
lines.append(f'subgraph {loc_id} ["Location - {loc}"]')
134140
lines.extend(nodes_in_loc)
135141
lines.append("end")
136142

137143
for cad, nodes_in_cad in by_cadence.items():
138-
lines.append(f'subgraph "Cadence - {cad}"')
144+
# Sanitize cad for ID
145+
cad_id = "cad_" + re.sub(r'\W+', '_', cad.lower())
146+
lines.append(f'subgraph {cad_id} ["Cadence - {cad}"]')
139147
lines.extend(nodes_in_cad)
140148
lines.append("end")
141149

142-
lines.extend(edge_lines)
150+
# Final filter to ensure no edges to missing nodes and no self-edges
151+
valid_edges = []
152+
for edge in edge_lines:
153+
# Simple extraction of IDs from "id1 --o|...| id2" or "id1 --x|...| id2" or "id1 -->|...| id2"
154+
match = re.search(r'^(\w+)\s*--[->ox]\s*\|".*?"\|\s*(\w+)$', edge)
155+
if match:
156+
source_id = match.group(1)
157+
target_id = match.group(2)
158+
if source_id in processed_ids and target_id in processed_ids:
159+
if source_id != target_id:
160+
valid_edges.append(edge)
161+
else:
162+
print(f"Filtered out self-edge: {source_id} --> {target_id}")
163+
164+
lines.extend(valid_edges)
143165

144166
return "\n".join(lines)
145167

scripts/generate_html_only.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import sys
3+
from export_graph_mermaid import generate_mermaid
4+
from visualize_graph import generate_html
5+
6+
def main():
7+
print("Generating Clustered Mermaid graph HTML only...")
8+
try:
9+
mermaid_code = generate_mermaid()
10+
html = generate_html(mermaid_code)
11+
12+
output_dir = "output"
13+
if not os.path.exists(output_dir):
14+
os.makedirs(output_dir)
15+
16+
file_path = os.path.join(output_dir, "graph_visualizer_test.html")
17+
with open(file_path, "w", encoding="utf-8") as f:
18+
f.write(html)
19+
20+
abs_path = os.path.abspath(file_path)
21+
print(f"Graph generated: {abs_path}")
22+
23+
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
24+
print("[SUCCESS] HTML file generated and is not empty.")
25+
sys.exit(0)
26+
else:
27+
print("[FAIL] HTML file was not generated or is empty.")
28+
sys.exit(1)
29+
30+
except Exception as e:
31+
print(f"Error: {e}")
32+
sys.exit(1)
33+
34+
if __name__ == "__main__":
35+
main()

scripts/test_render.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const { chromium } = require('playwright');
2+
const path = require('path');
3+
const fs = require('fs');
4+
5+
(async () => {
6+
const browser = await chromium.launch();
7+
const context = await browser.newContext();
8+
const page = await context.newPage();
9+
10+
page.on('console', msg => {
11+
console.log(`PAGE LOG [${msg.type()}]: ${msg.text()}`);
12+
});
13+
14+
const filePath = path.resolve('output/graph_visualizer_test.html');
15+
const url = 'file://' + filePath;
16+
console.log('Loading ' + url);
17+
18+
await page.goto(url);
19+
20+
// Wait for either the SVG or the error message
21+
console.log('Waiting for mermaid output...');
22+
await page.waitForTimeout(5000); // Give it plenty of time
23+
24+
// Take a full page screenshot
25+
const screenshotPath = 'output/graph_debug.png';
26+
await page.screenshot({ path: screenshotPath, fullPage: true });
27+
console.log(`Screenshot saved to ${screenshotPath}`);
28+
29+
// Check for error text in the page
30+
const content = await page.content();
31+
if (content.includes('Syntax error') || content.includes('Parser error')) {
32+
console.log('[FAIL] Syntax error detected in page content.');
33+
} else {
34+
const svgExists = await page.locator('.mermaid svg').count() > 0;
35+
console.log(`SVG element exists: ${svgExists}`);
36+
}
37+
38+
await browser.close();
39+
})();

0 commit comments

Comments
 (0)