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