Skip to content

Commit 770fc73

Browse files
committed
Implement location clustering and category toggles in graph visualizer
1 parent e90b0eb commit 770fc73

2 files changed

Lines changed: 134 additions & 66 deletions

File tree

scripts/export_graph_mermaid.py

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,34 @@ def load_graph():
1111
def generate_mermaid():
1212
nodes = load_graph()
1313

14+
# 1. Map associations
15+
location_map = {} # QuestID -> LocationName
16+
cadence_map = {} # AbilityID -> CadenceName
17+
18+
for node in nodes:
19+
nid = node["id"]
20+
ntype = node["type"]
21+
out_edges = node.get("out_edges", {})
22+
23+
if ntype == "Location":
24+
if "contains" in out_edges:
25+
for edge in out_edges["contains"]:
26+
location_map[edge["targetId"]] = node["name"]
27+
28+
if ntype == "Cadence":
29+
for field in ["provides_ability", "unlocks_ability"]:
30+
if field in out_edges:
31+
for edge in out_edges[field]:
32+
cadence_map[edge["targetId"]] = node["name"]
33+
34+
# 2. Group nodes
35+
by_location = {} # LocName -> [NodeLines]
36+
by_cadence = {} # CadName -> [NodeLines]
37+
globals = []
38+
1439
lines = ["graph TD"]
1540

16-
# Define styles with better contrast
41+
# Styles
1742
lines.append("classDef quest fill:#4b0082,stroke:#f9f,stroke-width:2px,color:#fff;")
1843
lines.append("classDef location fill:#00008b,stroke:#ccf,stroke-width:2px,color:#fff;")
1944
lines.append("classDef cadence fill:#006400,stroke:#cfc,stroke-width:2px,color:#fff;")
@@ -22,85 +47,84 @@ def generate_mermaid():
2247
lines.append("classDef ability fill:#2f4f4f,stroke:#00ced1,stroke-width:1px,color:#fff;")
2348
lines.append("classDef root fill:#8b8b00,stroke:#ff9,stroke-width:4px,color:#fff;")
2449

25-
# We want to show almost everything now
26-
# Filter out some very common ones if they make it too noisy,
27-
# but for now let's include all for a "Complete Map"
50+
edge_lines = []
2851

2952
for node in nodes:
3053
nid = node["id"]
3154
ntype = node["type"]
3255
name = node["name"]
3356

34-
# 1. Define Node
3557
style = ntype.lower()
36-
if nid == "quest_prologue":
37-
style = "root"
38-
39-
# Shorten some names for the graph
40-
display_name = name.replace('"', "'")
41-
lines.append(f'{nid}["{display_name}"]:::{style}')
58+
if nid == "quest_prologue": style = "root"
59+
60+
node_def = f'{nid}["{name.replace('"', "'")}"]:::{style}'
61+
62+
# Assign to group
63+
if ntype == "Quest" and nid in location_map:
64+
loc = location_map[nid]
65+
if loc not in by_location: by_location[loc] = []
66+
by_location[loc].append(node_def)
67+
elif ntype == "Ability" and nid in cadence_map:
68+
cad = cadence_map[nid]
69+
if cad not in by_cadence: by_cadence[cad] = []
70+
by_cadence[cad].append(node_def)
71+
elif ntype == "Location":
72+
# Locations themselves stay global or we could group them by region?
73+
# Let's keep them global to connect subgraphs
74+
globals.append(node_def)
75+
else:
76+
globals.append(node_def)
4277

43-
# 2. In-Edges (Requirements)
78+
# Edges
4479
in_edges = node.get("in_edges", {})
80+
out_edges = node.get("out_edges", {})
4581

46-
# Quest Requirements
4782
if "requires_quest" in in_edges:
4883
for req_id in in_edges["requires_quest"]:
49-
# If Location requires Quest, it's an "Enables" link
50-
if ntype == "Location":
51-
lines.append(f"{req_id} -->|Enables| {nid}")
52-
else:
53-
lines.append(f"{req_id} -->|Requires| {nid}")
84+
label = "Enables" if ntype == "Location" else "Requires"
85+
edge_lines.append(f"{req_id} -->|{label}| {nid}")
5486

55-
# Ability Requirements (for Refinements usually)
5687
if "requires_ability" in in_edges:
5788
for req_id in in_edges["requires_ability"]:
58-
lines.append(f"{req_id} -.->|Allows| {nid}")
89+
edge_lines.append(f"{req_id} -.->|Allows| {nid}")
5990

