reduce visible stats.

This commit is contained in:
Felix Delattre 2026-05-03 19:43:54 +02:00
parent 5ceeeabd11
commit 33746b35f9
2 changed files with 40 additions and 63 deletions

View file

@ -134,7 +134,10 @@
<canvas id="scenariosgcctimeseries" class="combined-plot-canvas"></canvas> <canvas id="scenariosgcctimeseries" class="combined-plot-canvas"></canvas>
</div> </div>
<div class="combined-plot"> <div class="combined-plot">
<div class="combined-plot-label">Metrics Comparison: All Scenarios</div> <div class="combined-plot-label">Metrics vs PhenoCam (fusion scenarios)</div>
<p style="margin:4px 0 8px; font-size:11px; color:#555; max-width:720px;">
R² (variance explained), nRMSE (RMSE normalised by PhenoCam σ), NSE_PC (NashSutcliffe vs PhenoCam). Other metrics remain in <code>metrics.json</code>.
</p>
<div id="metricsTable" style="overflow-x: auto; margin-top: 10px;"></div> <div id="metricsTable" style="overflow-x: auto; margin-top: 10px;"></div>
</div> </div>
</div> </div>
@ -605,8 +608,8 @@
const scenarios = ["aggressive_sigma20", "aggressive_sigma30", "nonaggressive_sigma20", "nonaggressive_sigma30"]; const scenarios = ["aggressive_sigma20", "aggressive_sigma30", "nonaggressive_sigma20", "nonaggressive_sigma30"];
const scenarioNames = ["Aggressive σ20", "Aggressive σ30", "Non-aggressive σ20", "Non-aggressive σ30"]; const scenarioNames = ["Aggressive σ20", "Aggressive σ30", "Non-aggressive σ20", "Non-aggressive σ30"];
const metrics = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse_pc"]; const metrics = ["r_squared", "nrmse", "nse_pc"];
const metricLabels = { pearson_r: "r", r_squared: "R²", rmse: "RMSE", mae: "MAE", nrmse: "nRMSE", nse_pc: "NSE_PC" }; const metricLabels = { r_squared: "R²", nrmse: "nRMSE", nse_pc: "NSE_PC" };
let html = "<table style='width:100%; border-collapse:collapse; font-size:11px;'>"; let html = "<table style='width:100%; border-collapse:collapse; font-size:11px;'>";
html += "<thead><tr style='background:#f5f5f5; border-bottom:2px solid #ccc;'>"; html += "<thead><tr style='background:#f5f5f5; border-bottom:2px solid #ccc;'>";
@ -621,7 +624,12 @@
html += `<td style='padding:6px 8px; font-weight:600;'>S2 (baseline)</td>`; html += `<td style='padding:6px 8px; font-weight:600;'>S2 (baseline)</td>`;
metrics.forEach(m => { metrics.forEach(m => {
const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m]; const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m];
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse_pc" ? val.toFixed(3) : val.toFixed(4)) : "—"; const fmt =
val !== null && val !== undefined
? m === "nrmse"
? val.toFixed(4)
: val.toFixed(3)
: "—";
html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`; html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`;
}); });
html += "</tr>"; html += "</tr>";
@ -635,7 +643,12 @@
html += `<td style='padding:6px 8px; font-weight:500;'>${scenarioNames[i]}</td>`; html += `<td style='padding:6px 8px; font-weight:500;'>${scenarioNames[i]}</td>`;
metrics.forEach(m => { metrics.forEach(m => {
const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m]; const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m];
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse_pc" ? val.toFixed(3) : val.toFixed(4)) : "—"; const fmt =
val !== null && val !== undefined
? m === "nrmse"
? val.toFixed(4)
: val.toFixed(3)
: "—";
html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`; html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`;
}); });
html += "</tr>"; html += "</tr>";
@ -646,7 +659,7 @@
// Add phenocam stats info if available // Add phenocam stats info if available
if (metricsData.phenocam_stats) { if (metricsData.phenocam_stats) {
const stats = metricsData.phenocam_stats; const stats = metricsData.phenocam_stats;
html += `<p style='margin-top:10px; font-size:10px; color:#666;'>PhenoCam stats: mean=${stats.mean.toFixed(3)}, std=${stats.std.toFixed(3)}, n=${stats.n_samples}</p>`; html += `<p style='margin-top:10px; font-size:10px; color:#666;'>PhenoCam GCC samples (ground truth): n = ${stats.n_samples}</p>`;
} }
container.innerHTML = html; container.innerHTML = html;

View file

