Mercurial > repos > goeckslab > image_learner
diff 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 |
line wrap: on
line diff
--- a/utils.py Wed Aug 27 21:02:48 2025 +0000 +++ b/utils.py Mon Sep 08 22:38:35 2025 +0000 @@ -3,205 +3,301 @@ def get_html_template(): + """ + Returns the opening HTML, <head> (with CSS/JS), and opens <body> + .container. + Includes: + - Base styling for layout and tables + - Sortable table headers with 3-state arrows (none ⇅, asc ↑, desc ↓) + - A scroll helper class (.scroll-rows-30) that approximates ~30 visible rows + - A guarded script so initializing runs only once even if injected twice + """ return """ - <html> - <head> - <meta charset="UTF-8"> - <title>Galaxy-Ludwig Report</title> - <style> - body { - font-family: Arial, sans-serif; - margin: 0; - padding: 20px; - background-color: #f4f4f4; - } - .container { - max-width: 800px; - margin: auto; - background: white; - padding: 20px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - overflow-x: auto; - } - h1 { - text-align: center; - color: #333; - } - h2 { - border-bottom: 2px solid #4CAF50; - color: #4CAF50; - padding-bottom: 5px; - } - /* baseline table setup */ - table { - border-collapse: collapse; - margin: 20px 0; - width: 100%; - table-layout: fixed; - } - table, th, td { - border: 1px solid #ddd; - } - th, td { - padding: 8px; - text-align: center; - vertical-align: middle; - word-wrap: break-word; - } - th { - background-color: #4CAF50; - color: white; - } - .plot { - text-align: center; - margin: 20px 0; - } - .plot img { - max-width: 100%; - height: auto; - } +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Galaxy-Ludwig Report</title> + <style> + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f4f4f4; + } + .container { + max-width: 1200px; + margin: auto; + background: white; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + overflow-x: auto; + } + h1 { + text-align: center; + color: #333; + } + h2 { + border-bottom: 2px solid #4CAF50; + color: #4CAF50; + padding-bottom: 5px; + margin-top: 28px; + } + + /* baseline table setup */ + table { + border-collapse: collapse; + margin: 20px 0; + width: 100%; + table-layout: fixed; + background: #fff; + } + table, th, td { + border: 1px solid #ddd; + } + th, td { + padding: 10px; + text-align: center; + vertical-align: middle; + word-break: break-word; + white-space: normal; + overflow-wrap: anywhere; + } + th { + background-color: #4CAF50; + color: white; + } + + .plot { + text-align: center; + margin: 20px 0; + } + .plot img { + max-width: 100%; + height: auto; + border: 1px solid #ddd; + } + + /* ------------------- + sortable columns (3-state: none ⇅, asc ↑, desc ↓) + ------------------- */ + table.performance-summary th.sortable { + cursor: pointer; + position: relative; + user-select: none; + } + /* default icon space */ + table.performance-summary th.sortable::after { + content: '⇅'; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 0.8em; + color: #eaf5ea; /* light on green */ + text-shadow: 0 0 1px rgba(0,0,0,0.15); + } + /* three states override the default */ + table.performance-summary th.sortable.sorted-none::after { content: '⇅'; color: #eaf5ea; } + table.performance-summary th.sortable.sorted-asc::after { content: '↑'; color: #ffffff; } + table.performance-summary th.sortable.sorted-desc::after { content: '↓'; color: #ffffff; } + + /* show ~30 rows with a scrollbar (tweak if you want) */ + .scroll-rows-30 { + max-height: 900px; /* ~30 rows depending on row height */ + overflow-y: auto; /* vertical scrollbar (“sidebar”) */ + overflow-x: auto; + } - /* ------------------- - SORTABLE COLUMNS - ------------------- */ - table.performance-summary th.sortable { - cursor: pointer; - position: relative; - user-select: none; - } - /* hide arrows by default */ - table.performance-summary th.sortable::after { - content: ''; - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - font-size: 0.8em; - color: #666; - } - /* three states */ - table.performance-summary th.sortable.sorted-none::after { - content: '⇅'; - } - table.performance-summary th.sortable.sorted-asc::after { - content: '↑'; - } - table.performance-summary th.sortable.sorted-desc::after { - content: '↓'; - } - </style> + /* Tabs + Help button (used by build_tabbed_html) */ + .tabs { + display: flex; + align-items: center; + border-bottom: 2px solid #ccc; + margin-bottom: 1rem; + gap: 6px; + flex-wrap: wrap; + } + .tab { + padding: 10px 20px; + cursor: pointer; + border: 1px solid #ccc; + border-bottom: none; + background: #f9f9f9; + margin-right: 5px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + } + .tab.active { + background: white; + font-weight: bold; + } + .help-btn { + margin-left: auto; + padding: 6px 12px; + font-size: 0.9rem; + border: 1px solid #4CAF50; + border-radius: 4px; + background: #4CAF50; + color: white; + cursor: pointer; + } + .tab-content { + display: none; + padding: 20px; + border: 1px solid #ccc; + border-top: none; + background: #fff; + } + .tab-content.active { + display: block; + } - <!-- sorting script --> - <script> - document.addEventListener('DOMContentLoaded', () => { - // 1) record each row's original position - document.querySelectorAll('table.performance-summary tbody').forEach(tbody => { - Array.from(tbody.rows).forEach((row, i) => { - row.dataset.originalOrder = i; - }); - }); + /* Modal (used by get_metrics_help_modal) */ + .modal { + display: none; + position: fixed; + z-index: 9999; + left: 0; top: 0; + width: 100%; height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.4); + } + .modal-content { + background-color: #fefefe; + margin: 8% auto; + padding: 20px; + border: 1px solid #888; + width: 90%; + max-width: 900px; + border-radius: 8px; + } + .modal .close { + color: #777; + float: right; + font-size: 28px; + font-weight: bold; + line-height: 1; + margin-left: 8px; + } + .modal .close:hover, + .modal .close:focus { + color: black; + text-decoration: none; + cursor: pointer; + } + .metrics-guide h3 { margin-top: 20px; } + .metrics-guide p { margin: 6px 0; } + .metrics-guide ul { margin: 10px 0; padding-left: 20px; } + </style> - const getText = cell => cell.innerText.trim(); - const comparer = (idx, asc) => (a, b) => { - const v1 = getText(a.children[idx]); - const v2 = getText(b.children[idx]); - const n1 = parseFloat(v1), n2 = parseFloat(v2); - if (!isNaN(n1) && !isNaN(n2)) { - return asc ? n1 - n2 : n2 - n1; + <script> + // Guard to avoid double-initialization if this block is included twice + (function(){ + if (window.__perfSummarySortInit) return; + window.__perfSummarySortInit = true; + + function initPerfSummarySorting() { + // Record original order for "back to original" + document.querySelectorAll('table.performance-summary tbody').forEach(tbody => { + Array.from(tbody.rows).forEach((row, i) => { row.dataset.originalOrder = i; }); + }); + + const getText = td => (td?.innerText || '').trim(); + const cmp = (idx, asc) => (a, b) => { + const v1 = getText(a.children[idx]); + const v2 = getText(b.children[idx]); + const n1 = parseFloat(v1), n2 = parseFloat(v2); + if (!isNaN(n1) && !isNaN(n2)) return asc ? n1 - n2 : n2 - n1; // numeric + return asc ? v1.localeCompare(v2) : v2.localeCompare(v1); // lexical + }; + + document.querySelectorAll('table.performance-summary th.sortable').forEach(th => { + // initialize to “none” + th.classList.remove('sorted-asc','sorted-desc'); + th.classList.add('sorted-none'); + + th.addEventListener('click', () => { + const table = th.closest('table'); + const headerRow = th.parentNode; + const allTh = headerRow.querySelectorAll('th.sortable'); + const tbody = table.querySelector('tbody'); + + // Determine current state BEFORE clearing + const isAsc = th.classList.contains('sorted-asc'); + const isDesc = th.classList.contains('sorted-desc'); + + // Reset all headers in this row + allTh.forEach(x => x.classList.remove('sorted-asc','sorted-desc','sorted-none')); + + // Compute next state + let next; + if (!isAsc && !isDesc) { + next = 'asc'; + } else if (isAsc) { + next = 'desc'; + } else { + next = 'none'; } - return asc - ? v1.localeCompare(v2) - : v2.localeCompare(v1); - }; - - document - .querySelectorAll('table.performance-summary th.sortable') - .forEach(th => { - // initialize to "none" state - th.classList.add('sorted-none'); - th.addEventListener('click', () => { - const table = th.closest('table'); - const allTh = table.querySelectorAll('th.sortable'); - - // 1) determine current state BEFORE clearing classes - let curr = th.classList.contains('sorted-asc') - ? 'asc' - : th.classList.contains('sorted-desc') - ? 'desc' - : 'none'; - // 2) cycle to next state - let next = curr === 'none' - ? 'asc' - : curr === 'asc' - ? 'desc' - : 'none'; + th.classList.add('sorted-' + next); - // 3) clear all sort markers - allTh.forEach(h => - h.classList.remove('sorted-none','sorted-asc','sorted-desc') - ); - // 4) apply the new marker - th.classList.add(`sorted-${next}`); + // Sort rows according to the chosen state + const rows = Array.from(tbody.rows); + if (next === 'none') { + rows.sort((a, b) => (a.dataset.originalOrder - b.dataset.originalOrder)); + } else { + const idx = Array.from(headerRow.children).indexOf(th); + rows.sort(cmp(idx, next === 'asc')); + } + rows.forEach(r => tbody.appendChild(r)); + }); + }); + } - // 5) sort or restore original order - const tbody = table.querySelector('tbody'); - let rows = Array.from(tbody.rows); - if (next === 'none') { - rows.sort((a, b) => - a.dataset.originalOrder - b.dataset.originalOrder - ); - } else { - const idx = Array.from(th.parentNode.children).indexOf(th); - rows.sort(comparer(idx, next === 'asc')); - } - rows.forEach(r => tbody.appendChild(r)); - }); - }); - }); - </script> - </head> - <body> - <div class="container"> - """ + // Run after DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPerfSummarySorting); + } else { + initPerfSummarySorting(); + } + })(); + </script> +</head> +<body> + <div class="container"> +""" def get_html_closing(): + """Closes .container, body, and html.""" return """ - </div> - </body> - </html> - """ + </div> +</body> +</html> +""" -def encode_image_to_base64(image_path): +def encode_image_to_base64(image_path: str) -> str: """Convert an image file to a base64 encoded string.""" with open(image_path, "rb") as img_file: return base64.b64encode(img_file.read()).decode("utf-8") -def json_to_nested_html_table(json_data, depth=0): +def json_to_nested_html_table(json_data, depth: int = 0) -> str: """ - Convert JSON object to an HTML nested table. - - Parameters: - json_data (dict or list): The JSON data to convert. - depth (int): Current depth level for indentation. - - Returns: - str: HTML string for the nested table. + Convert a JSON-able object to an HTML nested table. + Renders dicts as two-column tables (key/value) and lists as index/value rows. """ - # Base case: if JSON is a simple key-value pair dictionary + # Base case: flat dict (no nested dict/list values) if isinstance(json_data, dict) and all( not isinstance(v, (dict, list)) for v in json_data.values() ): - # Render a flat table rows = [ f"<tr><th>{key}</th><td>{value}</td></tr>" for key, value in json_data.items() ] return f"<table>{''.join(rows)}</table>" - # Base case: if JSON is a list of simple values + # Base case: list of simple values if isinstance(json_data, list) and all( not isinstance(v, (dict, list)) for v in json_data ): @@ -211,36 +307,34 @@ ] return f"<table>{''.join(rows)}</table>" - # Recursive case: if JSON contains nested structures + # Recursive cases if isinstance(json_data, dict): rows = [ - f"<tr><th style='padding-left:{depth * 20}px;'>{key}</th>" - f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" + ( + f"<tr><th style='text-align:left;padding-left:{depth * 20}px;'>{key}</th>" + f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" + ) for key, value in json_data.items() ] return f"<table>{''.join(rows)}</table>" if isinstance(json_data, list): rows = [ - f"<tr><th style='padding-left:{depth * 20}px;'>[{i}]</th>" - f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" + ( + f"<tr><th style='text-align:left;padding-left:{depth * 20}px;'>[{i}]</th>" + f"<td>{json_to_nested_html_table(value, depth + 1)}</td></tr>" + ) for i, value in enumerate(json_data) ] return f"<table>{''.join(rows)}</table>" - # Base case: simple value + # Primitive return f"{json_data}" -def json_to_html_table(json_data): +def json_to_html_table(json_data) -> str: """ - Convert JSON to a vertically oriented HTML table. - - Parameters: - json_data (str or dict): JSON string or dictionary. - - Returns: - str: HTML table representation. + Convert JSON (dict or string) into a vertically oriented HTML table. """ if isinstance(json_data, str): json_data = json.loads(json_data) @@ -248,56 +342,19 @@ def build_tabbed_html(metrics_html: str, train_val_html: str, test_html: str) -> str: + """ + Build a 3-tab interface: + - Config and Results Summary + - Train/Validation Results + - Test Results + Includes a persistent "Help" button that toggles the metrics modal. + """ return f""" -<style> - .tabs {{ - display: flex; - align-items: center; - border-bottom: 2px solid #ccc; - margin-bottom: 1rem; - }} - .tab {{ - padding: 10px 20px; - cursor: pointer; - border: 1px solid #ccc; - border-bottom: none; - background: #f9f9f9; - margin-right: 5px; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - }} - .tab.active {{ - background: white; - font-weight: bold; - }} - /* new help-button styling */ - .help-btn {{ - margin-left: auto; - padding: 6px 12px; - font-size: 0.9rem; - border: 1px solid #4CAF50; - border-radius: 4px; - background: #4CAF50; - color: white; - cursor: pointer; - }} - .tab-content {{ - display: none; - padding: 20px; - border: 1px solid #ccc; - border-top: none; - }} - .tab-content.active {{ - display: block; - }} -</style> - <div class="tabs"> <div class="tab active" onclick="showTab('metrics')">Config and Results Summary</div> <div class="tab" onclick="showTab('trainval')">Train/Validation Results</div> <div class="tab" onclick="showTab('test')">Test Results</div> - <!-- always-visible help button --> - <button id="openMetricsHelp" class="help-btn">Help</button> + <button id="openMetricsHelp" class="help-btn" title="Open metrics help">Help</button> </div> <div id="metrics" class="tab-content active"> @@ -311,17 +368,26 @@ </div> <script> -function showTab(id) {{ - document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); - document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); - document.getElementById(id).classList.add('active'); - document.querySelector(`.tab[onclick*="${{id}}"]`).classList.add('active'); -}} + function showTab(id) {{ + document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); + document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); + document.getElementById(id).classList.add('active'); + // find tab with matching onclick target + document.querySelectorAll('.tab').forEach(t => {{ + if (t.getAttribute('onclick') && t.getAttribute('onclick').includes(id)) {{ + t.classList.add('active'); + }} + }}); + }} </script> """ def get_metrics_help_modal() -> str: + """ + Returns a ready-to-use modal with a comprehensive metrics guide and + the small script that wires the "Help" button to open/close the modal. + """ modal_html = ( '<div id="metricsHelpModal" class="modal">' ' <div class="modal-content">' @@ -442,73 +508,23 @@ " </div>" "</div>" ) - modal_css = ( - "<style>" - ".modal {" - " display: none;" - " position: fixed;" - " z-index: 1;" - " left: 0;" - " top: 0;" - " width: 100%;" - " height: 100%;" - " overflow: auto;" - " background-color: rgba(0,0,0,0.4);" - "}" - ".modal-content {" - " background-color: #fefefe;" - " margin: 15% auto;" - " padding: 20px;" - " border: 1px solid #888;" - " width: 80%;" - " max-width: 800px;" - "}" - ".close {" - " color: #aaa;" - " float: right;" - " font-size: 28px;" - " font-weight: bold;" - "}" - ".close:hover," - ".close:focus {" - " color: black;" - " text-decoration: none;" - " cursor: pointer;" - "}" - ".metrics-guide h3 {" - " margin-top: 20px;" - "}" - ".metrics-guide p {" - " margin: 5px 0;" - "}" - ".metrics-guide ul {" - " margin: 10px 0;" - " padding-left: 20px;" - "}" - "</style>" - ) + modal_js = ( "<script>" - 'document.addEventListener("DOMContentLoaded", function() {' - ' var modal = document.getElementById("metricsHelpModal");' - ' var openBtn = document.getElementById("openMetricsHelp");' - ' var span = document.getElementsByClassName("close")[0];' + "document.addEventListener('DOMContentLoaded', function() {" + " var modal = document.getElementById('metricsHelpModal');" + " var openBtn = document.getElementById('openMetricsHelp');" + " var closeBtn = modal ? modal.querySelector('.close') : null;" " if (openBtn && modal) {" - " openBtn.onclick = function() {" - ' modal.style.display = "block";' - " };" + " openBtn.addEventListener('click', function(){ modal.style.display = 'block'; });" " }" - " if (span && modal) {" - " span.onclick = function() {" - ' modal.style.display = "none";' - " };" + " if (closeBtn && modal) {" + " closeBtn.addEventListener('click', function(){ modal.style.display = 'none'; });" " }" - " window.onclick = function(event) {" - " if (event.target == modal) {" - ' modal.style.display = "none";' - " }" - " }" + " window.addEventListener('click', function(ev){" + " if (ev.target === modal) { modal.style.display = 'none'; }" + " });" "});" "</script>" ) - return modal_css + modal_html + modal_js + return modal_html + modal_js