Skip to content

Commit 09f31bd

Browse files
committed
Distance bug in graph output fixed. Split main.js in small files for management, and implementing a more complete figure exporter
1 parent 2ae0178 commit 09f31bd

25 files changed

Lines changed: 7999 additions & 3366 deletions

MHCXGraph/app.py

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,61 +19,124 @@ def create_master_dashboard(export_data, output_dir, log):
1919
"""Helper to inject the master aggregated JSON into the dashboard template."""
2020
import base64
2121
import json
22+
import re
23+
from pathlib import Path
2224

2325
assets_dir = Path(__file__).resolve().parent / "assets"
26+
html_files = assets_dir / "dashboard"
27+
js_files = html_files / "js"
28+
29+
def _load_asset(folder, filename, default=""):
30+
p = folder / filename
31+
return p.read_text(encoding="utf-8") if p.exists() else default
2432

2533
# Safely load local Frameworks or fallback to CDNs
26-
vis_local = assets_dir / "vis-network.min.js"
34+
vis_local = js_files / "vis-network.min.js"
2735
if vis_local.exists():
2836
vis_injection = f'<script>\n{vis_local.read_text(encoding="utf-8")}\n</script>'
2937
else:
3038
vis_injection = '<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>'
3139

32-
mol3d_local = assets_dir / "3Dmol-min.js"
40+
mol3d_local = js_files / "3Dmol-min.js"
3341
if mol3d_local.exists():
3442
mol3d_injection = f'<script>\n{mol3d_local.read_text(encoding="utf-8")}\n</script>'
3543
else:
3644
mol3d_injection = '<script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>'
3745

38-
mhcx_logo_path = assets_dir / "MHCXGraph logo.png"
39-
mhcx_logo_injection = "<h2>MHCXGraph</h2>"
46+
fabric_local = js_files / "fabric.min.js"
47+
if fabric_local.exists():
48+
fabric_injection = f'<script>\n{fabric_local.read_text(encoding="utf-8")}\n</script>'
49+
else:
50+
fabric_injection = '<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>'
51+
52+
plotly_local = js_files / "plotly.min.js"
53+
if plotly_local.exists():
54+
plotly_injection = f'<script>\n{plotly_local.read_text(encoding="utf-8")}\n</script>'
55+
else:
56+
plotly_injection = '<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.32.0/plotly.min.js"></script>'
57+
58+
mhcx_logo_path = assets_dir / "images/MHCXGraph logo.png"
59+
mhcx_logo_injection = "<b>MHCXGraph</b>\n"
4060
favicon_injection = ""
4161
if mhcx_logo_path.exists():
4262
with open(mhcx_logo_path, "rb") as image_file:
4363
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;">'
64+
mhcx_logo_injection = f'<img src="data:image/png;base64,{encoded}" alt="MHCXGraph Logo" style="width: 100%; height: 100%; margin-bottom: -10px;">'
4565
favicon_injection = f'<link rel="icon" type="image/png" href="data:image/png;base64,{encoded}">'
4666

