diff test-data/sample_output.html @ 0:375c36923da1 draft default tip

planemo upload for repository https://github.com/goeckslab/gleam.git commit 1c6c1ad7a1b2bd3645aa0eafa2167784820b52e0
author goeckslab
date Tue, 09 Dec 2025 23:49:47 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sample_output.html	Tue Dec 09 23:49:47 2025 +0000
@@ -0,0 +1,990 @@
+<!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;
+    }
+    h3 {
+      margin-top: 20px;
+      color: #333;
+    }
+    .section {
+      margin-bottom: 30px;
+    }
+    .section-title {
+      border-bottom: 2px solid #4CAF50;
+      color: #4CAF50;
+      padding-bottom: 5px;
+      margin-top: 28px;
+    }
+    .card {
+      background: #fff;
+      padding: 15px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      margin: 10px 0;
+    }
+
+    /* 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;
+    }
+    .metric-table {
+      width: 100%;
+    }
+    .kv-table {
+      width: 100%;
+    }
+    .kv-table th:first-child,
+    .kv-table td:first-child {
+      text-align: left;
+      width: 30%;
+    }
+
+    .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;
+    }
+
+    /* 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;
+    }
+    .plotly-center {
+      display: flex;
+      justify-content: center;
+    }
+    .plotly-center .plotly-graph-div, .plotly-center .js-plotly-plot {
+      margin: 0 auto !important;
+    }
+    .js-plotly-plot, .plotly-graph-div {
+      margin-left: auto !important;
+      margin-right: auto !important;
+    }
+
+    /* 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>
+
+  <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';
+            }
+            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));
+          });
+        });
+      }
+
+      // Run after DOM is ready
+      if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', initPerfSummarySorting);
+      } else {
+        initPerfSummarySorting();
+      }
+    })();
+  </script>
+</head>
+<body>
+  <div class="container">
+    <script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
+    <style>
+      .plotly-center { 
+        display: flex; 
+        justify-content: center; 
+        margin: 20px 0;
+        width: 100%;
+      }
+      .plotly-center .plotly-graph-div, .plotly-center .js-plotly-plot { 
+        margin: 0 auto !important; 
+      }
+      .js-plotly-plot, .plotly-graph-div { 
+        margin-left: auto !important; 
+        margin-right: auto !important; 
+      }
+      #learning-curve-accuracy, #learning-curve-loss, #threshold-plot,
+      #confusion-matrix, #per-class-metrics, #roc-curve, #pr-curve {
+        min-height: 400px;
+      }
+    </style>
+
+    <div class="tabs">
+      <div class="tab active" onclick="showTab('summary')">Model Summary & Config</div>
+      <div class="tab" onclick="showTab('train')">Train/Validation Summary</div>
+      <div class="tab" onclick="showTab('test')">Test Summary</div>
+      <div class="tab" onclick="showTab('feature')">Feature Importance</div>
+      <button id="openMetricsHelp" class="help-btn">Help</button>
+    </div>
+
+    <!-- Summary Tab -->
+    <div id="summary" class="tab-content active">
+      <section class="section">
+        <h2 class="section-title">Model Performance Summary</h2>
+        <div class="card">
+          <table class="metric-table">
+            <thead><tr><th>Metric</th><th>Train</th><th>Validation</th><th>Test</th></tr></thead>
+            <tbody>
+              <tr><td>accuracy</td><td>0.9234</td><td>0.8912</td><td>0.8856</td></tr>
+              <tr><td>f1</td><td>0.9201</td><td>0.8876</td><td>0.8823</td></tr>
+              <tr><td>precision</td><td>0.9156</td><td>0.8845</td><td>0.8798</td></tr>
+              <tr><td>recall</td><td>0.9245</td><td>0.8907</td><td>0.8849</td></tr>
+              <tr><td>roc_auc</td><td>0.9789</td><td>0.9543</td><td>0.9512</td></tr>
+              <tr><td>log_loss</td><td>0.2134</td><td>0.2876</td><td>0.3012</td></tr>
+            </tbody>
+          </table>
+        </div>
+      </section>
+
+      <section class="section">
+        <h2 class="section-title">Run Configuration</h2>
+        <div class="card">
+          <table class="kv-table">
+            <thead><tr><th>Key</th><th>Value</th></tr></thead>
+            <tbody>
+              <tr><td>Predictor type</td><td>MultiModalPredictor</td></tr>
+              <tr><td>Framework</td><td>AutoGluon Multimodal</td></tr>
+              <tr><td>Model architecture</td><td>timm_image=resnet50, hf_text=bert-base-uncased</td></tr>
+              <tr><td>Modalities & Inputs</td><td>Images + Tabular</td></tr>
+              <tr><td>Label column</td><td>target</td></tr>
+              <tr><td>Image columns</td><td>image_path</td></tr>
+              <tr><td>Tabular columns</td><td>15</td></tr>
+              <tr><td>Presets</td><td>medium_quality</td></tr>
+              <tr><td>Eval metric</td><td>accuracy</td></tr>
+              <tr><td>Decision threshold calibration</td><td>enabled</td></tr>
+              <tr><td>Decision threshold (Test only)</td><td>0.500</td></tr>
+              <tr><td>Seed</td><td>42</td></tr>
+              <tr><td>time limit(s)</td><td>3600</td></tr>
+            </tbody>
+          </table>
+        </div>
+      </section>
+
+      <section class="section">
+        <h2 class="section-title">Class Balance (Train Full)</h2>
+        <div class="card">
+          <h3>Class Balance (Train Full)</h3>
+          <table class="table">
+            <thead><tr><th>Class</th><th>Count</th><th>Percent</th></tr></thead>
+            <tbody>
+              <tr><td>0</td><td>1245</td><td>45.23%</td></tr>
+              <tr><td>1</td><td>1508</td><td>54.77%</td></tr>
+            </tbody>
+          </table>
+        </div>
+      </section>
+    </div>
+
+    <!-- Train Tab -->
+    <div id="train" class="tab-content">
+      <h2>Train/Validation Performance Summary</h2>
+      
+      <div class="card">
+        <table class="metric-table">
+          <thead><tr><th>Metric</th><th>Train</th><th>Validation</th></tr></thead>
+          <tbody>
+            <tr><td>accuracy</td><td>0.9234</td><td>0.8912</td></tr>
+            <tr><td>f1</td><td>0.9201</td><td>0.8876</td></tr>
+            <tr><td>precision</td><td>0.9156</td><td>0.8845</td></tr>
+            <tr><td>recall</td><td>0.9245</td><td>0.8907</td></tr>
+            <tr><td>roc_auc</td><td>0.9789</td><td>0.9543</td></tr>
+            <tr><td>log_loss</td><td>0.2134</td><td>0.2876</td></tr>
+          </tbody>
+        </table>
+      </div>
+
+      <h2>Learning Curves — Label Accuracy</h2>
+      <div class="plotly-center">
+        <div id="learning-curve-accuracy" style="width:900px;height:500px;"></div>
+      </div>
+
+      <h2>Learning Curves — Label Loss</h2>
+      <div class="plotly-center">
+        <div id="learning-curve-loss" style="width:900px;height:500px;"></div>
+      </div>
+    </div>
+
+    <!-- Test Tab -->
+    <div id="test" class="tab-content">
+      <h2>Test Performance Summary</h2>
+      <table class="performance-summary">
+        <thead>
+          <tr>
+            <th class="sortable">Metric</th>
+            <th class="sortable">Test</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr><td>accuracy</td><td>0.8856</td></tr>
+          <tr><td>f1</td><td>0.8823</td></tr>
+          <tr><td>precision</td><td>0.8798</td></tr>
+          <tr><td>recall</td><td>0.8849</td></tr>
+          <tr><td>roc_auc</td><td>0.9512</td></tr>
+          <tr><td>log_loss</td><td>0.3012</td></tr>
+          <tr><td>specificity (TNR)</td><td>0.8765</td></tr>
+          <tr><td>sensitivity (Sensitivity/TPR)</td><td>0.8923</td></tr>
+        </tbody>
+      </table>
+
+      <h2>Confusion Matrix</h2>
+      <div class="plotly-center">
+        <div id="confusion-matrix" style="width:700px;height:600px;"></div>
+      </div>
+
+      <h2>Per-Class Metrics</h2>
+      <div class="plotly-center">
+        <div id="per-class-metrics" style="width:900px;height:500px;"></div>
+      </div>
+
+      <h2>ROC Curve</h2>
+      <div class="plotly-center">
+        <div id="roc-curve" style="width:800px;height:600px;"></div>
+      </div>
+
+      <h2>Precision–Recall Curve</h2>
+      <div class="plotly-center">
+        <div id="pr-curve" style="width:800px;height:600px;"></div>
+      </div>
+
+      <h2>Threshold Plot</h2>
+      <div class="plotly-center">
+        <div id="threshold-plot" style="width:900px;height:500px;"></div>
+      </div>
+    </div>
+
+    <!-- Feature Importance Tab -->
+    <div id="feature" class="tab-content">
+      <section class="section">
+        <h2 class="section-title">Feature Importance</h2>
+        <div class="card">
+          <p>Permutation importance is not supported for MultiModalPredictor in this tool. For tabular-only runs, this section shows permutation importance.</p>
+        </div>
+      </section>
+
+      <section class="section">
+        <h2 class="section-title">Modalities & Inputs</h2>
+        <div class="card">
+          <h3>Modalities & Inputs</h3>
+          <p>This run used <b>MultiModalPredictor</b> (images + tabular).</p>
+          <p><b>Label column:</b> target</p>
+          <p><b>Image column:</b> image_path</p>
+          <div><b>Tabular columns:</b> 15
+            <ul>
+              <li>feature_1</li>
+              <li>feature_2</li>
+              <li>feature_3</li>
+              <li>feature_4</li>
+              <li>feature_5</li>
+              <li>feature_6</li>
+              <li>feature_7</li>
+              <li>feature_8</li>
+              <li>feature_9</li>
+              <li>feature_10</li>
+              <li>feature_11</li>
+              <li>feature_12</li>
+              <li>feature_13</li>
+              <li>feature_14</li>
+              <li>feature_15</li>
+            </ul>
+          </div>
+        </div>
+      </section>
+    </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');
+      }
+
+      // Fixed random data for reproducibility
+      const epochs = Array.from({length: 31}, (_, i) => i);
+      const accuracy_train = [0.552, 0.568, 0.581, 0.595, 0.612, 0.628, 0.645, 0.662, 0.678, 0.692, 0.708, 0.722, 0.735, 0.748, 0.761, 0.773, 0.784, 0.795, 0.805, 0.814, 0.823, 0.831, 0.839, 0.846, 0.853, 0.859, 0.865, 0.870, 0.875, 0.880, 0.884];
+      const accuracy_val = [0.752, 0.768, 0.782, 0.795, 0.807, 0.818, 0.828, 0.837, 0.845, 0.852, 0.859, 0.865, 0.871, 0.876, 0.881, 0.885, 0.889, 0.893, 0.896, 0.899, 0.902, 0.905, 0.907, 0.909, 0.911, 0.913, 0.915, 0.917, 0.918, 0.920, 0.921];
+      const loss_train = [1.352, 1.285, 1.221, 1.158, 1.098, 1.041, 0.987, 0.936, 0.888, 0.842, 0.799, 0.758, 0.720, 0.684, 0.650, 0.618, 0.588, 0.560, 0.534, 0.510, 0.487, 0.466, 0.447, 0.429, 0.412, 0.397, 0.383, 0.370, 0.358, 0.347, 0.337];
+      const loss_val = [0.802, 0.765, 0.730, 0.697, 0.666, 0.637, 0.610, 0.585, 0.561, 0.539, 0.518, 0.499, 0.481, 0.464, 0.448, 0.433, 0.419, 0.406, 0.394, 0.383, 0.372, 0.362, 0.353, 0.345, 0.337, 0.330, 0.323, 0.317, 0.311, 0.306, 0.301];
+
+      // Wait for Plotly to load before creating plots
+      function createPlots() {
+        if (typeof Plotly === 'undefined') {
+          console.log('Waiting for Plotly...');
+          setTimeout(createPlots, 100);
+          return;
+        }
+        
+        console.log('Plotly loaded, creating plots...');
+
+        // Learning Curves — Label Accuracy (with both training and validation)
+        try {
+          Plotly.newPlot('learning-curve-accuracy', [
+            {
+              x: epochs,
+              y: accuracy_train,
+              type: 'scatter',
+              mode: 'lines+markers',
+              name: 'Training',
+              line: { width: 3, color: '#1f77b4', shape: 'spline' },
+              marker: { size: 8, color: '#1f77b4' }
+            },
+            {
+              x: epochs,
+              y: accuracy_val,
+              type: 'scatter',
+              mode: 'lines+markers',
+              name: 'Validation',
+              line: { width: 3, color: '#ff7f0e', shape: 'spline' },
+              marker: { size: 8, color: '#ff7f0e' }
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'Epoch', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false
+            },
+            yaxis: { 
+              title: 'Accuracy', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0.5, 1.0]
+            },
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white'
+          });
+          console.log('Accuracy plot created');
+        } catch(e) {
+          console.error('Error creating accuracy plot:', e);
+        }
+
+        // Learning Curves — Label Loss (with both training and validation)
+        try {
+          Plotly.newPlot('learning-curve-loss', [
+            {
+              x: epochs,
+              y: loss_train,
+              type: 'scatter',
+              mode: 'lines+markers',
+              name: 'Training Loss',
+              line: { width: 3, color: '#1f77b4', shape: 'spline' },
+              marker: { size: 8, color: '#1f77b4' }
+            },
+            {
+              x: epochs,
+              y: loss_val,
+              type: 'scatter',
+              mode: 'lines+markers',
+              name: 'Validation Loss',
+              line: { width: 3, color: '#ff7f0e', shape: 'spline' },
+              marker: { size: 8, color: '#ff7f0e' }
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'Epoch', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false
+            },
+            yaxis: { 
+              title: 'Loss', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0.2, 1.4]
+            },
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white'
+          });
+          console.log('Loss plot created');
+        } catch(e) {
+          console.error('Error creating loss plot:', e);
+        }
+
+        // Threshold Plot (with fixed data matching the expected visualization)
+        // Precision: starts low (~0.42), increases to peak (~0.95) around threshold 0.5, then drops sharply to 0
+        // Recall: starts at 1, stays at 1 until ~0.1, then decreases to 0 around 0.9
+        // F1: starts around 0.6, peaks around 0.5 at ~0.95, then drops to 0
+        const thresholds = Array.from({length: 101}, (_, i) => i / 100);
+        
+        // Generate precision values: low at start, peak around 0.5, drop to 0
+        const precision_vals = thresholds.map(t => {
+          if (t <= 0.5) {
+            return 0.42 + (t / 0.5) * 0.53; // 0.42 to 0.95
+          } else if (t <= 0.9) {
+            return 0.95 - ((t - 0.5) / 0.4) * 0.95; // 0.95 to 0
+          } else {
+            return 0;
+          }
+        });
+        
+        // Generate recall values: starts at 1, stays at 1 until ~0.1, then decreases to 0 around 0.9
+        const recall_vals = thresholds.map(t => {
+          if (t <= 0.1) {
+            return 1.0;
+          } else if (t <= 0.9) {
+            return 1.0 - ((t - 0.1) / 0.8) * 1.0; // 1.0 to 0
+          } else {
+            return 0;
+          }
+        });
+        
+        // Calculate F1 from precision and recall
+        const f1_vals = thresholds.map((t, i) => {
+          const p = precision_vals[i];
+          const r = recall_vals[i];
+          if (p + r === 0) return 0;
+          return 2 * (p * r) / (p + r);
+        });
+        
+        // Queue Rate: decreases linearly from 1 to 0
+        const queue_rate = thresholds.map(t => 1 - t);
+
+        try {
+          Plotly.newPlot('threshold-plot', [
+            { 
+              x: thresholds, 
+              y: precision_vals, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: 'Precision', 
+              line: { width: 3, color: '#1f77b4' } 
+            },
+            { 
+              x: thresholds, 
+              y: recall_vals, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: 'Recall', 
+              line: { width: 3, color: '#ff7f0e' } 
+            },
+            { 
+              x: thresholds, 
+              y: f1_vals, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: 'F1', 
+              line: { width: 3, color: '#2ca02c' } 
+            },
+            { 
+              x: thresholds, 
+              y: queue_rate, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: 'Queue Rate', 
+              line: { width: 2, color: '#808080', dash: 'dash' } 
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'Discrimination Threshold', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false
+            },
+            yaxis: { 
+              title: 'Score', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0, 1]
+            },
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white',
+            shapes: [{
+              type: 'line',
+              xref: 'x',
+              yref: 'y',
+              x0: 0.51,
+              y0: 0,
+              x1: 0.51,
+              y1: 1,
+              line: {
+                color: 'black',
+                width: 2,
+                dash: 'dash'
+              }
+            }],
+            annotations: [{
+              x: 0.51,
+              y: 0.85,
+              text: 't* = 0.51',
+              showarrow: false,
+              font: { size: 12, color: 'black' }
+            }]
+          });
+          console.log('Threshold plot created');
+        } catch(e) {
+          console.error('Error creating threshold plot:', e);
+        }
+
+        // Confusion Matrix (matching imagelearner style with Blues colorscale)
+        const cm_data = [[542, 78], [65, 515]];
+        const total = cm_data.flat().reduce((a, b) => a + b, 0);
+        const labels = ['0', '1'];
+        
+        // Build annotations array with all white text
+        const annotations = [];
+        cm_data.forEach((row, i) => {
+          row.forEach((val, j) => {
+            const pct = ((val / total) * 100).toFixed(1);
+            // All text is white
+            const textColor = 'white';
+            // Count annotation (bold, bottom)
+            annotations.push({
+              x: labels[j],
+              y: labels[i],
+              text: '<b>' + val + '</b>',
+              showarrow: false,
+              font: { color: textColor, size: 14 },
+              xanchor: 'center',
+              yanchor: 'bottom',
+              yshift: 2
+            });
+            // Percentage annotation (top)
+            annotations.push({
+              x: labels[j],
+              y: labels[i],
+              text: pct + '%',
+              showarrow: false,
+              font: { color: textColor, size: 13 },
+              xanchor: 'center',
+              yanchor: 'top',
+              yshift: -2
+            });
+          });
+        });
+        
+        try {
+          Plotly.newPlot('confusion-matrix', [{
+            z: cm_data,
+            x: labels,
+            y: labels,
+            type: 'heatmap',
+            colorscale: 'Blues',
+            showscale: true,
+            colorbar: { title: 'Count' },
+            xgap: 2,
+            ygap: 2,
+            hovertemplate: 'True=%{y}<br>Pred=%{x}<br>Count=%{z}<extra></extra>',
+            zmin: 0
+          }], {
+            xaxis: { title: 'Predicted label', type: 'category' },
+            yaxis: { title: 'True label', type: 'category', autorange: 'reversed' },
+            margin: { l: 80, r: 20, t: 40, b: 80 },
+            template: 'plotly_white',
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white',
+            annotations: annotations
+          });
+          console.log('Confusion matrix created');
+        } catch(e) {
+          console.error('Error creating confusion matrix:', e);
+        }
+
+        // Per-Class Metrics
+        const classes = ['Class 0', 'Class 1'];
+        const precision_per_class = [0.8929, 0.8685];
+        const recall_per_class = [0.8742, 0.8879];
+        const f1_per_class = [0.8835, 0.8781];
+
+        try {
+          Plotly.newPlot('per-class-metrics', [
+            { 
+              x: classes, 
+              y: precision_per_class, 
+              type: 'bar', 
+              name: 'Precision', 
+              marker: { color: '#4CAF50' } 
+            },
+            { 
+              x: classes, 
+              y: recall_per_class, 
+              type: 'bar', 
+              name: 'Recall', 
+              marker: { color: '#2196F3' } 
+            },
+            { 
+              x: classes, 
+              y: f1_per_class, 
+              type: 'bar', 
+              name: 'F1', 
+              marker: { color: '#FF9800' } 
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'Class', 
+              gridcolor: '#e0e0e0',
+              showgrid: true
+            },
+            yaxis: { 
+              title: 'Score', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              range: [0, 1]
+            },
+            barmode: 'group',
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white'
+          });
+          console.log('Per-class metrics created');
+        } catch(e) {
+          console.error('Error creating per-class metrics:', e);
+        }
+
+        // ROC Curve (with fixed data)
+        const fpr = [0,0.01,0.02,0.03,0.04,0.05,0.06,0.07,0.08,0.09,0.10,0.11,0.12,0.13,0.14,0.15,0.16,0.17,0.18,0.19,0.20,0.21,0.22,0.23,0.24,0.25,0.26,0.27,0.28,0.29,0.30,0.31,0.32,0.33,0.34,0.35,0.36,0.37,0.38,0.39,0.40,0.41,0.42,0.43,0.44,0.45,0.46,0.47,0.48,0.49,0.50,0.51,0.52,0.53,0.54,0.55,0.56,0.57,0.58,0.59,0.60,0.61,0.62,0.63,0.64,0.65,0.66,0.67,0.68,0.69,0.70,0.71,0.72,0.73,0.74,0.75,0.76,0.77,0.78,0.79,0.80,0.81,0.82,0.83,0.84,0.85,0.86,0.87,0.88,0.89,0.90,0.91,0.92,0.93,0.94,0.95,0.96,0.97,0.98,0.99,1.00];
+        const tpr = [0,0.12,0.23,0.33,0.42,0.50,0.57,0.63,0.69,0.74,0.78,0.82,0.85,0.88,0.90,0.92,0.94,0.95,0.96,0.97,0.98,0.985,0.99,0.992,0.994,0.996,0.997,0.998,0.999,0.9995,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0];
+        const auc_score = 0.935;
+
+        try {
+          Plotly.newPlot('roc-curve', [
+            { 
+              x: [0, 1], 
+              y: [0, 1], 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: 'Random (AUC = 0.50)', 
+              line: { dash: 'dash', color: 'gray', width: 2 } 
+            },
+            { 
+              x: fpr, 
+              y: tpr, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: `ROC Curve (AUC = ${auc_score.toFixed(3)})`, 
+              line: { width: 3, color: '#1f77b4' } 
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'False Positive Rate', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0, 1]
+            },
+            yaxis: { 
+              title: 'True Positive Rate', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0, 1]
+            },
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white'
+          });
+          console.log('ROC curve created');
+        } catch(e) {
+          console.error('Error creating ROC curve:', e);
+        }
+
+        // Precision-Recall Curve (with fixed data)
+        const recall_pr = [0,0.01,0.02,0.03,0.04,0.05,0.06,0.07,0.08,0.09,0.10,0.11,0.12,0.13,0.14,0.15,0.16,0.17,0.18,0.19,0.20,0.21,0.22,0.23,0.24,0.25,0.26,0.27,0.28,0.29,0.30,0.31,0.32,0.33,0.34,0.35,0.36,0.37,0.38,0.39,0.40,0.41,0.42,0.43,0.44,0.45,0.46,0.47,0.48,0.49,0.50,0.51,0.52,0.53,0.54,0.55,0.56,0.57,0.58,0.59,0.60,0.61,0.62,0.63,0.64,0.65,0.66,0.67,0.68,0.69,0.70,0.71,0.72,0.73,0.74,0.75,0.76,0.77,0.78,0.79,0.80,0.81,0.82,0.83,0.84,0.85,0.86,0.87,0.88,0.89,0.90,0.91,0.92,0.93,0.94,0.95,0.96,0.97,0.98,0.99,1.00];
+        const precision_pr = [0.95,0.949,0.948,0.947,0.946,0.945,0.944,0.943,0.942,0.941,0.940,0.939,0.938,0.937,0.936,0.935,0.934,0.933,0.932,0.931,0.930,0.929,0.928,0.927,0.926,0.925,0.924,0.923,0.922,0.921,0.920,0.919,0.918,0.917,0.916,0.915,0.914,0.913,0.912,0.911,0.910,0.909,0.908,0.907,0.906,0.905,0.904,0.903,0.902,0.901,0.900,0.899,0.898,0.897,0.896,0.895,0.894,0.893,0.892,0.891,0.890,0.889,0.888,0.887,0.886,0.885,0.884,0.883,0.882,0.881,0.880,0.879,0.878,0.877,0.876,0.875,0.874,0.873,0.872,0.871,0.870,0.869,0.868,0.867,0.866,0.865,0.864,0.863,0.862,0.861,0.860,0.859,0.858,0.857,0.856,0.855,0.854,0.853,0.852,0.851,0.850];
+        const ap_score = 0.9234;
+
+        try {
+          Plotly.newPlot('pr-curve', [
+            { 
+              x: recall_pr, 
+              y: precision_pr, 
+              type: 'scatter', 
+              mode: 'lines', 
+              name: `Precision-Recall Curve (AP = ${ap_score.toFixed(4)})`, 
+              line: { width: 3, color: '#1f77b4' } 
+            }
+          ], {
+            template: 'plotly_white',
+            xaxis: { 
+              title: 'Recall', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0, 1]
+            },
+            yaxis: { 
+              title: 'Precision', 
+              gridcolor: '#e0e0e0',
+              showgrid: true,
+              zeroline: false,
+              range: [0.6, 1]
+            },
+            legend: { 
+              orientation: 'h', 
+              yanchor: 'bottom', 
+              y: 1.02, 
+              xanchor: 'right', 
+              x: 1.0 
+            },
+            margin: { l: 60, r: 20, t: 40, b: 50 },
+            plot_bgcolor: 'white',
+            paper_bgcolor: 'white'
+          });
+          console.log('PR curve created');
+        } catch(e) {
+          console.error('Error creating PR curve:', e);
+        }
+        
+        console.log('All plots created successfully!');
+      }
+
+      // Start creating plots when DOM is ready
+      if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', createPlots);
+      } else {
+        createPlots();
+      }
+    </script>
+  </div>
+</body>
+</html>
+