Foo
This commit is contained in:
parent
77e1488830
commit
374be6865d
19 changed files with 1276 additions and 64 deletions
|
|
@ -44,6 +44,7 @@
|
|||
<a href="fusion.html" class="active">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="siteName">Innsbruck</h1>
|
||||
|
|
|
|||
283
webapp/gap_validation.html
Normal file
283
webapp/gap_validation.html
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Gap validation</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: 18px; }
|
||||
.selectors select { padding: 5px 10px; font-size: 14px; margin-right: 15px; }
|
||||
h1 { font-size: 22px; margin-top: 0; }
|
||||
h2 { font-size: 16px; margin-top: 22px; color: #333; }
|
||||
h2:first-of-type { margin-top: 8px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 12px; margin-bottom: 14px; }
|
||||
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; vertical-align: top; }
|
||||
th { background: #f5f5f5; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
td.paths { font-size: 11px; word-break: break-all; color: #444; max-width: 420px; }
|
||||
.intro { font-size: 13px; color: #333; background: #fafafa; border: 1px solid #e5e5e5;
|
||||
padding: 10px 12px; border-radius: 4px; margin-bottom: 16px; line-height: 1.5; }
|
||||
.intro code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
|
||||
.section-note { font-size: 12px; color: #555; margin: -6px 0 8px 0; line-height: 1.45; }
|
||||
.empty { color: #666; font-style: italic; }
|
||||
.err { color: #a00; }
|
||||
details.meta { font-size: 12px; margin-top: 12px; border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px 12px; background: #fafafa; }
|
||||
details.meta summary { cursor: pointer; font-weight: 600; }
|
||||
details.meta pre { margin: 8px 0 0; overflow: auto; font-size: 11px; max-height: 200px; }
|
||||
</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">Metrics</a>
|
||||
<a href="gap_validation.html" class="active">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="pageTitle">Gap validation</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>
|
||||
let siteName = "innsbruck",
|
||||
season = "2024";
|
||||
let availableSiteSeasons = {};
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
async function probeSummary(sn, s) {
|
||||
try {
|
||||
const res = await fetch(`data/${sn}/${s}/validation/gap_validation_summary.json`, {
|
||||
method: "HEAD",
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(v, d = 4) {
|
||||
if (v == null || typeof v !== "number" || !Number.isFinite(v)) return "—";
|
||||
return v.toFixed(d);
|
||||
}
|
||||
|
||||
function fmtInt(v) {
|
||||
if (v == null || typeof v !== "number" || !Number.isFinite(v)) return "—";
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
||||
function crossoverBlock(summary) {
|
||||
const scen = summary.scenario;
|
||||
const wcRoot = summary.whittaker_crossover || {};
|
||||
const wc = (scen && wcRoot[scen]) || Object.values(wcRoot)[0];
|
||||
if (!wc) return "";
|
||||
const first = wc.first_gap_days_fusion_nse_below_whittaker;
|
||||
const def = wc.whittaker_definition || "";
|
||||
let h = `<h2>Whittaker crossover (NSE<sub>S2</sub>)</h2>`;
|
||||
h += `<p class="section-note">${def}</p>`;
|
||||
h += `<p class="section-note"><b>First gap length (days)</b> where fusion NSE<sub>S2</sub> < Whittaker NSE<sub>S2</sub> (strict): <b>${first != null ? first : "—"}</b> (none if fusion never falls below).</p>`;
|
||||
const rows = wc.by_gap || [];
|
||||
if (rows.length) {
|
||||
h += `<table><tr><th>Gap days</th><th class="num">NSE<sub>S2</sub> fusion</th><th class="num">NSE<sub>S2</sub> Whittaker</th></tr>`;
|
||||
for (const r of rows) {
|
||||
h += `<tr><td>${r.gap_days}</td><td class="num">${fmt(r.nse_s2_fusion, 3)}</td><td class="num">${fmt(r.nse_s2_whittaker, 3)}</td></tr>`;
|
||||
}
|
||||
h += `</table>`;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function manifestTable(manifest) {
|
||||
if (!manifest?.entries?.length) return "";
|
||||
let h = `<h2>Gap manifest</h2>`;
|
||||
h += `<p class="section-note">From <code>data/${siteName}/${season}/validation/gap_manifest.json</code>. Midpoint rule: ${manifest.entries[0]?.midpoint_rule || "—"}.</p>`;
|
||||
h += `<table><tr><th>Gap days</th><th>Prediction</th><th>Window</th><th>Withheld S2</th></tr>`;
|
||||
for (const e of manifest.entries) {
|
||||
const w = `${e.window_start} → ${e.window_end}`;
|
||||
h += `<tr><td>${e.gap_days}</td><td>${e.prediction_date}</td><td>${w}</td><td>${e.withheld_s2_filename || "—"}</td></tr>`;
|
||||
}
|
||||
h += `</table>`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function resultsTable(results) {
|
||||
if (!results?.length) return `<p class="empty">No result rows in summary.</p>`;
|
||||
const head = `<tr>
|
||||
<th>Gap</th><th>Prediction</th><th>Withheld REFL</th>
|
||||
<th class="num">RMSE<br><span style="font-weight:normal">gap</span></th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">gap</span></th>
|
||||
<th class="num">RMSE<br><span style="font-weight:normal">no gap</span></th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">no gap</span></th>
|
||||
<th class="num">ΔRMSE</th><th class="num">ΔNSE</th>
|
||||
<th class="num">NSE<sub>S2</sub><br><span style="font-weight:normal">Whitt.</span></th>
|
||||
<th class="num">n</th>
|
||||
<th>Paths / error</th>
|
||||
</tr>`;
|
||||
const parts = [head];
|
||||
for (const r of results) {
|
||||
if (r.error) {
|
||||
parts.push(
|
||||
`<tr><td>${r.gap_days ?? "—"}</td><td colspan="10" class="err">${r.error}</td><td class="paths">${r.fused_gap_path || ""}</td></tr>`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const g = r.spatial?.gap || {};
|
||||
const ng = r.spatial?.no_gap || {};
|
||||
const wh = r.spatial?.whittaker || {};
|
||||
const dRm = r.spatial?.delta_rmse;
|
||||
const dNs = r.spatial?.delta_nse;
|
||||
const p = r.paths || {};
|
||||
const pathNote = [p.fused_gap, p.fused_no_gap, p.withheld_s2_refl].filter(Boolean).join("<br>");
|
||||
parts.push(`<tr>
|
||||
<td>${r.gap_days}</td>
|
||||
<td>${r.prediction_date || "—"}</td>
|
||||
<td style="font-size:11px">${r.withheld_s2_filename || "—"}</td>
|
||||
<td class="num">${fmt(g.rmse)}</td>
|
||||
<td class="num">${fmt(g.nse_s2, 3)}</td>
|
||||
<td class="num">${fmt(ng.rmse)}</td>
|
||||
<td class="num">${fmt(ng.nse_s2, 3)}</td>
|
||||
<td class="num">${fmt(dRm)}</td>
|
||||
<td class="num">${fmt(dNs, 3)}</td>
|
||||
<td class="num">${fmt(wh.nse_s2, 3)}</td>
|
||||
<td class="num">${fmtInt(g.n_pixels)}</td>
|
||||
<td class="paths">${pathNote}</td>
|
||||
</tr>`);
|
||||
}
|
||||
return `<table>${parts.join("")}</table>`;
|
||||
}
|
||||
|
||||
function metaDetails(summary) {
|
||||
const cmd = summary.command_line;
|
||||
const git = summary.git_commit;
|
||||
if (!cmd && !git) return "";
|
||||
let h = `<details class="meta"><summary>Run metadata</summary>`;
|
||||
if (git) h += `<p>Git: <code>${git}</code></p>`;
|
||||
if (cmd?.length) h += `<pre>${cmd.map((x) => String(x)).join(" ")}</pre>`;
|
||||
h += `</details>`;
|
||||
return h;
|
||||
}
|
||||
|
||||
async function render(summary, manifest) {
|
||||
const el = document.getElementById("content");
|
||||
if (!summary) {
|
||||
el.innerHTML = `<p class="err">Could not load <code>data/${siteName}/${season}/validation/gap_validation_summary.json</code>.</p>
|
||||
<p class="section-note">From <code>processing/</code>: <code>python -m gap_validation.run --site ${siteName} --season ${season} --lat LAT --lon LON</code> (see <code>--help</code>). Serve from <code>processing/</code>: <code>python3 -m http.server 8000</code> → <code>/webapp/gap_validation.html</code> (<code>webapp/data</code> → <code>../data</code>).</p>`;
|
||||
if (manifest?.entries) el.innerHTML += manifestTable(manifest);
|
||||
return;
|
||||
}
|
||||
const scen = summary.scenario || "—";
|
||||
const sn = summary.site_name ?? siteName;
|
||||
const se = summary.season ?? season;
|
||||
let html = `<div class="intro">
|
||||
Tier-2 withheld S2, spatial GCC vs withheld scene, NSE<sub>S2</sub>, and Whittaker comparison.
|
||||
Summary: <code>data/${sn}/${se}/validation/gap_validation_summary.json</code>.
|
||||
Scenario in this file: <b>${scen}</b> (one run overwrites; re-run CLI for other strategy/σ/mode).
|
||||
</div>`;
|
||||
html += `<h2>Spatial metrics (per gap length)</h2>`;
|
||||
html += `<p class="section-note">Reference = GCC from withheld S2 REFL (bilinear to fusion grid). Prediction = fused GCC. ΔRMSE = RMSE<sub>gap</sub> − RMSE<sub>no gap</sub>; ΔNSE = NSE<sub>no gap</sub> − NSE<sub>gap</sub>.</p>`;
|
||||
html += resultsTable(summary.results);
|
||||
html += crossoverBlock(summary);
|
||||
html += metaDetails(summary);
|
||||
if (manifest?.entries) html += manifestTable(manifest);
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
let summary = null,
|
||||
manifest = null;
|
||||
try {
|
||||
const r1 = await fetch(`data/${siteName}/${season}/validation/gap_validation_summary.json`);
|
||||
summary = r1.ok ? await r1.json() : null;
|
||||
} catch {
|
||||
summary = null;
|
||||
}
|
||||
try {
|
||||
const r2 = await fetch(`data/${siteName}/${season}/validation/gap_manifest.json`);
|
||||
manifest = r2.ok ? await r2.json() : null;
|
||||
} catch {
|
||||
manifest = null;
|
||||
}
|
||||
await render(summary, manifest);
|
||||
const site = window.sitesData?.features?.find((f) => f.properties?.sitename === siteName);
|
||||
document.getElementById("pageTitle").textContent =
|
||||
(site?.properties?.description || siteName) + " — gap validation — " + 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 probeSummary(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>
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
|
|
@ -136,7 +137,7 @@
|
|||
<div class="combined-plot">
|
||||
<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 (Nash–Sutcliffe vs PhenoCam). <b>ΔNSE_PC</b> = NSE_PC(σ20)−NSE_PC(σ30): positive → σ20 better; negative → σ30 better. <b>Mean residual</b> (fused−PhenoCam): positive → fusion high vs PhenoCam on average; negative → low; compare BtI vs ItB in the same row (closer to 0 = less mean bias). Tables at the top when <code>metrics.json</code> has <code>derived</code> (regenerate with <code>metrics_stats.py</code> / <code>run.py</code>).
|
||||
<b>R² vs mean</b> (JSON <code>r_squared</code>): generalized R² vs predicting mean PhenoCam each day — same numeric value as <b>NSE_PC</b>, not (Pearson <i>r</i>)²; can be negative. <b>nRMSE</b> (RMSE / mean PhenoCam). <b>ΔNSE_PC</b> = NSE_PC(σ20)−NSE_PC(σ30): positive → σ20 better; negative → σ30 better. <b>Mean residual</b> (fused−PhenoCam): positive → fusion high vs PhenoCam on average; negative → low; compare BtI vs ItB in the same row (closer to 0 = less mean bias). Tables at the top when <code>metrics.json</code> has <code>derived</code> (regenerate with <code>metrics_stats.py</code> / <code>run.py</code>).
|
||||
</p>
|
||||
<div id="metricsTable" style="overflow-x: auto; margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
|
@ -642,7 +643,7 @@
|
|||
html +=
|
||||
"<p style='margin:0 0 2px; font-size:11px; font-weight:600;'>Mean residual (fused − PhenoCam): BtI vs ItB</p>";
|
||||
html +=
|
||||
"<p style='margin:0 0 6px; font-size:10px; color:#555; line-height:1.35;'>Each cell: mean(fused−PhenoCam) on matched dates. <b>+</b> overestimates, <b>−</b> underestimates; <b>~0</b> little mean bias (see R²/MAE for overall fit). Same row: column closer to 0 → less systematic offset vs PhenoCam (RQ1.1).</p>";
|
||||
"<p style='margin:0 0 6px; font-size:10px; color:#555; line-height:1.35;'>Each cell: mean(fused−PhenoCam) on matched dates. <b>+</b> overestimates, <b>−</b> underestimates; <b>~0</b> little mean bias (see R² vs mean / nRMSE / NSE_PC for overall fit). Same row: column closer to 0 → less systematic offset vs PhenoCam (RQ1.1).</p>";
|
||||
html +=
|
||||
"<table style='width:100%; border-collapse:collapse; font-size:11px; margin-bottom:10px;'><thead><tr style='background:#f5f5f5;'><th style='padding:6px;'>Strategy</th><th style='padding:6px;'>σ</th><th style='padding:6px; text-align:right;'>BtI</th><th style='padding:6px; text-align:right;'>ItB</th></tr></thead><tbody>";
|
||||
for (const row of d.bti_vs_itb_mean_residual) {
|
||||
|
|
@ -654,7 +655,7 @@
|
|||
const scenarios = ["aggressive_sigma20", "aggressive_sigma30", "nonaggressive_sigma20", "nonaggressive_sigma30"];
|
||||
const scenarioNames = ["Aggressive σ20", "Aggressive σ30", "Non-aggressive σ20", "Non-aggressive σ30"];
|
||||
const metrics = ["r_squared", "nrmse", "nse_pc"];
|
||||
const metricLabels = { r_squared: "R²", nrmse: "nRMSE", nse_pc: "NSE_PC" };
|
||||
const metricLabels = { r_squared: "R² vs mean", nrmse: "nRMSE", nse_pc: "NSE_PC" };
|
||||
|
||||
html += "<table style='width:100%; border-collapse:collapse; font-size:11px;'>";
|
||||
html += "<thead><tr style='background:#f5f5f5; border-bottom:2px solid #ccc;'>";
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html" class="active">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="siteName">Metrics</h1>
|
||||
|
|
@ -70,7 +71,7 @@
|
|||
/** 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²",
|
||||
r_squared: "R² vs mean",
|
||||
nrmse: "nRMSE",
|
||||
nse_pc: "NSE_PC",
|
||||
};
|
||||
|
|
@ -229,9 +230,9 @@
|
|||
<summary>How to read</summary>
|
||||
<ol>
|
||||
<li>All scores are satellite or fusion <b>GCC</b> vs <b>PhenoCam GCC</b> at the site 3×3 window, <b>same calendar days</b> only. Extra stats: <code>metrics.json</code>.</li>
|
||||
<li><b>R²</b>, <b>NSE_PC</b>: higher = better. <b>nRMSE</b>: lower = better.</li>
|
||||
<li><b>R² vs mean</b> and <b>NSE_PC</b> are the same value (1 − SS<sub>res</sub>/SS<sub>tot</sub> vs predicting mean PhenoCam each day); not (Pearson <i>r</i>)²; can be negative. Higher = better. <b>nRMSE</b>: lower = better.</li>
|
||||
<li><b>Fusion:</b> same row number in BtI and in ItB = same screening + same σ — compare left/right. Down one block = change screening or σ.</li>
|
||||
<li><b>Mean resid.</b> (if present): mean(fused − PhenoCam). Sign = average bias; use R² / nRMSE / NSE_PC for overall fit.</li>
|
||||
<li><b>Mean resid.</b> (if present): mean(fused − PhenoCam). Sign = average bias; use R² vs mean / nRMSE / NSE_PC for overall fit.</li>
|
||||
<li><b>Summaries:</b> ΔNSE_PC = NSE at σ20 minus NSE at σ30 (+ means σ20 wins). Paired table: closer to 0 = less mean bias.</li>
|
||||
</ol>
|
||||
</details>`;
|
||||
|
|
@ -245,6 +246,7 @@
|
|||
<li><b>ItB</b>: GCC on S2 and S3, then fuse GCC.</li>
|
||||
<li><b>Scenario</b>: screening (<code>aggressive</code> / <code>nonaggressive</code>) × σ (20 / 30 days).</li>
|
||||
<li><a href="phenology.html">Phenology</a> — PhenoCam SOS/EOS (TIMESAT).</li>
|
||||
<li><b>R² vs mean</b> — coefficient of determination vs a constant mean(PhenoCam) baseline; JSON key <code>r_squared</code>; duplicates <code>nse_pc</code>. Not (Pearson <i>r</i>)².</li>
|
||||
<li><code>metrics.json</code> — also Pearson <i>r</i>, RMSE, MAE, <code>n_samples</code>.</li>
|
||||
</ul>
|
||||
</details>`;
|
||||
|
|
@ -274,7 +276,7 @@
|
|||
const baselineTbl = baselineTable(data.baseline);
|
||||
if (baselineTbl) {
|
||||
html += `<h2>Baselines (vs PhenoCam)</h2>`;
|
||||
html += `<p class="section-note">Same columns as fusion (vs PhenoCam). Higher R² / NSE_PC, lower nRMSE = better. S3 = coarse-only; Whittaker = smoothed S2-only.</p>`;
|
||||
html += `<p class="section-note">Same columns as fusion (vs PhenoCam). Higher R² vs mean / NSE_PC, lower nRMSE = better. S3 = coarse-only; Whittaker = smoothed S2-only.</p>`;
|
||||
html += baselineTbl;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html" class="active">Phenology</a>
|
||||
</div>
|
||||
<h1>PhenoCam phenology (50% amplitude)</h1>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html" class="active">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="siteName">Innsbruck</h1>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="siteName">Innsbruck</h1>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<a href="fusion.html">Fusion</a>
|
||||
<a href="postprocessed.html">Postprocessed</a>
|
||||
<a href="metrics.html">Metrics</a>
|
||||
<a href="gap_validation.html">Gap validation</a>
|
||||
<a href="phenology.html">Phenology</a>
|
||||
</div>
|
||||
<h1 id="siteName">Innsbruck</h1>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue