view utils.py @ 8:1aed7d47c5ec draft default tip

planemo upload for repository https://github.com/goeckslab/gleam commit 8112548ac44b7a4769093d76c722c8fcdeaaef54
author goeckslab
date Fri, 25 Jul 2025 19:02:32 +0000
parents a32ff7201629
children
line wrap: on
line source

import base64
import logging
from typing import Optional

import numpy as np

logging.basicConfig(level=logging.DEBUG)
LOG = logging.getLogger(__name__)


def get_html_template() -> str:
    return """
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Model Training Report</title>
        <style>
          body {
              font-family: Arial, sans-serif;
              margin: 0;
              padding: 20px;
              background-color: #f4f4f4;
          }
          /* allow horizontal scrolling if content overflows */
          .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;
          }

          /* wrapper for tables to allow individual horizontal scroll */
          .table-wrapper {
              overflow-x: auto;
              margin: 1rem 0;
          }

          /* revert table styling to full borders */
          table {
              width: 100%;
              border-collapse: collapse;
              margin: 20px 0;
          }
          table, th, td {
              border: 1px solid #ddd;
          }
          th, td {
              padding: 8px;
              text-align: left;
          }
          th {
              background-color: #4CAF50;
              color: white;
          }

          .plot {
              text-align: center;
              margin: 20px 0;
          }
          .plot img {
              max-width: 100%;
              height: auto;
          }

          .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;
          }

          .tab-content {
              display: none;
              padding: 20px;
              border: 1px solid #ccc;
              border-top: none;
              background: white;
          }
          .tab-content.active {
              display: block;
          }

          .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;
          }

          /* sortable table header arrows */
          table.sortable th {
              position: relative;
              padding-right: 20px; /* room for the arrow */
              cursor: pointer;
          }
          table.sortable th::after {
              content: '↕';
              position: absolute;
              right: 8px;
              opacity: 0.4;
              transition: opacity 0.2s;
          }
          table.sortable th:hover::after {
              opacity: 0.7;
          }
          table.sortable th.sorted-asc::after {
              content: '↑';
              opacity: 1;
          }
          table.sortable th.sorted-desc::after {
              content: '↓';
              opacity: 1;
          }
        </style>
    </head>
    <body>
    <div class="container">
    """


def get_html_closing() -> str:
    return """
    </div>
    <script>
    document.addEventListener('DOMContentLoaded', () => {
      document.querySelectorAll('table.sortable').forEach(table => {
        const getCellValue = (row, idx) =>
          row.children[idx].innerText.trim() || '';

        const comparer = (idx, asc) => (a, b) => {
          const v1 = getCellValue(asc ? a : b, idx);
          const v2 = getCellValue(asc ? b : a, idx);
          const n1 = parseFloat(v1), n2 = parseFloat(v2);
          if (!isNaN(n1) && !isNaN(n2)) return n1 - n2;
          return v1.localeCompare(v2);
        };

        table.querySelectorAll('th').forEach((th, idx) => {
          let asc = true;
          th.addEventListener('click', () => {
            // sort rows
            const tbody = table.tBodies[0];
            Array.from(tbody.rows)
              .sort(comparer(idx, asc))
              .forEach(row => tbody.appendChild(row));
            // update arrow classes
            table.querySelectorAll('th').forEach(h => {
              h.classList.remove('sorted-asc','sorted-desc');
            });
            th.classList.add(asc ? 'sorted-asc' : 'sorted-desc');
            asc = !asc;
          });
        });
      });
    });
    </script>
    </body>
    </html>
    """


def build_tabbed_html(
    summary_html: str,
    test_html: str,
    feature_html: str,
    explainer_html: Optional[str] = None,
) -> str:
    """
    Render the tabbed sections and an always-visible Help button.
    """
    # CSS
    css = get_html_template().split("<body>")[1].rsplit("</style>", 1)[0] + "</style>"

    # Tabs header
    tabs = [
        '<div class="tabs">',
        '<div class="tab active" onclick="showTab(\'summary\')">Validation Summary & Config</div>',
        '<div class="tab" onclick="showTab(\'test\')">Test Summary</div>',
        '<div class="tab" onclick="showTab(\'feature\')">Feature Importance</div>',
    ]
    if explainer_html:
        tabs.append(
            '<div class="tab" onclick="showTab(\'explainer\')">Explainer Plots</div>'
        )
    tabs.append('<button id="openMetricsHelp" class="help-btn">Help</button>')
    tabs.append("</div>")
    tabs_section = "\n".join(tabs)

    # Content
    contents = [
        f'<div id="summary" class="tab-content active">{summary_html}</div>',
        f'<div id="test" class="tab-content">{test_html}</div>',
        f'<div id="feature" class="tab-content">{feature_html}</div>',
    ]
    if explainer_html:
        contents.append(
            f'<div id="explainer" class="tab-content">{explainer_html}</div>'
        )
    content_section = "\n".join(contents)

    # JS
    js = """
<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');
}
</script>
"""

    return css + "\n" + tabs_section + "\n" + content_section + "\n" + js


def customize_figure_layout(fig, margin_dict=None):
    if margin_dict is None:
        margin_dict = {"l": 40, "r": 40, "t": 40, "b": 40}
    fig.update_layout(margin=margin_dict)
    return fig


def add_plot_to_html(fig, include_plotlyjs=True) -> str:
    custom_margin = {"l": 40, "r": 40, "t": 60, "b": 60}
    fig = customize_figure_layout(fig, margin_dict=custom_margin)
    return fig.to_html(
        full_html=False,
        default_height=350,
        include_plotlyjs="cdn" if include_plotlyjs else False,
    )


def add_hr_to_html() -> str:
    return "<hr>"


def encode_image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as img_file:
        return base64.b64encode(img_file.read()).decode("utf-8")


def predict_proba(self, X):
    pred = self.predict(X)
    return np.vstack((1 - pred, pred)).T