Mercurial > repos > goeckslab > image_learner
comparison utils.py @ 10:b0d893d04d4c draft default tip
planemo upload for repository https://github.com/goeckslab/gleam.git commit 1594d503179f28987720594eb49b48a15486f073
author | goeckslab |
---|---|
date | Mon, 08 Sep 2025 22:38:35 +0000 |
parents | 9e912fce264c |
children |
comparison
equal
deleted
inserted
replaced
9:9e912fce264c | 10:b0d893d04d4c |
---|---|
1 import base64 | 1 import base64 |
2 import json | 2 import json |
3 | 3 |
4 | 4 |
5 def get_html_template(): | 5 def get_html_template(): |
6 """ | |
7 Returns the opening HTML, <head> (with CSS/JS), and opens <body> + .container. | |
8 Includes: | |
9 - Base styling for layout and tables | |
10 - Sortable table headers with 3-state arrows (none ⇅, asc ↑, desc ↓) | |
11 - A scroll helper class (.scroll-rows-30) that approximates ~30 visible rows | |
12 - A guarded script so initializing runs only once even if injected twice | |
13 """ | |
6 return """ | 14 return """ |
7 <html> | 15 <!DOCTYPE html> |
8 <head> | 16 <html> |
9 <meta charset="UTF-8"> | 17 <head> |
10 <title>Galaxy-Ludwig Report</title> | 18 <meta charset="UTF-8"> |
11 <style> | 19 <title>Galaxy-Ludwig Report</title> |
12 body { | 20 <style> |
13 font-family: Arial, sans-serif; | 21 body { |
14 margin: 0; | 22 font-family: Arial, sans-serif; |
15 padding: 20px; | 23 margin: 0; |
16 background-color: #f4f4f4; | 24 padding: 20px; |
17 } | 25 background-color: #f4f4f4; |
18 .container { | 26 } |
19 max-width: 800px; | 27 .container { |
20 margin: auto; | 28 max-width: 1200px; |
21 background: white; | 29 margin: auto; |
22 padding: 20px; | 30 background: white; |
23 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | 31 padding: 20px; |
24 overflow-x: auto; | 32 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
25 } | 33 overflow-x: auto; |
26 h1 { | 34 } |
27 text-align: center; | 35 h1 { |
28 color: #333; | 36 text-align: center; |
29 } | 37 color: #333; |
30 h2 { | 38 } |
31 border-bottom: 2px solid #4CAF50; | 39 h2 { |
32 color: #4CAF50; | 40 border-bottom: 2px solid #4CAF50; |
33 padding-bottom: 5px; | 41 color: #4CAF50; |
34 } | 42 padding-bottom: 5px; |
35 /* baseline table setup */ | 43 margin-top: 28px; |
36 table { | 44 } |
37 border-collapse: collapse; | 45 |
38 margin: 20px 0; | 46 /* baseline table setup */ |
39 width: 100%; | 47 table { |
40 table-layout: fixed; | 48 border-collapse: collapse; |
41 } | 49 margin: 20px 0; |
42 table, th, td { | 50 width: 100%; |
43 border: 1px solid #ddd; | 51 table-layout: fixed; |
44 } | 52 background: #fff; |
45 th, td { | 53 } |
46 padding: 8px; | 54 table, th, td { |
47 text-align: center; | 55 border: 1px solid #ddd; |
48 vertical-align: middle; | 56 } |
49 word-wrap: break-word; | 57 th, td { |
50 } | 58 padding: 10px; |
51 th { | 59 text-align: center; |
52 background-color: #4CAF50; | 60 vertical-align: middle; |
53 color: white; | 61 word-break: break-word; |
54 } | 62 white-space: normal; |
55 .plot { | 63 overflow-wrap: anywhere; |
56 text-align: center; | 64 } |
57 margin: 20px 0; | 65 th { |
58 } | 66 background-color: #4CAF50; |
59 .plot img { | 67 color: white; |
60 max-width: 100%; | 68 } |
61 height: auto; | 69 |
62 } | 70 .plot { |
63 | 71 text-align: center; |
64 /* ------------------- | 72 margin: 20px 0; |
65 SORTABLE COLUMNS | 73 } |
66 ------------------- */ | 74 .plot img { |
67 table.performance-summary th.sortable { | 75 max-width: 100%; |
68 cursor: pointer; | 76 height: auto; |
69 position: relative; | 77 border: 1px solid #ddd; |
70 user-select: none; | 78 } |
71 } | 79 |
72 /* hide arrows by default */ | 80 /* ------------------- |
73 table.performance-summary th.sortable::after { | 81 sortable columns (3-state: none ⇅, asc ↑, desc ↓) |
74 content: ''; | 82 ------------------- */ |
75 position: absolute; | 83 table.performance-summary th.sortable { |
76 right: 12px; | 84 cursor: pointer; |
77 top: 50%; | 85 position: relative; |
78 transform: translateY(-50%); | 86 user-select: none; |
79 font-size: 0.8em; | 87 } |
80 color: #666; | 88 /* default icon space */ |
81 } | 89 table.performance-summary th.sortable::after { |
82 /* three states */ | 90 content: '⇅'; |
83 table.performance-summary th.sortable.sorted-none::after { | 91 position: absolute; |
84 content: '⇅'; | 92 right: 12px; |
85 } | 93 top: 50%; |
86 table.performance-summary th.sortable.sorted-asc::after { | 94 transform: translateY(-50%); |
87 content: '↑'; | 95 font-size: 0.8em; |
88 } | 96 color: #eaf5ea; /* light on green */ |
89 table.performance-summary th.sortable.sorted-desc::after { | 97 text-shadow: 0 0 1px rgba(0,0,0,0.15); |
90 content: '↓'; | 98 } |
91 } | 99 /* three states override the default */ |
92 </style> | 100 table.performance-summary th.sortable.sorted-none::after { content: '⇅'; color: #eaf5ea; } |
93 | 101 table.performance-summary th.sortable.sorted-asc::after { content: '↑'; color: #ffffff; } |
94 <!-- sorting script --> | 102 table.performance-summary th.sortable.sorted-desc::after { content: '↓'; color: #ffffff; } |
95 <script> | 103 |
96 document.addEventListener('DOMContentLoaded', () => { | 104 /* show ~30 rows with a scrollbar (tweak if you want) */ |
97 // 1) record each row's original position | 105 .scroll-rows-30 { |
98 document.querySelectorAll('table.performance-summary tbody').forEach(tbody => { | 106 max-height: 900px; /* ~30 rows depending on row height */ |
99 Array.from(tbody.rows).forEach((row, i) => { | 107 overflow-y: auto; /* vertical scrollbar (“sidebar”) */ |
100 row.dataset.originalOrder = i; | 108 overflow-x: auto; |
101 }); | 109 } |
110 | |
111 /* Tabs + Help button (used by build_tabbed_html) */ | |
112 .tabs { | |
113 display: flex; | |
114 align-items: center; | |
115 border-bottom: 2px solid #ccc; | |
116 margin-bottom: 1rem; | |
117 gap: 6px; | |
118 flex-wrap: wrap; | |
119 } | |
120 .tab { | |
121 padding: 10px 20px; | |
122 cursor: pointer; | |
123 border: 1px solid #ccc; | |
124 border-bottom: none; | |
125 background: #f9f9f9; | |
126 margin-right: 5px; | |
127 border-top-left-radius: 8px; | |
128 border-top-right-radius: 8px; | |
129 } | |
130 .tab.active { | |
131 background: white; | |
132 font-weight: bold; | |
133 } | |
134 .help-btn { | |
135 margin-left: auto; | |
136 padding: 6px 12px; | |
137 font-size: 0.9rem; | |
138 border: 1px solid #4CAF50; | |
139 border-radius: 4px; | |
140 background: #4CAF50; | |
141 color: white; | |
142 cursor: pointer; | |
143 } | |
144 .tab-content { | |
145 display: none; | |
146 padding: 20px; | |
147 border: 1px solid #ccc; | |
148 border-top: none; | |
149 background: #fff; | |
150 } | |
151 .tab-content.active { | |
152 display: block; | |
153 } | |
154 | |
155 /* Modal (used by get_metrics_help_modal) */ | |
156 .modal { | |
157 display: none; | |
158 position: fixed; | |
159 z-index: 9999; | |
160 left: 0; top: 0; | |
161 width: 100%; height: 100%; | |
162 overflow: auto; | |
163 background-color: rgba(0,0,0,0.4); | |
164 } | |
165 .modal-content { | |
166 background-color: #fefefe; | |
167 margin: 8% auto; | |
168 padding: 20px; | |
169 border: 1px solid #888; | |
170 width: 90%; | |
171 max-width: 900px; | |
172 border-radius: 8px; | |
173 } | |
174 .modal .close { | |
175 color: #777; | |
176 float: right; | |
177 font-size: 28px; | |
178 font-weight: bold; | |
179 line-height: 1; | |
180 margin-left: 8px; | |
181 } | |
182 .modal .close:hover, | |
183 .modal .close:focus { | |
184 color: black; | |
185 text-decoration: none; | |
186 cursor: pointer; | |
187 } | |
188 .metrics-guide h3 { margin-top: 20px; } | |
189 .metrics-guide p { margin: 6px 0; } | |
190 .metrics-guide ul { margin: 10px 0; padding-left: 20px; } | |
191 </style> | |
192 | |
193 <script> | |
194 // Guard to avoid double-initialization if this block is included twice | |
195 (function(){ | |
196 if (window.__perfSummarySortInit) return; | |
197 window.__perfSummarySortInit = true; | |
198 | |
199 function initPerfSummarySorting() { | |
200 // Record original order for "back to original" | |
201 document.querySelectorAll('table.performance-summary tbody').forEach(tbody => { | |
202 Array.from(tbody.rows).forEach((row, i) => { row.dataset.originalOrder = i; }); | |
203 }); | |
204 | |
205 const getText = td => (td?.innerText || '').trim(); | |
206 const cmp = (idx, asc) => (a, b) => { | |
207 const v1 = getText(a.children[idx]); | |
208 const v2 = getText(b.children[idx]); | |
209 const n1 = parseFloat(v1), n2 = parseFloat(v2); | |
210 if (!isNaN(n1) && !isNaN(n2)) return asc ? n1 - n2 : n2 - n1; // numeric | |
211 return asc ? v1.localeCompare(v2) : v2.localeCompare(v1); // lexical | |
212 }; | |
213 | |
214 document.querySelectorAll('table.performance-summary th.sortable').forEach(th => { | |
215 // initialize to “none” | |
216 th.classList.remove('sorted-asc','sorted-desc'); | |
217 th.classList.add('sorted-none'); | |
218 | |
219 th.addEventListener('click', () => { | |
220 const table = th.closest('table'); | |
221 const headerRow = th.parentNode; | |
222 const allTh = headerRow.querySelectorAll('th.sortable'); | |
223 const tbody = table.querySelector('tbody'); | |
224 | |
225 // Determine current state BEFORE clearing | |
226 const isAsc = th.classList.contains('sorted-asc'); | |
227 const isDesc = th.classList.contains('sorted-desc'); | |
228 | |
229 // Reset all headers in this row | |
230 allTh.forEach(x => x.classList.remove('sorted-asc','sorted-desc','sorted-none')); | |
231 | |
232 // Compute next state | |
233 let next; | |
234 if (!isAsc && !isDesc) { | |
235 next = 'asc'; | |
236 } else if (isAsc) { | |
237 next = 'desc'; | |
238 } else { | |
239 next = 'none'; | |
240 } | |
241 th.classList.add('sorted-' + next); | |
242 | |
243 // Sort rows according to the chosen state | |
244 const rows = Array.from(tbody.rows); | |
245 if (next === 'none') { | |
246 rows.sort((a, b) => (a.dataset.originalOrder - b.dataset.originalOrder)); | |
247 } else { | |
248 const idx = Array.from(headerRow.children).indexOf(th); | |
249 rows.sort(cmp(idx, next === 'asc')); | |
250 } | |
251 rows.forEach(r => tbody.appendChild(r)); | |
102 }); | 252 }); |
103 | |
104 const getText = cell => cell.innerText.trim(); | |
105 const comparer = (idx, asc) => (a, b) => { | |
106 const v1 = getText(a.children[idx]); | |
107 const v2 = getText(b.children[idx]); | |
108 const n1 = parseFloat(v1), n2 = parseFloat(v2); | |
109 if (!isNaN(n1) && !isNaN(n2)) { | |
110 return asc ? n1 - n2 : n2 - n1; | |
111 } | |
112 return asc | |
113 ? v1.localeCompare(v2) | |
114 : v2.localeCompare(v1); | |
115 }; | |
116 | |
117 document | |
118 .querySelectorAll('table.performance-summary th.sortable') | |
119 .forEach(th => { | |
120 // initialize to "none" state | |
121 th.classList.add('sorted-none'); | |
122 th.addEventListener('click', () => { | |
123 const table = th.closest('table'); | |
124 const allTh = table.querySelectorAll('th.sortable'); | |
125 | |
126 // 1) determine current state BEFORE clearing classes | |
127 let curr = th.classList.contains('sorted-asc') | |
128 ? 'asc' | |
129 : th.classList.contains('sorted-desc') | |
130 ? 'desc' | |
131 : 'none'; | |
132 // 2) cycle to next state | |
133 let next = curr === 'none' | |
134 ? 'asc' | |
135 : curr === 'asc' | |
136 ? 'desc' | |
137 : 'none'; | |
138 | |
139 // 3) clear all sort markers | |
140 allTh.forEach(h => | |
141 h.classList.remove('sorted-none','sorted-asc','sorted-desc') | |
142 ); | |
143 // 4) apply the new marker | |
144 th.classList.add(`sorted-${next}`); | |
145 | |
146 // 5) sort or restore original order | |
147 const tbody = table.querySelector('tbody'); | |
148 let rows = Array.from(tbody.rows); | |
149 if (next === 'none') { | |
150 rows.sort((a, b) => | |
151 a.dataset.originalOrder - b.dataset.originalOrder | |
152 ); | |
153 } else { | |
154 const idx = Array.from(th.parentNode.children).indexOf(th); | |
155 rows.sort(comparer(idx, next === 'asc')); | |
156 } | |
157 rows.forEach(r => tbody.appendChild(r)); | |
158 }); | |
159 }); | |
160 }); | 253 }); |
161 </script> | 254 } |
162 </head> | 255 |
163 <body> | 256 // Run after DOM is ready |
164 <div class="container"> | 257 if (document.readyState === 'loading') { |
165 """ | 258 document.addEventListener('DOMContentLoaded', initPerfSummarySorting); |
259 } else { | |
260 initPerfSummarySorting(); | |
261 } | |
262 })(); | |
263 </script> | |
264 </head> | |
265 <body> | |
266 <div class="container"> | |
267 """ | |
166 | 268 |
167 | 269 |
168 def get_html_closing(): | 270 def get_html_closing(): |
271 """Closes .container, body, and html.""" | |
169 return """ | 272 return """ |
170 </div> | 273 </div> |
171 </body> | 274 </body> |
172 </html> | 275 </html> |
173 """ | 276 """ |
174 | 277 |
175 | 278 |
176 def encode_image_to_base64(image_path): | 279 def encode_image_to_base64(image_path: str) -> str: |
177 """Convert an image file to a base64 encoded string.""" | 280 """Convert an image file to a base64 encoded string.""" |
178 with open(image_path, "rb") as img_file: | 281 with open(image_path, "rb") as img_file: |
179 return base64.b64encode(img_file.read()).decode("utf-8") | 282 return base64.b64encode(img_file.read()).decode("utf-8") |
180 | 283 |
181 | 284 |
182 def json_to_nested_html_table(json_data, depth=0): | 285 def json_to_nested_html_table(json_data, depth: int = 0) -> str: |
183 """ | 286 """ |
184 Convert JSON object to an HTML nested table. | 287 Convert a JSON-able object to an HTML nested table. |
185 | 288 Renders dicts as two-column tables (key/value) and lists as index/value rows. |
186 Parameters: | 289 """ |
187 json_data (dict or list): The JSON data to convert. | 290 # Base case: flat dict (no nested dict/list values) |
188 depth (int): Current depth level for indentation. | |
189 | |
190 Returns: | |
191 str: HTML string for the nested table. | |
192 """ | |
193 # Base case: if JSON is a simple key-value pair dictionary | |
194 if isinstance(json_data, dict) and all( | 291 if isinstance(json_data, dict) and all( |
195 not isinstance(v, (dict, list)) for v in json_data.values() | 292 not isinstance(v, (dict, list)) for v in json_data.values() |
196 ): | 293 ): |
197 # Render a flat table | |
198 rows = [ | 294 rows = [ |
199 f"<tr><th>{key}</th><td>{value}</td></tr>" | 295 f"<tr><th>{key}</th><td>{value}</td></tr>" |
200 for key, value in json_data.items() | 296 for key, value in json_data.items() |
201 ] | 297 ] |
202 return f"<table>{''.join(rows)}</table>" | 298 return f"<table>{''.join(rows)}</table>" |
203 | 299 |
204 # Base case: if JSON is a list of simple values | 300 # Base case: list of simple values |
205 if isinstance(json_data, list) and all( | 301 if isinstance(json_data, list) and all( |
206 not isinstance(v, (dict, list)) for v in json_data | 302 not isinstance(v, (dict, list)) for v in json_data |
207 ): | 303 ): |
208 rows = [ | 304 rows = [ |
209 f"<tr><th>Index {i}</th><td>{value}</td></tr>" | 305 f"<tr><th>Index {i}</th><td>{value}</td></tr>" |
210 for i, value in enumerate(json_data) | 306 for i, value in enumerate(json_data) |
211 ] | 307 ] |
212 return f"<table>{''.join(rows)}</table>" | 308 return f"<table>{''.join(rows)}</table>" |
213 | 309 |
214 # Recursive case: if JSON contains nested structures | 310 # Recursive cases |
215 if isinstance(json_data, dict): | 311 if isinstance(json_data, dict): |
216 rows = [ | 312 rows = [ |
217 f"<tr><th style='padding-left:{depth * 20}px;'>{key}</th>" | 313 ( |
218 f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" | 314 f"<tr><th style='text-align:left;padding-left:{depth * 20}px;'>{key}</th>" |
315 f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" | |
316 ) | |
219 for key, value in json_data.items() | 317 for key, value in json_data.items() |
220 ] | 318 ] |
221 return f"<table>{''.join(rows)}</table>" | 319 return f"<table>{''.join(rows)}</table>" |
222 | 320 |
223 if isinstance(json_data, list): | 321 if isinstance(json_data, list): |
224 rows = [ | 322 rows = [ |
225 f"<tr><th style='padding-left:{depth * 20}px;'>[{i}]</th>" | 323 ( |
226 f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" | 324 f"<tr><th style='text-align:left;padding-left:{depth * 20}px;'>[{i}]</th>" |
325 f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" | |
326 ) | |
227 for i, value in enumerate(json_data) | 327 for i, value in enumerate(json_data) |
228 ] | 328 ] |
229 return f"<table>{''.join(rows)}</table>" | 329 return f"<table>{''.join(rows)}</table>" |
230 | 330 |
231 # Base case: simple value | 331 # Primitive |
232 return f"{json_data}" | 332 return f"{json_data}" |
233 | 333 |
234 | 334 |
235 def json_to_html_table(json_data): | 335 def json_to_html_table(json_data) -> str: |
236 """ | 336 """ |
237 Convert JSON to a vertically oriented HTML table. | 337 Convert JSON (dict or string) into a vertically oriented HTML table. |
238 | |
239 Parameters: | |
240 json_data (str or dict): JSON string or dictionary. | |
241 | |
242 Returns: | |
243 str: HTML table representation. | |
244 """ | 338 """ |
245 if isinstance(json_data, str): | 339 if isinstance(json_data, str): |
246 json_data = json.loads(json_data) | 340 json_data = json.loads(json_data) |
247 return json_to_nested_html_table(json_data) | 341 return json_to_nested_html_table(json_data) |
248 | 342 |
249 | 343 |
250 def build_tabbed_html(metrics_html: str, train_val_html: str, test_html: str) -> str: | 344 def build_tabbed_html(metrics_html: str, train_val_html: str, test_html: str) -> str: |
345 """ | |
346 Build a 3-tab interface: | |
347 - Config and Results Summary | |
348 - Train/Validation Results | |
349 - Test Results | |
350 Includes a persistent "Help" button that toggles the metrics modal. | |
351 """ | |
251 return f""" | 352 return f""" |
252 <style> | |
253 .tabs {{ | |
254 display: flex; | |
255 align-items: center; | |
256 border-bottom: 2px solid #ccc; | |
257 margin-bottom: 1rem; | |
258 }} | |
259 .tab {{ | |
260 padding: 10px 20px; | |
261 cursor: pointer; | |
262 border: 1px solid #ccc; | |
263 border-bottom: none; | |
264 background: #f9f9f9; | |
265 margin-right: 5px; | |
266 border-top-left-radius: 8px; | |
267 border-top-right-radius: 8px; | |
268 }} | |
269 .tab.active {{ | |
270 background: white; | |
271 font-weight: bold; | |
272 }} | |
273 /* new help-button styling */ | |
274 .help-btn {{ | |
275 margin-left: auto; | |
276 padding: 6px 12px; | |
277 font-size: 0.9rem; | |
278 border: 1px solid #4CAF50; | |
279 border-radius: 4px; | |
280 background: #4CAF50; | |
281 color: white; | |
282 cursor: pointer; | |
283 }} | |
284 .tab-content {{ | |
285 display: none; | |
286 padding: 20px; | |
287 border: 1px solid #ccc; | |
288 border-top: none; | |
289 }} | |
290 .tab-content.active {{ | |
291 display: block; | |
292 }} | |
293 </style> | |
294 | |
295 <div class="tabs"> | 353 <div class="tabs"> |
296 <div class="tab active" onclick="showTab('metrics')">Config and Results Summary</div> | 354 <div class="tab active" onclick="showTab('metrics')">Config and Results Summary</div> |
297 <div class="tab" onclick="showTab('trainval')">Train/Validation Results</div> | 355 <div class="tab" onclick="showTab('trainval')">Train/Validation Results</div> |
298 <div class="tab" onclick="showTab('test')">Test Results</div> | 356 <div class="tab" onclick="showTab('test')">Test Results</div> |
299 <!-- always-visible help button --> | 357 <button id="openMetricsHelp" class="help-btn" title="Open metrics help">Help</button> |
300 <button id="openMetricsHelp" class="help-btn">Help</button> | |
301 </div> | 358 </div> |
302 | 359 |
303 <div id="metrics" class="tab-content active"> | 360 <div id="metrics" class="tab-content active"> |
304 {metrics_html} | 361 {metrics_html} |
305 </div> | 362 </div> |
309 <div id="test" class="tab-content"> | 366 <div id="test" class="tab-content"> |
310 {test_html} | 367 {test_html} |
311 </div> | 368 </div> |
312 | 369 |
313 <script> | 370 <script> |
314 function showTab(id) {{ | 371 function showTab(id) {{ |
315 document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); | 372 document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); |
316 document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); | 373 document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); |
317 document.getElementById(id).classList.add('active'); | 374 document.getElementById(id).classList.add('active'); |
318 document.querySelector(`.tab[onclick*="${{id}}"]`).classList.add('active'); | 375 // find tab with matching onclick target |
319 }} | 376 document.querySelectorAll('.tab').forEach(t => {{ |
377 if (t.getAttribute('onclick') && t.getAttribute('onclick').includes(id)) {{ | |
378 t.classList.add('active'); | |
379 }} | |
380 }}); | |
381 }} | |
320 </script> | 382 </script> |
321 """ | 383 """ |
322 | 384 |
323 | 385 |
324 def get_metrics_help_modal() -> str: | 386 def get_metrics_help_modal() -> str: |
387 """ | |
388 Returns a ready-to-use modal with a comprehensive metrics guide and | |
389 the small script that wires the "Help" button to open/close the modal. | |
390 """ | |
325 modal_html = ( | 391 modal_html = ( |
326 '<div id="metricsHelpModal" class="modal">' | 392 '<div id="metricsHelpModal" class="modal">' |
327 ' <div class="modal-content">' | 393 ' <div class="modal-content">' |
328 ' <span class="close">×</span>' | 394 ' <span class="close">×</span>' |
329 " <h2>Model Evaluation Metrics — Help Guide</h2>" | 395 " <h2>Model Evaluation Metrics — Help Guide</h2>" |
440 " </ul>" | 506 " </ul>" |
441 " </div>" | 507 " </div>" |
442 " </div>" | 508 " </div>" |
443 "</div>" | 509 "</div>" |
444 ) | 510 ) |
445 modal_css = ( | 511 |
446 "<style>" | |
447 ".modal {" | |
448 " display: none;" | |
449 " position: fixed;" | |
450 " z-index: 1;" | |
451 " left: 0;" | |
452 " top: 0;" | |
453 " width: 100%;" | |
454 " height: 100%;" | |
455 " overflow: auto;" | |
456 " background-color: rgba(0,0,0,0.4);" | |
457 "}" | |
458 ".modal-content {" | |
459 " background-color: #fefefe;" | |
460 " margin: 15% auto;" | |
461 " padding: 20px;" | |
462 " border: 1px solid #888;" | |
463 " width: 80%;" | |
464 " max-width: 800px;" | |
465 "}" | |
466 ".close {" | |
467 " color: #aaa;" | |
468 " float: right;" | |
469 " font-size: 28px;" | |
470 " font-weight: bold;" | |
471 "}" | |
472 ".close:hover," | |
473 ".close:focus {" | |
474 " color: black;" | |
475 " text-decoration: none;" | |
476 " cursor: pointer;" | |
477 "}" | |
478 ".metrics-guide h3 {" | |
479 " margin-top: 20px;" | |
480 "}" | |
481 ".metrics-guide p {" | |
482 " margin: 5px 0;" | |
483 "}" | |
484 ".metrics-guide ul {" | |
485 " margin: 10px 0;" | |
486 " padding-left: 20px;" | |
487 "}" | |
488 "</style>" | |
489 ) | |
490 modal_js = ( | 512 modal_js = ( |
491 "<script>" | 513 "<script>" |
492 'document.addEventListener("DOMContentLoaded", function() {' | 514 "document.addEventListener('DOMContentLoaded', function() {" |
493 ' var modal = document.getElementById("metricsHelpModal");' | 515 " var modal = document.getElementById('metricsHelpModal');" |
494 ' var openBtn = document.getElementById("openMetricsHelp");' | 516 " var openBtn = document.getElementById('openMetricsHelp');" |
495 ' var span = document.getElementsByClassName("close")[0];' | 517 " var closeBtn = modal ? modal.querySelector('.close') : null;" |
496 " if (openBtn && modal) {" | 518 " if (openBtn && modal) {" |
497 " openBtn.onclick = function() {" | 519 " openBtn.addEventListener('click', function(){ modal.style.display = 'block'; });" |
498 ' modal.style.display = "block";' | |
499 " };" | |
500 " }" | 520 " }" |
501 " if (span && modal) {" | 521 " if (closeBtn && modal) {" |
502 " span.onclick = function() {" | 522 " closeBtn.addEventListener('click', function(){ modal.style.display = 'none'; });" |
503 ' modal.style.display = "none";' | |
504 " };" | |
505 " }" | 523 " }" |
506 " window.onclick = function(event) {" | 524 " window.addEventListener('click', function(ev){" |
507 " if (event.target == modal) {" | 525 " if (ev.target === modal) { modal.style.display = 'none'; }" |
508 ' modal.style.display = "none";' | 526 " });" |
509 " }" | |
510 " }" | |
511 "});" | 527 "});" |
512 "</script>" | 528 "</script>" |
513 ) | 529 ) |
514 return modal_css + modal_html + modal_js | 530 return modal_html + modal_js |