efast-phenocam-validation/webapp/metrics.html
Felix Delattre 94f910d978 foo
2026-04-20 08:55:35 +02:00

250 lines
12 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; }
.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>
</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"];
/** Spatial fusion metrics in metrics.json (no RMSE block at site level). */
const SPATIAL_METRIC_COLS = ["pearson_r", "r_squared", "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) {
if (!obj || typeof obj !== "object" || !Object.keys(obj).length) {
return `<h2>${title}</h2><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 `<h2>${title}</h2><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("");
return `<h2>${title}</h2><p class="compare-note">${blurb}</p><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 = "";
if (data.phenocam_stats) {
html += `<h2>PhenoCam</h2><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>`;
}
html += baselineSection(data.baseline);
html += btiItbCompareSection(
"Temporal: BtI vs ItB (paired)",
data.temporal,
"Δ = ItB BtI. For Pearson r, R², and NSE (%), positive Δ means ItB is higher. For RMSE, MAE, and NRMSE, negative Δ means ItB is better (lower error)."
);
html += tableSection("Temporal (vs PhenoCam)", data.temporal);
html += btiItbCompareSection(
"Spatial: BtI vs ItB (paired)",
data.spatial,
"Δ = ItB BtI. For Pearson r and R², positive Δ means ItB is higher.",
SPATIAL_METRIC_COLS
);
html += tableSection("Spatial (3×3 fusion mean vs PhenoCam)", data.spatial);
if (data.summary) {
html += `<h2>Summary</h2><pre style="font-size:13px;background:#f9f9f9;padding:10px;">${JSON.stringify(data.summary, null, 2)}</pre>`;
}
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>