60-
# Stat Requirements
6191
if "requires_stat" in in_edges:
6292
for stat_name, val in in_edges["requires_stat"].items():
6393
stat_id = f"stat_{stat_name.lower()}"
64-
lines.append(f"{stat_id} -.->|Requires {val}| {nid}")
94+
edge_lines.append(f"{stat_id} -.->|Req {val}| {nid}")
6595

66-
# 3. Out-Edges (Unlocks / Rewards / Productions)
67-
out_edges = node.get("out_edges", {})
68-
69-
# Cadence/Ability Unlocks
70-
for unlock_type in ["unlocks_cadence", "unlocks_ability", "provides_ability"]:
96+
for unlock_type in ["unlocks_cadence", "unlocks_ability", "provides_ability", "unlocks_location"]:
7197
if unlock_type in out_edges:
7298
for edge in out_edges[unlock_type]:
73-
target_id = edge["targetId"]
74-
lines.append(f"{nid} ==>|Unlocks| {target_id}")
75-
76-
# Location Unlocks (explicit)
77-
if "unlocks_location" in out_edges:
78-
for edge in out_edges["unlocks_location"]:
79-
target_id = edge["targetId"]
80-
lines.append(f"{nid} ==>|Unlocks| {target_id}")
99+
edge_lines.append(f"{nid} ==>|Unlocks| {edge['targetId']}")
81100

82-
# Item Rewards / Production
83101
for reward_type in ["rewards", "produces", "rewards_item"]:
84102
if reward_type in out_edges:
85103
for edge in out_edges[reward_type]:
86-
target_id = edge["targetId"]
87104
qty = edge.get("quantity", 1)
88-
lines.append(f"{nid} --o|Gives {qty}| {target_id}")
105+
edge_lines.append(f"{nid} --o|Gives {qty}| {edge['targetId']}")
89106

90-
# Item Consumption
91107
if "consumes" in out_edges:
92108
for edge in out_edges["consumes"]:
93-
target_id = edge["targetId"]
94109
qty = edge.get("quantity", 1)
95-
# Link Item to the consumer
96-
lines.append(f"{target_id} --x|Consumes {qty}| {nid}")
97-
98-
# Containment (Optional, but useful for locations)
99-
if ntype == "Location" and "contains" in out_edges:
100-
for edge in out_edges["contains"]:
101-
target_id = edge["targetId"]
102-
# lines.append(f"{nid} -.->|Home of| {target_id}") # Usually too noisy
110+
edge_lines.append(f"{edge['targetId']} --x|Consumes {qty}| {nid}")
103111

112+
# Build final output
113+
for node_def in globals:
114+
lines.append(node_def)
115+
116+
for loc, nodes_in_loc in by_location.items():
117+
lines.append(f'subgraph "Location: {loc}"')
118+
lines.extend(nodes_in_loc)
119+
lines.append("end")
120+
121+
for cad, nodes_in_cad in by_cadence.items():
122+
lines.append(f'subgraph "Cadence: {cad}"')
123+
lines.extend(nodes_in_cad)
124+
lines.append("end")
125+
126+
lines.extend(edge_lines)
127+
104128
return "\n".join(lines)
105129

106130
if __name__ == "__main__":

scripts/visualize_graph.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import webbrowser
3-
import tempfile
43
from export_graph_mermaid import generate_mermaid
54