@ -18,7 +18,6 @@
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; } th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
th { background: #f5f5f5; } th { background: #f5f5f5; }
td.num { text-align: right; font-variant-numeric: tabular-nums; } td.num { text-align: right; font-variant-numeric: tabular-nums; }
.compare-note { font-size: 12px; color: #555; margin: 0 0 8px 0; max-width: 720px; }
.section-note { font-size: 12px; color: #555; margin: -6px 0 8px 0; max-width: 720px; line-height: 1.45; } .section-note { font-size: 12px; color: #555; margin: -6px 0 8px 0; max-width: 720px; line-height: 1.45; }
.section-note code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; font-size: 11px; } .section-note code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
.intro { font-size: 13px; color: #333; background: #fafafa; border: 1px solid #e5e5e5; .intro { font-size: 13px; color: #333; background: #fafafa; border: 1px solid #e5e5e5;
@ -50,10 +49,22 @@
<div id="content"></div> <div id="content"></div>
</div> </div>
<script> <script>
const METRIC_COLS = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse_pc", "n_samples"]; /** Shown in the UI; pearson_r, rmse, mae, n_samples remain in metrics.json only. */
const DISPLAY_METRIC_COLS = ["r_squared", "nrmse", "nse_pc"];
const DISPLAY_METRIC_LABELS = {
r_squared: "R²",
nrmse: "nRMSE",
nse_pc: "NSE_PC",
};
function mv(m, c) { function mv(m, c) {
return c === "nse_pc" ? (m.nse_pc ?? m.nse) : m[c]; return c === "nse_pc" ? (m.nse_pc ?? m.nse) : m[c];
} }
function fmtMetric(col, v) {
if (v == null || typeof v !== "number") return "—";
if (col === "r_squared" || col === "nse_pc") return v.toFixed(3);
if (col === "nrmse") return v.toFixed(4);
return fmt(v);
}
let siteName = "innsbruck", season = "2024"; let siteName = "innsbruck", season = "2024";
let availableSiteSeasons = {}; let availableSiteSeasons = {};
const urlParams = new URLSearchParams(location.search); const urlParams = new URLSearchParams(location.search);
@ -76,68 +87,22 @@
return `${heading}<p class="empty">No data</p>`; return `${heading}<p class="empty">No data</p>`;
} }
const keys = Object.keys(obj).sort(); const keys = Object.keys(obj).sort();
let head = `<tr><th>Scenario</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`; let head = `<tr><th>Scenario</th>${DISPLAY_METRIC_COLS.map((c) => `<th>${DISPLAY_METRIC_LABELS[c]}</th>`).join("")}</tr>`;
const rows = keys.map((k) => { const rows = keys.map((k) => {
const m = obj[k] || {}; const m = obj[k] || {};
return `<tr><td>${k}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(mv(m, c))}</td>`).join("")}</tr>`; return `<tr><td>${k}</td>${DISPLAY_METRIC_COLS.map((c) => `<td class="num">${fmtMetric(c, mv(m, c))}</td>`).join("")}</tr>`;
}).join(""); }).join("");
return `${heading}<table>${head}${rows}</table>`; return `${heading}<table>${head}${rows}</table>`;
} }
/** Pair BtI keys (`aggressive_sigma20`) with ItB (`aggressive_sigma20_itb`). */
function btiItbPairs(obj) {
if (!obj || typeof obj !== "object") return [];
const pairs = [];
for (const itbKey of Object.keys(obj)) {
if (!itbKey.endsWith("_itb")) continue;
const btiKey = itbKey.slice(0, -"_itb".length);
const bti = obj[btiKey];
const itb = obj[itbKey];
if (!bti || !itb) continue;
pairs.push({ label: btiKey, bti, itb });
}
pairs.sort((a, b) => a.label.localeCompare(b.label));
return pairs;
}
function fmtDelta(btiM, itbM, col) {
const a = mv(btiM, col);
const b = mv(itbM, col);
if (a == null || b == null || typeof a !== "number" || typeof b !== "number") return "—";
return fmt(b - a);
}
function btiItbCompareSection(title, obj, blurb, metricCols = METRIC_COLS) {
const pairs = btiItbPairs(obj);
if (!pairs.length) return "";
const subHead = metricCols.map(
() => `<th class="num">BtI</th><th class="num">ItB</th><th class="num">Δ</th>`
).join("");
const head =
`<tr><th rowspan="2">Scenario</th>${metricCols.map((c) => `<th colspan="3">${c}</th>`).join("")}</tr>` +
`<tr>${subHead}</tr>`;
const rows = pairs
.map((p) => {
const cells = metricCols.map((c) => {
const vB = fmt(mv(p.bti, c));
const vI = fmt(mv(p.itb, c));
const d = fmtDelta(p.bti, p.itb, c);
return `<td class="num">${vB}</td><td class="num">${vI}</td><td class="num">${d}</td>`;
}).join("");
return `<tr><td>${p.label}</td>${cells}</tr>`;
})
.join("");
const heading = title ? `<h2>${title}</h2>` : "";
const note = blurb ? `<p class="compare-note">${blurb}</p>` : "";
return `${heading}${note}<table>${head}${rows}</table>`;
}
function baselineSection(b) { function baselineSection(b) {
if (!b || typeof b !== "object") return ""; if (!b || typeof b !== "object") return "";
const rows = []; const rows = [];
const pushRow = (label, m) => { const pushRow = (label, m) => {
if (!m || typeof m !== "object") return; if (!m || typeof m !== "object") return;
rows.push(`<tr><td>${label}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(mv(m, c))}</td>`).join("")}</tr>`); rows.push(
`<tr><td>${label}</td>${DISPLAY_METRIC_COLS.map((c) => `<td class="num">${fmtMetric(c, mv(m, c))}</td>`).join("")}</tr>`
);
}; };
pushRow("S2 GCC (all acquisitions)", b.s2); pushRow("S2 GCC (all acquisitions)", b.s2);
for (const strat of ["aggressive", "nonaggressive"]) { for (const strat of ["aggressive", "nonaggressive"]) {
@ -146,7 +111,7 @@
pushRow(`S2 Whittaker λ=400 (${strat})`, b.s2_whittaker_lambda400?.[strat]); pushRow(`S2 Whittaker λ=400 (${strat})`, b.s2_whittaker_lambda400?.[strat]);
} }
if (!rows.length) return ""; if (!rows.length) return "";
const head = `<tr><th>Baseline</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`; const head = `<tr><th>Baseline</th>${DISPLAY_METRIC_COLS.map((c) => `<th>${DISPLAY_METRIC_LABELS[c]}</th>`).join("")}</tr>`;
return `<h2>Baselines (temporal vs PhenoCam)</h2><table>${head}${rows.join("")}</table>`; return `<h2>Baselines (temporal vs PhenoCam)</h2><table>${head}${rows.join("")}</table>`;
} }
@ -166,6 +131,8 @@
<li><b>ItB</b> (<i>Index-then-Bands</i>): compute GCC from S2 and S3 first, then fuse the GCC rasters.</li> <li><b>ItB</b> (<i>Index-then-Bands</i>): compute GCC from S2 and S3 first, then fuse the GCC rasters.</li>
<li>Scenarios combine a cloud-screening <b>strategy</b> (<code>aggressive</code> / <code>nonaggressive</code>) <li>Scenarios combine a cloud-screening <b>strategy</b> (<code>aggressive</code> / <code>nonaggressive</code>)
and an EFAST fusion <b>σ</b> (<code>sigma20</code> / <code>sigma30</code>).</li> and an EFAST fusion <b>σ</b> (<code>sigma20</code> / <code>sigma30</code>).</li>
<li>Tables list <b></b> (fit vs PhenoCam), <b>nRMSE</b> (RMSE normalised by PhenoCam variability),
and <b>NSE_PC</b> (NashSutcliffe using PhenoCam as reference). Pearson <i>r</i>, RMSE, MAE, and sample counts stay in <code>metrics.json</code>.</li>
</ul> </ul>
</div>`; </div>`;
if (data.phenocam_stats) { if (data.phenocam_stats) {
@ -181,9 +148,6 @@
html += `<p class="section-note">Reference GCC series <i>before</i> any fusion: raw S2 (all dates and cloud-screened per strategy), S3 composite per strategy, and a Whittaker-smoothed S2 series (λ=400). Useful to see what fusion has to beat.</p>`; html += `<p class="section-note">Reference GCC series <i>before</i> any fusion: raw S2 (all dates and cloud-screened per strategy), S3 composite per strategy, and a Whittaker-smoothed S2 series (λ=400). Useful to see what fusion has to beat.</p>`;
html += baselineHtml.replace(/^<h2>[^<]*<\/h2>/, ""); html += baselineHtml.replace(/^<h2>[^<]*<\/h2>/, "");
} }
html += `<h2>Temporal: BtI vs ItB (paired)</h2>`;
html += `<p class="section-note">Per scenario (same strategy + σ), BtI and ItB side-by-side with <b>Δ = ItB BtI</b>. Positive Δ is better for Pearson r, R², and NSE; negative Δ is better for RMSE, MAE, and NRMSE.</p>`;
html += btiItbCompareSection("", data.temporal, "") || `<p class="empty">No paired scenarios</p>`;
html += `<h2>Temporal (vs PhenoCam)</h2>`; html += `<h2>Temporal (vs PhenoCam)</h2>`;
html += `<p class="section-note">Per-scenario agreement between the fusion GCC <b>timeseries</b> at the site 3×3 window and the PhenoCam GCC timeseries, across all matched dates. Scenarios ending in <code>_itb</code> are Index-then-Bands; the others are Bands-then-Index.</p>`; html += `<p class="section-note">Per-scenario agreement between the fusion GCC <b>timeseries</b> at the site 3×3 window and the PhenoCam GCC timeseries, across all matched dates. Scenarios ending in <code>_itb</code> are Index-then-Bands; the others are Bands-then-Index.</p>`;
html += tableSection("", data.temporal); html += tableSection("", data.temporal);