47-
logo_dark_path = assets_dir / "LNBio white.png"
48-
logo_light_path = assets_dir / "LNBio.png"
67+
logo_dark_path = assets_dir / "images/LNBio white.png"
68+
logo_light_path = assets_dir / "images/LNBio.png"
4969
logo_injection = ""
5070
if logo_light_path.exists():
5171
with open(logo_light_path, "rb") as image_file:
5272
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;">'
73+
logo_injection += f'<img src="data:image/png;base64,{encoded}" alt="LNBio Logo" class="logo-light" style="height: 8rem; width: auto;">'
5474
if logo_dark_path.exists():
5575
with open(logo_dark_path, "rb") as image_file:
5676
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;">'
77+
logo_injection += f'<img src="data:image/png;base64,{encoded}" alt="LNBio Logo" class="logo-dark" style="height: 8rem; width: auto;">'
5878
if not logo_injection:
59-
log.debug("LNBio logos not found in assets/. Skipping logo injection.")
60-
79+
log.debug("LNBio logos not found in assets/images. Skipping logo injection.")
6180

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}.")
81+
# --- NEW MODULAR LOADING LOGIC ---
82+
html_template = _load_asset(html_files, "base.html")
83+
if not html_template:
84+
log.error(f"Template base.html not found at {html_files}.")
6885
return
6986

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)
87+
sidebar_html = _load_asset(html_files, "sidebar.html")
88+
modal_html = _load_asset(html_files, "export_modal.html")
89+
main_js = _load_asset(js_files, "main.js")
90+
modal_js = _load_asset(js_files, "export_modal.js")
91+
grid_js = _load_asset(js_files, "grid.js")
92+
data_js = _load_asset(js_files, "data.js")
93+
theme_js = _load_asset(js_files, "theme.js")
94+
viewer_js = _load_asset(js_files, "viewer.js")
95+
structures_js = _load_asset(js_files, "structures.js")
96+
init_js = _load_asset(js_files, "init_functions.js")
97+
analysis_js = _load_asset(js_files, "analysis.js")
98+
graph_js = _load_asset(js_files, "graph.js")
99+
100+
def split_html(raw_html):
101+
css_match = re.search(r'<style>(.*?)</style>', raw_html, re.DOTALL)
102+
css = css_match.group(1) if css_match else ""
103+
clean_html = re.sub(r'<style>.*?</style>', '', raw_html, flags=re.DOTALL).strip()
104+
return css, clean_html
105+
106+
def inject_js(html, name, code):
107+
placeholder = f"__{name.upper()}_JS_INJECTION__"
108+
return html.replace(placeholder, code)
109+
110+
sidebar_css, sidebar_dom = split_html(sidebar_html)
111+
modal_css, modal_dom = split_html(modal_html)
112+
113+
# Inject everything
114+
final_html = html_template.replace("__FAVICON_INJECTION__", favicon_injection)
115+
final_html = inject_js(final_html, "vis", vis_injection)
116+
final_html = inject_js(final_html, "3Dmol", mol3d_injection)
117+
final_html = inject_js(final_html, "fabric", fabric_injection) # ADD THIS
118+
final_html = inject_js(final_html, "plotly", plotly_injection)
119+
final_html = final_html.replace("__SIDEBAR_CSS_INJECTION__", sidebar_css)
120+
final_html = final_html.replace("__MODAL_CSS_INJECTION__", modal_css)
121+
final_html = final_html.replace("__SIDEBAR_HTML_INJECTION__", sidebar_dom)
74122
final_html = final_html.replace("__MHCXGRAPH_LOGO_INJECTION__", mhcx_logo_injection)
75123
final_html = final_html.replace("__LNBIO_LOGO_INJECTION__", logo_injection)
76124

