Skip to content

Commit 2ae0178

Browse files
authored
Add dark theme, logo, and enhance dashboard features (#15)
* adding dark theme and also a logo for the program * Increasing the height of mol subviewer * Adding several features to the dashboard. Also adding a temporary logo for the software MHCXGraph.
1 parent f068728 commit 2ae0178

9 files changed

Lines changed: 3180 additions & 539 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ share/python-wheels/
2929
.installed.cfg
3030
*.egg
3131
MANIFEST
32+
results/
3233

3334
# PyInstaller
3435
# Usually these files are written by a python script from a template

MHCXGraph/app.py

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,73 @@
1515
from MHCXGraph.workflow.association import run_association_task
1616
from MHCXGraph.workflow.manifest import build_association_config, load_manifest
1717

18+
def create_master_dashboard(export_data, output_dir, log):
19+
"""Helper to inject the master aggregated JSON into the dashboard template."""
20+
import base64
21+
import json
22+
23+
assets_dir = Path(__file__).resolve().parent / "assets"
24+
25+
# Safely load local Frameworks or fallback to CDNs
26+
vis_local = assets_dir / "vis-network.min.js"
27+
if vis_local.exists():
28+
vis_injection = f'<script>\n{vis_local.read_text(encoding="utf-8")}\n</script>'
29+
else:
30+
vis_injection = '<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>'
31+
32+
mol3d_local = assets_dir / "3Dmol-min.js"
33+
if mol3d_local.exists():
34+
mol3d_injection = f'<script>\n{mol3d_local.read_text(encoding="utf-8")}\n</script>'
35+
else:
36+
mol3d_injection = '<script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>'
37+
38+
mhcx_logo_path = assets_dir / "MHCXGraph logo.png"
39+
mhcx_logo_injection = "<h2>MHCXGraph</h2>"
40+
favicon_injection = ""
41+
if mhcx_logo_path.exists():
42+
with open(mhcx_logo_path, "rb") as image_file:
43+
encoded = base64.b64encode(image_file.read()).decode("utf-8")
44+
mhcx_logo_injection = f'<img src="data:image/png;base64,{encoded}" alt="MHCXGraph Logo" style="max-width: 80%; height: auto;">'
45+
favicon_injection = f'<link rel="icon" type="image/png" href="data:image/png;base64,{encoded}">'
46+
47+
logo_dark_path = assets_dir / "LNBio white.png"
48+
logo_light_path = assets_dir / "LNBio.png"
49+
logo_injection = ""
50+
if logo_light_path.exists():
51+
with open(logo_light_path, "rb") as image_file:
52+
encoded = base64.b64encode(image_file.read()).decode("utf-8")
53+
logo_injection += f'<img src="data:image/png;base64,{encoded}" alt="LNBio Logo" class="logo-light" style="height: 7rem; width: auto;">'
54+
if logo_dark_path.exists():
55+
with open(logo_dark_path, "rb") as image_file:
56+
encoded = base64.b64encode(image_file.read()).decode("utf-8")
57+
logo_injection += f'<img src="data:image/png;base64,{encoded}" alt="LNBio Logo" class="logo-dark" style="height: 7rem; width: auto;">'
58+
if not logo_injection:
59+
log.debug("LNBio logos not found in assets/. Skipping logo injection.")
60+
61+
62+
template_path = assets_dir / "dashboard_template.html"
63+
try:
64+
with open(template_path, "r", encoding="utf-8") as f:
65+
html_template = f.read()
66+
except FileNotFoundError:
67+
log.error(f"Template not found at {template_path}.")
68+
return
69+
70+
final_html = html_template.replace("__GRAPH_DATA_INJECTION__", json.dumps(export_data))
71+
final_html = final_html.replace("__FAVICON_INJECTION__", favicon_injection)
72+
final_html = final_html.replace("__VIS_JS_INJECTION__", vis_injection)
73+
final_html = final_html.replace("__3DMOL_JS_INJECTION__", mol3d_injection)
74+
final_html = final_html.replace("__MHCXGRAPH_LOGO_INJECTION__", mhcx_logo_injection)
75+
final_html = final_html.replace("__LNBIO_LOGO_INJECTION__", logo_injection)
76+
77+
# Dynamically name the output file based on the mode
78+
mode = export_data.get("mode", "all")
79+
file_name = "Dashboard_Pairs.html" if mode == "pair" else "Dashboard_All.html"
80+
81+
full_path = output_dir / file_name
82+
with open(str(full_path), "w+", encoding="utf-8") as out:
83+
out.write(final_html)
84+
log.info(f"Interactive Dashboard saved to {full_path}")
1885

1986
def setup_trackers(output_dir, settings):
2087
"""
@@ -85,14 +152,27 @@ def run_all_mode(graphs, base_output, run_name, config, log):
85152
"""
86153
target_dir = base_output / "ALL"
87154

88-
run_association_task(
155+
G = run_association_task(
89156
graphs=graphs,
90157
output_path=target_dir,
91158
run_name=run_name,
92159
association_config=config,
93160
log=log,
94161
)
95162

163+
if G and G.associated_graphs is not None:
164+
global_proteins = [clean_graph_name(g) for g in graphs]
165+
166+
# G.get_dashboard_data correctly formats nodes, edges, components & filtered_graphs
167+
master_export = G.get_dashboard_data(global_proteins)
168+
169+
# Append the top-level parameters required by the JS frontend
170+
master_export["mode"] = "all"
171+
master_export["run_name"] = run_name
172+
master_export["metadata"] = config
173+
174+
create_master_dashboard(master_export, target_dir, log)
175+
96176

97177
def clean_graph_name(graph):
98178
"""Extract cleaned stem name from graph tuple."""
@@ -131,21 +211,36 @@ def run_pair_mode(graphs, base_output, run_name, config, log):
131211
"""
132212
pair_base_dir = base_output / "PAIR"
133213

214+
global_proteins = [clean_graph_name(g) for g in graphs]
215+
216+
master_export = {
217+
"mode": "pair",
218+
"run_name": run_name,
219+
"metadata": config,
220+
"proteins": global_proteins,
221+
"protein_paths": [str(Path(g[1]).resolve()) for g in graphs],
222+
"pairs": {}
223+
}
224+
134225
for g1, g2 in combinations(graphs, 2):
135226
name1 = clean_graph_name(g1)
136227
name2 = clean_graph_name(g2)
137228

138229
pair_folder = f"{name1}_vs_{name2}"
230+
pair_key = f"{name1}_vs_{name2}"
139231
pair_run_name = f"{run_name}_{name1}_{name2}"
140232

141-
run_association_task(
233+
G = run_association_task(
142234
graphs=[g1, g2],
143235
output_path=pair_base_dir / pair_folder,
144236
run_name=pair_run_name,
145237
association_config=config,
146238
log=log,
147239
)
240+
if G and G.associated_graphs is not None:
241+
master_export["pairs"][pair_key] = G.get_dashboard_data(global_proteins)
148242

243+
create_master_dashboard(master_export, pair_base_dir, log)
149244

150245
def run(args):
151246
manifest = load_manifest(args.manifest)
@@ -183,14 +278,14 @@ def run(args):
183278
if args.dashboard:
184279
log.info("Opening dashboard in the default web browser...")
185280
if run_mode == "all":
186-
dash_path = base_output / "ALL" / "Dashboard.html"
281+
dash_path = base_output / "ALL" / "Dashboard_All.html"
187282
if dash_path.exists():
188283
webbrowser.open(f"file://{dash_path.resolve()}")
189284
else:
190-
for dash_path in (base_output / "PAIR").rglob("Dashboard.html"):
285+
dash_path = base_output / "PAIR" / "Dashboard_Pairs.html"
286+
if dash_path.exists():
191287
webbrowser.open(f"file://{dash_path.resolve()}")
192288

193-
194289
def renumber(args):
195290
if args.mhc_class.upper() == "MHCI":
196291
load_templates = load_mhci_templates

MHCXGraph/assets/LNBio white.png

109 KB
Loading
558 KB
Loading

0 commit comments

Comments
 (0)