efast-phenocam-validation/webapp/metrics.html
Felix Delattre 5ceeeabd11 foo:
2026-05-03 17:07:25 +02:00

266 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Metrics</title>
<style>
body { margin: 0; font-family: sans-serif; }
.nav { margin-bottom: 15px; font-size: 14px; }
.nav a { margin-right: 12px; color: #0066cc; text-decoration: none; }
.nav a:hover { text-decoration: underline; }
.nav a.active { font-weight: bold; }
.container { max-width: 1100px; margin: 0 auto; padding: 20px; }
.selectors { margin-bottom: 20px; }
.selectors select { padding: 5px 10px; font-size: 14px; margin-right: 15px; }
h1 { font-size: 22px; }
h2 { font-size: 16px; margin-top: 24px; color: #333; }
table { border-collapse: collapse; width: 100%; font-size: 13px; margin-bottom: 12px; }
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
th { background: #f5f5f5; }
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 code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
.intro { font-size: 13px; color: #333; background: #fafafa; border: 1px solid #e5e5e5;
padding: 10px 12px; border-radius: 4px; margin-bottom: 18px; line-height: 1.5; }
.intro ul { margin: 6px 0 0 18px; padding: 0; }
.intro li { margin-bottom: 2px; }
.empty { color: #666; font-style: italic; }
.err { color: #a00; }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<a href="index.html">Full</a>
<a href="preselection.html">Pre-selection</a>
<a href="prepared.html">Prepared</a>
<a href="fusion.html">Fusion</a>
<a href="postprocessed.html">Postprocessed</a>
<a href="metrics.html" class="active">Metrics</a>
<a href="phenology.html">Phenology</a>
</div>
<h1 id="siteName">Metrics</h1>
<div class="selectors">
<label>Site:</label>
<select id="siteSelect"></select>
<label>Season:</label>
<select id="seasonSelect"></select>
</div>
<div id="content"></div>
</div>
<script>
const METRIC_COLS = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse_pc", "n_samples"];
function mv(m, c) {
return c === "nse_pc" ? (m.nse_pc ?? m.nse) : m[c];
}
let siteName = "innsbruck", season = "2024";
let availableSiteSeasons = {};
const urlParams = new URLSearchParams(location.search);
async function probeMetrics(sn, s) {
try {
const res = await fetch(`data/${sn}/${s}/metrics.json`, { method: "HEAD" });
return res.ok;
} catch { return false; }
}
function fmt(v) {
if (v == null || typeof v !== "number") return "—";
return Number.isInteger(v) ? String(v) : v.toFixed(4);
}
function tableSection(title, obj) {
const heading = title ? `<h2>${title}</h2>` : "";
if (!obj || typeof obj !== "object" || !Object.keys(obj).length) {
return `${heading}<p class="empty">No data</p>`;
}
const keys = Object.keys(obj).sort();
let head = `<tr><th>Scenario</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`;
const rows = keys.map((k) => {
const m = obj[k] || {};
return `<tr><td>${k}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(mv(m, c))}</td>`).join("")}</tr>`;
}).join("");
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) {
if (!b || typeof b !== "object") return "";
const rows = [];
const pushRow = (label, m) => {
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>`);
};
pushRow("S2 GCC (all acquisitions)", b.s2);
for (const strat of ["aggressive", "nonaggressive"]) {
pushRow(`S3 composite GCC (${strat})`, b.s3?.[strat]);
pushRow(`S2 GCC cloud-screened (${strat})`, b.s2_cloudfree?.[strat]);
pushRow(`S2 Whittaker λ=400 (${strat})`, b.s2_whittaker_lambda400?.[strat]);
}
if (!rows.length) return "";
const head = `<tr><th>Baseline</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`;
return `<h2>Baselines (temporal vs PhenoCam)</h2><table>${head}${rows.join("")}</table>`;
}
function render(data) {
const el = document.getElementById("content");
if (!data) {
el.innerHTML = `<p class="err">Could not load metrics.json</p>`;
return;
}
let html = "";
html += `
<div class="intro">
All metrics compare a greenness index (GCC) from satellite products against PhenoCam
ground-truth GCC at the site's 3×3 pixel window.
<ul>
<li><b>BtI</b> (<i>Bands-then-Index</i>): fuse S2/S3 reflectance, then compute GCC from the fused bands.</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>)
and an EFAST fusion <b>σ</b> (<code>sigma20</code> / <code>sigma30</code>).</li>
</ul>
</div>`;
if (data.phenocam_stats) {
html += `<h2>PhenoCam</h2>`;
html += `<p class="section-note">Summary statistics of the PhenoCam GCC timeseries used as ground truth for this site and season.</p>`;
html += `<table><tr><th>mean</th><th>std</th><th>min</th><th>max</th><th>n</th></tr><tr>`;
const p = data.phenocam_stats;
html += `<td class="num">${fmt(p.mean)}</td><td class="num">${fmt(p.std)}</td><td class="num">${fmt(p.min)}</td><td class="num">${fmt(p.max)}</td><td class="num">${fmt(p.n_samples)}</td></tr></table>`;
}
const baselineHtml = baselineSection(data.baseline);
if (baselineHtml) {
html += `<h2>Baselines (temporal vs PhenoCam)</h2>`;
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 += `<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 += `<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);
el.innerHTML = html || `<p class="empty">Empty metrics file</p>`;
}
async function load() {
try {
const res = await fetch(`data/${siteName}/${season}/metrics.json`);
render(res.ok ? await res.json() : null);
} catch {
render(null);
}
const site = window.sitesData?.features?.find((f) => f.properties?.sitename === siteName);
document.getElementById("siteName").textContent = (site?.properties?.description || siteName) + " — " + season;
urlParams.set("site", siteName);
urlParams.set("season", season);
history.replaceState({}, "", `?${urlParams}`);
}
async function init() {
try {
const res = await fetch("data/sites.geojson");
window.sitesData = res.ok ? await res.json() : { features: [] };
} catch { window.sitesData = { features: [] }; }
const features = window.sitesData.features || [];
for (const f of features) {
const sn = f.properties?.sitename;
if (!sn) continue;
const seasonsFromGeo = f.properties?.seasons ? Object.keys(f.properties.seasons).sort() : [];
const withData = [];
for (const s of seasonsFromGeo) {
if (await probeMetrics(sn, s)) withData.push(s);
}
if (withData.length) availableSiteSeasons[sn] = withData;
}
const availableSites = Object.keys(availableSiteSeasons);
const siteSelect = document.getElementById("siteSelect");
siteSelect.innerHTML = "";
(availableSites.length ? availableSites.sort() : ["innsbruck"]).forEach((sn) => {
const opt = document.createElement("option");
opt.value = sn;
opt.textContent = sn;
siteSelect.appendChild(opt);
if (!availableSiteSeasons[sn]) availableSiteSeasons[sn] = ["2024"];
});
const urlSite = urlParams.get("site");
const urlSeason = urlParams.get("season");
const initialSite = urlSite && availableSiteSeasons[urlSite] ? urlSite : availableSites[0] || "innsbruck";
const initialSeason =
urlSeason && (availableSiteSeasons[initialSite] || []).includes(urlSeason)
? urlSeason
: (availableSiteSeasons[initialSite] || [])[0] || "2024";
siteSelect.value = initialSite;
document.getElementById("seasonSelect").innerHTML = (availableSiteSeasons[initialSite] || [])
.map((s) => `<option value="${s}">${s}</option>`)
.join("");
document.getElementById("seasonSelect").value = initialSeason;
siteName = initialSite;
season = initialSeason;
siteSelect.addEventListener("change", function () {
const sn = this.value;
const seas = availableSiteSeasons[sn] || [];
document.getElementById("seasonSelect").innerHTML = seas.map((s) => `<option value="${s}">${s}</option>`).join("");
document.getElementById("seasonSelect").value = seas[0] || "2024";
siteName = sn;
season = document.getElementById("seasonSelect").value;
load();
});
document.getElementById("seasonSelect").addEventListener("change", function () {
season = this.value;
load();
});
await load();
}
init();
</script>
</body>
</html>