65
def generate_html(mermaid_code):
@@ -24,12 +23,31 @@ def generate_html(mermaid_code):
2423
padding: 10px 20px;
2524
border-bottom: 1px solid #30363d;
2625
display: flex;
27-
justify-content: space-between;
28-
align-items: center;
26+
flex-direction: column;
2927
position: sticky;
3028
top: 0;
3129
z-index: 100;
3230
}}
31+
.header-top {{
32+
display: flex;
33+
justify-content: space-between;
34+
align-items: center;
35+
margin-bottom: 10px;
36+
}}
37+
.controls {{
38+
display: flex;
39+
gap: 20px;
40+
background: #21262d;
41+
padding: 8px 15px;
42+
border-radius: 6px;
43+
font-size: 0.9em;
44+
}}
45+
.control-item {{
46+
display: flex;
47+
align-items: center;
48+
gap: 8px;
49+
cursor: pointer;
50+
}}
3351
.legend {{
3452
display: flex;
3553
gap: 15px;
@@ -46,7 +64,7 @@ def generate_html(mermaid_code):
4664
4765
#graph-container {{
4866
width: 100vw;
49-
height: calc(100vh - 60px);
67+
height: calc(100vh - 100px);
5068
overflow: auto;
5169
padding: 40px;
5270
box-sizing: border-box;
@@ -57,20 +75,41 @@ def generate_html(mermaid_code):
5775
margin: 0;
5876
display: inline-block;
5977
}}
78+
79+
/* Toggling Logic */
80+
body.hide-items .item, body.hide-items [class*="Gives"], body.hide-items [class*="Consumes"] {{ display: none !important; }}
81+
body.hide-stats .stat, body.hide-stats [class*="Req"] {{ display: none !important; }}
82+
body.hide-abilities .ability, body.hide-abilities .cadence, body.hide-abilities [class*="Allows"] {{ display: none !important; }}
83+
84+
/* Mermaid Specific SVG targeting */
85+
body.hide-items g.node.item, body.hide-items g.edgePath path[stroke*="o"], body.hide-items g.edgePath path[stroke*="x"] {{ opacity: 0; pointer-events: none; }}
86+
/* Standard CSS display:none doesn't work well on SVG elements inside Mermaid sometimes, so we use opacity/pointer-events */
6087
</style>
6188
</head>
62-
<body style="overflow: hidden;">
89+
<body>
6390
<div class="header">
64-
<div>
65-
<h2 style="margin:0; font-size: 1.2em;">Mythril Content Graph</h2>
91+
<div class="header-top">
92+
<h2 style="margin:0; font-size: 1.2em;">Mythril Content Graph (Clustered)</h2>
93+
<div class="legend">
94+
<div class="legend-item"><div class="legend-box quest-box"></div> Quest</div>
95+
<div class="legend-item"><div class="legend-box location-box"></div> Location</div>
96+
<div class="legend-item"><div class="legend-box cadence-box"></div> Cadence</div>
97+
<div class="legend-item"><div class="legend-box item-box"></div> Item</div>
98+
<div class="legend-item"><div class="legend-box stat-box"></div> Stat</div>
99+
<div class="legend-item"><div class="legend-box ability-box"></div> Ability</div>
100+
</div>
66101
</div>
67-
<div class="legend">
68-
<div class="legend-item"><div class="legend-box quest-box"></div> Quest</div>
69-
<div class="legend-item"><div class="legend-box location-box"></div> Location</div>
70-
<div class="legend-item"><div class="legend-box cadence-box"></div> Cadence</div>
71-
<div class="legend-item"><div class="legend-box item-box"></div> Item</div>
72-
<div class="legend-item"><div class="legend-box stat-box"></div> Stat</div>
73-
<div class="legend-item"><div class="legend-box ability-box"></div> Ability</div>
102+
<div class="controls">
103+
<strong>Visibility Toggles:</strong>
104+
<label class="control-item">
105+
<input type="checkbox" checked onchange="toggleCategory('hide-items', this.checked)"> Items & Economy
106+
</label>
107+
<label class="control-item">
108+
<input type="checkbox" checked onchange="toggleCategory('hide-stats', this.checked)"> Stat Requirements
109+
</label>
110+
<label class="control-item">
111+
<input type="checkbox" checked onchange="toggleCategory('hide-abilities', this.checked)"> Cadences & Abilities
112+
</label>
74113
</div>
75114
</div>
76115
<div id="graph-container">
@@ -90,19 +129,26 @@ def generate_html(mermaid_code):
90129
padding: 50
91130
}}
92131
}});
132+
133+
function toggleCategory(className, isVisible) {{
134+
if (isVisible) {{
135+
document.body.classList.remove(className);
136+
}} else {{
137+
document.body.classList.add(className);
138+
}}
139+
}}
93140
</script>
94141
</body>
95142
</html>
96143
"""
97144
return html_template
98145

99146
def main():
100-
print("Generating Mermaid graph content...")
147+
print("Generating Clustered Mermaid graph...")
101148
try:
102149
mermaid_code = generate_mermaid()
103150
html = generate_html(mermaid_code)
104151

105-
# Ensure output directory exists
106152
output_dir = "output"
107153
if not os.path.exists(output_dir):
108154
os.makedirs(output_dir)
@@ -113,8 +159,6 @@ def main():
113159

114160
abs_path = os.path.abspath(file_path)
115161
print(f"Graph generated: {abs_path}")
116-
117-
# Open in browser
118162
print("Opening browser...")
119163
webbrowser.open(f"file://{abs_path}")
120164

0 commit comments

Comments
 (0)