125+
final_html = final_html.replace("__MODAL_HTML_INJECTION__", modal_dom)
126+
127+
# Javascript injection
128+
final_html = inject_js(final_html, "main", main_js)
129+
final_html = inject_js(final_html, "data", data_js)
130+
final_html = inject_js(final_html, "graph_data", json.dumps(export_data))
131+
final_html = inject_js(final_html, "init", init_js)
132+
final_html = inject_js(final_html, "viewer", viewer_js)
133+
final_html = inject_js(final_html, "theme", theme_js)
134+
final_html = inject_js(final_html, "grid", grid_js)
135+
final_html = inject_js(final_html, "analysis", analysis_js)
136+
final_html = inject_js(final_html, "graph", graph_js)
137+
final_html = inject_js(final_html, "structures", structures_js)
138+
final_html = inject_js(final_html, "modal", modal_js)
139+
77140
# Dynamically name the output file based on the mode
78141
mode = export_data.get("mode", "all")
79142
file_name = "Dashboard_Pairs.html" if mode == "pair" else "Dashboard_All.html"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>MHCXGraph Dashboard</title>
6+
__FAVICON_INJECTION__
7+
__VIS_JS_INJECTION__
8+
__3DMOL_JS_INJECTION__
9+
__FABRIC_JS_INJECTION__
10+
__PLOTLY_JS_INJECTION__
11+
12+
<style>
13+
:root {
14+
--bg-body: #f3f4f6; --bg-panel: #ffffff; --bg-control: #f9fafb;
15+
--border: #d1d5db; --border-light: #e5e7eb; --text-main: #111827;
16+
--text-muted: #4b5563; --text-faint: #6b7280; --hover-bg: #e5e7eb;
17+
--btn-bg: #2563eb; --btn-hover: #1d4ed8;
18+
}
19+
[data-theme="dark"] {
20+
--bg-body: #030712; --bg-panel: #111827; --bg-control: #1f2937;
21+
--border: #374151; --border-light: #1f2937; --text-main: #f9fafb;
22+
--text-muted: #9ca3af; --text-faint: #6b7280; --hover-bg: #374151;
23+
--btn-bg: #3b82f6; --btn-hover: #60a5fa;
24+
}
25+
26+
body { margin: 0; padding: 0; display: flex; flex-direction: column; height: 100vh; font-family: ui-sans-serif, system-ui, sans-serif; overflow: hidden; background: var(--bg-body); color: var(--text-main); transition: background 0.3s, color 0.3s;}
27+
28+
#app-container { display: flex; flex: 1; overflow: hidden; position: relative; }
29+
#main-content { flex: 1; display: flex; flex-direction: column; position: relative; min-width: 0; overflow: hidden;}
30+
31+
#top-panel { flex: 1; min-height: 10%; position: relative; background: var(--bg-panel); overflow: hidden; }
32+
#v-splitter { height: 8px; background: var(--border-light); cursor: row-resize; z-index: 50; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); display: flex; justify-content: center; align-items: center; transition: background 0.2s;}
33+
#v-splitter:hover { background: var(--border); }
34+
#v-splitter::after { content: "•••"; color: var(--text-faint); font-size: 10px; letter-spacing: 2px; }
35+
#bottom-panel { flex: 1; min-height: 10%; position: relative; background: var(--bg-control); overflow: hidden; display: flex; flex-direction: column; }
36+
37+
#single-network { width: 100%; height: 100%; position: absolute; top:0; left:0; display: none; background: var(--bg-panel); }
38+
#single-mols { width: 100%; height: 100%; position: absolute; top:0; left:0; display: none; padding-top: 45px; box-sizing: border-box; align-content: flex-start; }
39+
#analysis-wrapper { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 80; background: var(--bg-body); overflow-y: auto; padding: 60px 40px 40px 40px; box-sizing: border-box; }
40+
41+
.summary-cards { display: flex; gap: 20px; justify-content: center; margin-bottom: 30px; flex-wrap: wrap; }
42+
.card { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05); flex: 1; min-width: 180px; max-width: 250px; }
43+
.card h4 { margin: 0 0 10px 0; color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: 1px; }
44+
.card .value { font-size: 28px; font-weight: 800; color: var(--btn-bg); }
45+
46+
.analysis-section { margin-bottom: 30px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px; padding: 25px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
47+
.analysis-section h2 { margin-top: 0; font-size: 18px; border-bottom: 1px solid var(--border-light); padding-bottom: 12px; margin-bottom: 20px; color: var(--text-main); }
48+
49+
.heatmap-wrapper { display: flex; justify-content: center; width: 100%; overflow-x: auto; padding: 10px 0; }
50+
.heatmap-grid { display: grid; gap: 4px; font-size: 13px; width: max-content; margin: 0 auto; }
51+
.heatmap-cell { width: 55px; height: 55px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #fff; text-shadow: 0px 1px 3px rgba(0,0,0,0.6); transition: transform 0.15s ease, box-shadow 0.15s ease; cursor: crosshair; position: relative; }
52+
.heatmap-cell:hover { transform: scale(1.15); z-index: 10; box-shadow: 0 6px 12px rgba(0,0,0,0.3); }
53+
.heatmap-header { font-weight: bold; color: var(--text-main); text-align: center; padding: 8px; display: flex; align-items: center; justify-content: center; font-size: 12px;}
54+
55+
.hm-tooltip { visibility: hidden; position: absolute; bottom: 120%; left: 50%; transform: translateX(-50%); background: var(--text-main); color: var(--bg-panel); padding: 8px 12px; border-radius: 6px; font-size: 12px; font-weight: normal; text-shadow: none; white-space: nowrap; z-index: 100; opacity: 0; transition: opacity 0.2s; pointer-events: none; box-shadow: 0 4px 6px rgba(0,0,0,0.2); }
56+
.heatmap-cell:hover .hm-tooltip { visibility: visible; opacity: 1; }
57+
.hm-tooltip::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: var(--text-main) transparent transparent transparent; }
58+
59+
.metrics-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; text-align: center; }
60+
.metrics-table th, .metrics-table td { padding: 12px; border-bottom: 1px solid var(--border-light); }
61+
.metrics-table th { background: var(--bg-control); font-weight: 600; color: var(--text-main); position: sticky; top: 0; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px;}
62+
.metrics-table tbody tr { transition: background 0.2s; }
63+
.metrics-table tbody tr:hover { background: var(--hover-bg); }
64+
.class-badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; margin: 2px; color: #fff;}
65+
66+
#sidebar-toggle { position: absolute; top: 10px; left: 10px; z-index: 100; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 6px 10px; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.1); font-size: 16px; color: var(--text-main);}
67+
#theme-toggle { position: absolute; top: 10px; right: 15px; z-index: 100; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 6px 10px; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.1); font-size: 16px; color: var(--text-main);}
68+
#sidebar-toggle:hover, #theme-toggle:hover { background: var(--hover-bg); }
69+
70+
#view-controls { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); z-index: 100; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; padding: 6px 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 8px; }
71+
#viewer-toolbar { position: absolute; top: 10px; left: 15px; z-index: 100; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 10px; font-size: 13px;}
72+
select, input[type="range"] { background: var(--bg-panel); color: var(--text-main); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; outline: none; font-size: 13px;}
73+
74+
.mol-subviewer { position: relative; width: 100%; height: 100%; border: 1px solid var(--border-light); box-sizing: border-box; border-radius: 6px; overflow: hidden; background: var(--bg-panel); box-shadow: 0 1px 3px rgba(0,0,0,0.1);}
75+
#mol-tooltip { position: fixed; display: none; background: rgba(17, 24, 39, 0.9); color: white; padding: 8px 12px; border-radius: 6px; font-size: 13px; pointer-events: none; z-index: 99999; box-shadow: 0 4px 6px rgba(0,0,0,0.1);}
76+
.viewer-title { position: absolute; top: 10px; left: 10px; background: var(--bg-panel); color: var(--text-main); padding: 4px 10px; border-radius: 4px; font-size: 13px; font-weight: bold; pointer-events: none; z-index: 50; border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.1);}
77+
.color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
78+
79+
#app-footer { height: 65px; background: var(--bg-panel); border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; font-size: 13px; color: var(--text-muted); flex-shrink: 0; box-shadow: 0 -2px 5px rgba(0,0,0,0.03); z-index: 100; }
80+
#app-footer a { color: var(--text-main); text-decoration: none; font-weight: 500; transition: color 0.2s;}
81+
#app-footer a:hover { color: var(--btn-bg); text-decoration: underline; }
82+
83+
[data-theme="light"] .logo-dark { display: none; }
84+
[data-theme="dark"] .logo-light { display: none; }
85+
86+
/* INJECTED CSS */
87+
__SIDEBAR_CSS_INJECTION__
88+
__MODAL_CSS_INJECTION__
89+
</style>
90+
</head>
91+
<body data-theme="dark">
92+
93+
<div id="app-container">
94+
95+
__SIDEBAR_HTML_INJECTION__
96+
97+
<div id="main-content">
98+
<button id="sidebar-toggle" onclick="toggleSidebar()" title="Toggle Sidebar"></button>
99+
<button id="theme-toggle" onclick="toggleTheme()" title="Toggle Theme">☀️ / 🌙</button>
100+
101+
<div id="view-controls" style="display: none;">
102+
<b style="font-size: 13px; color: var(--text-main);">Focus Graph View:</b>
103+
<select id="main-graph-selector" onchange="switchGraphViewMode()">
104+
<option value="associated">🌐 Associated Graph</option>
105+
<option value="filtered">Filtered Graphs</option>
106+
<option value="analysis" id="opt-analysis-tab">📊 Analysis & Metrics</option>
107+
</select>
108+
</div>
109+
110+
<div id="top-panel">
111+
<div id="single-network"></div>
112+
</div>
113+
114+
<div id="v-splitter"></div>
115+
116+
<div id="bottom-panel">
117+
<div id="viewer-toolbar" style="display: none;">
118+
<b style="color: var(--text-main);">3D View:</b>
119+
<select id="view-selector" onchange="triggerRebuild()">
120+
<option value="aligned">Aligned (Single View)</option>
121+
<option value="separate">Separate (Grid View)</option>
122+
</select>
123+
<label style="margin-left: 10px; font-size: 13px; color: var(--text-main); cursor: pointer; display: flex; align-items: center; gap: 4px;">
124+
<input type="checkbox" id="syncViews" checked> Sync Cameras
125+
</label>
126+
</div>
127+
<div id="single-mols"></div>
128+
</div>
129+
130+
<div id="analysis-wrapper"></div>
131+
</div>
132+
</div>
133+
134+
<div id="app-footer">
135+
<div class="footer-left">
136+
<b><a href="https://github.com/cnpem/MHCXGraph/" target="_blank">MHCXGraph</a></b>
137+
<span>|</span>
138+
<a href="https://lnbio.cnpem.br/" target="_blank">LNBio</a> - <a href="https://cnpem.br/" target="_blank">CNPEM</a>
139+
<span>|</span>
140+
<a href="https://github.com/cnpem/MHCXGraph/blob/main/LICENSE" target="_blank">AGPL-3.0 License</a>
141+
</div>
142+
<div class="footer-right">
143+
<a href="https://lnbio.cnpem.br/" target="_blank">__LNBIO_LOGO_INJECTION__</a>
144+
</div>
145+
</div>
146+
147+
<div id="mol-tooltip"></div>
148+
149+
__MODAL_HTML_INJECTION__
150+
151+
<script>
152+
__MAIN_JS_INJECTION__
153+
</script>
154+
</body>
155+
</html>

0 commit comments

Comments
 (0)