Added gcc suitability stats.
This commit is contained in:
parent
d55ee31e8d
commit
a593683314
3 changed files with 907 additions and 15 deletions
173
index.html
173
index.html
|
|
@ -181,6 +181,13 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col
|
|||
.stat-badge.bti { background: #e3f2fd; color: #0d47a1; }
|
||||
.stat-badge.none { background: #f5f5f5; color: #777; font-weight: 400; }
|
||||
.stat-badge.insuf { background: #fce4ec; color: #b71c1c; font-weight: 400; }
|
||||
.stat-badge.pass { background: #e8f5e9; color: #1b5e20; }
|
||||
.stat-badge.partial { background: #fff3e0; color: #e65100; font-weight: 400; }
|
||||
.stat-badge.fail { background: #fce4ec; color: #b71c1c; font-weight: 400; }
|
||||
.stat-site-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 8px; }
|
||||
.stat-site-table th, .stat-site-table td { padding: 4px 8px; text-align: left; border-bottom: 1px solid #f0f0f0; }
|
||||
.stat-site-table th { color: #888; font-weight: 500; }
|
||||
.stat-site-table .sval { font-variant-numeric: tabular-nums; }
|
||||
.stat-row-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.stat-row-table td { padding: 3px 0; vertical-align: top; }
|
||||
.stat-row-table .slabel { color: #888; width: 46%; }
|
||||
|
|
@ -243,12 +250,7 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col
|
|||
<span class="overlay-meta" id="statsMeta"></span>
|
||||
<button type="button" class="overlay-close" id="statsClose">Close</button>
|
||||
</div>
|
||||
<div id="statsTabGcc" class="stats-tab-panel" style="display:none">
|
||||
<div class="stat-placeholder">
|
||||
<p>GCC suitability</p>
|
||||
Coming soon.
|
||||
</div>
|
||||
</div>
|
||||
<div id="statsTabGcc" class="stats-tab-panel" style="display:none"></div>
|
||||
<div id="statsTabComparison" class="stats-tab-panel"></div>
|
||||
<div id="statsTabSites" class="stats-tab-panel" style="display:none">
|
||||
<div class="stat-placeholder">
|
||||
|
|
@ -352,6 +354,8 @@ let statsOverlayOpen = false;
|
|||
let statsTab = "comparison";
|
||||
let statsData = null;
|
||||
let statsYear = null;
|
||||
let gccSuitabilityData = null;
|
||||
let gccSuitabilityYear = null;
|
||||
const maps3 = {}; // { s2, fusion, s3 } Leaflet instances
|
||||
const overlays3 = {}; // current ImageOverlay per map
|
||||
const markers3 = {}; // site dot markers per map
|
||||
|
|
@ -620,15 +624,23 @@ function betterOrderLabel(order) {
|
|||
}
|
||||
|
||||
function updateStatsMeta() {
|
||||
if (statsTab !== "comparison" || !statsData) {
|
||||
qs("#statsMeta").textContent = `${currentYear}`;
|
||||
if (statsTab === "comparison" && statsData) {
|
||||
const nPairs = statsData.metrics?.nse?.n_pairs;
|
||||
const alpha = statsData.alpha ?? 0.05;
|
||||
qs("#statsMeta").textContent = nPairs != null
|
||||
? `${nPairs} paired site${nPairs === 1 ? "" : "s"} · α=${alpha} · ${currentYear}`
|
||||
: `${currentYear}`;
|
||||
return;
|
||||
}
|
||||
const nPairs = statsData.metrics?.nse?.n_pairs;
|
||||
const alpha = statsData.alpha ?? 0.05;
|
||||
qs("#statsMeta").textContent = nPairs != null
|
||||
? `${nPairs} paired site${nPairs === 1 ? "" : "s"} · α=${alpha} · ${currentYear}`
|
||||
: `${currentYear}`;
|
||||
if (statsTab === "gcc" && gccSuitabilityData) {
|
||||
const nEl = gccSuitabilityData.n_sites_eligible;
|
||||
const nRep = gccSuitabilityData.n_sites_repr_pass;
|
||||
qs("#statsMeta").textContent = nEl != null
|
||||
? `${nEl} eligible · ${nRep} representative · ${currentYear}`
|
||||
: `${currentYear}`;
|
||||
return;
|
||||
}
|
||||
qs("#statsMeta").textContent = `${currentYear}`;
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
|
|
@ -720,6 +732,130 @@ async function loadStatsPanel() {
|
|||
}
|
||||
}
|
||||
|
||||
function suitabilityVerdictLabel(v) {
|
||||
if (v === "suitable") return "Suitable";
|
||||
if (v === "partially suitable") return "Partially suitable";
|
||||
if (v === "not suitable") return "Not suitable";
|
||||
if (v === "insufficient data") return "Insufficient data";
|
||||
return v || "—";
|
||||
}
|
||||
|
||||
function suitabilityBadgeClass(v) {
|
||||
if (v === "suitable") return "pass";
|
||||
if (v === "partially suitable") return "partial";
|
||||
if (v === "not suitable") return "fail";
|
||||
return "insuf";
|
||||
}
|
||||
|
||||
function yesNo(v) {
|
||||
if (v === true) return "yes";
|
||||
if (v === false) return "no";
|
||||
return "—";
|
||||
}
|
||||
|
||||
function winnerLabel(w) {
|
||||
if (w === "bti") return "BtI";
|
||||
if (w === "itb") return "ItB";
|
||||
if (w === "tie") return "tie";
|
||||
return "—";
|
||||
}
|
||||
|
||||
function gccDroppedSitesList(data) {
|
||||
const dropped = data.dropped_sites || [];
|
||||
if (!dropped.length) return "";
|
||||
const links = dropped.map(site =>
|
||||
`<button type="button" class="stat-site-link" data-site="${escHtml(site)}">${escHtml(site)}</button>`
|
||||
).join("");
|
||||
return `<div class="stat-dropped">
|
||||
<h4>Excluded sites (${dropped.length})</h4>
|
||||
<p class="stat-dropped-note">Missing coordinates or fewer cloud-free S2 dates than the LOOCV minimum.</p>
|
||||
<div class="stat-dropped-list">${links}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderGccSuitabilityPanel(data) {
|
||||
const panel = qs("#statsTabGcc");
|
||||
const agg = data.aggregate || {};
|
||||
const threshold = data.repr_r_threshold ?? 0.7;
|
||||
const verdict = agg.suitability_verdict;
|
||||
const badge = `<span class="stat-badge ${suitabilityBadgeClass(verdict)}">${suitabilityVerdictLabel(verdict)}</span>`;
|
||||
const row = (label, val) =>
|
||||
`<tr><td class="slabel">${label}</td><td class="sval">${val}</td></tr>`;
|
||||
|
||||
const siteRows = Object.entries(data.sites || {}).sort(([a], [b]) => a.localeCompare(b)).map(([site, s]) => {
|
||||
const rep = s.representative ?? s.representativeness?.representative;
|
||||
const repBadge = rep
|
||||
? '<span class="stat-badge pass">pass</span>'
|
||||
: '<span class="stat-badge fail">fail</span>';
|
||||
const loocv = s.loocv || {};
|
||||
return `<tr>
|
||||
<td><button type="button" class="stat-site-link" data-site="${escHtml(site)}">${escHtml(site)}</button></td>
|
||||
<td class="sval">${fmtStat(s.representativeness?.r)}</td>
|
||||
<td>${repBadge}</td>
|
||||
<td class="sval">${loocv.n_dates ?? "—"}</td>
|
||||
<td class="sval">${winnerLabel(s.winner_s2)} / ${winnerLabel(s.winner_phenocam)}</td>
|
||||
<td class="sval">${yesNo(s.winner_agreement)}</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
const aggregateCard = `<div class="stat-card">
|
||||
<h3>Aggregate verdict ${badge}</h3>
|
||||
<table class="stat-row-table">
|
||||
${row("Pooled Spearman (errors)", fmtStat(agg.pooled_spearman))}
|
||||
${row("Residual correlation", fmtStat(agg.residual_corr))}
|
||||
${row("Winner agreement rate", fmtStat(agg.winner_agreement_rate))}
|
||||
${row("LOOCV dates (pooled)", agg.n_loocv_dates ?? "—")}
|
||||
${row("Representative sites", `${data.n_sites_repr_pass ?? "—"} / ${data.n_sites_total ?? "—"}`)}
|
||||
${row("LOOCV-eligible sites", `${data.n_sites_eligible ?? "—"} / ${data.n_sites_total ?? "—"}`)}
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
const reprCard = `<div class="stat-card">
|
||||
<h3>Line A — PhenoCam vs S2 representativeness</h3>
|
||||
<p style="font-size:11px;color:#999;margin:0 0 8px">Pass when Pearson r ≥ ${threshold} (oblique footprint tracks co-located S2 GCC).</p>
|
||||
<table class="stat-site-table">
|
||||
<thead><tr>
|
||||
<th>Site</th><th>r</th><th>Pass</th><th>LOOCV n</th><th>Winner S2 / PC</th><th>Agree</th>
|
||||
</tr></thead>
|
||||
<tbody>${siteRows || '<tr><td colspan="6">No sites</td></tr>'}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
const concordanceCard = `<div class="stat-card">
|
||||
<h3>Line B — LOOCV concordance</h3>
|
||||
<p style="font-size:11px;color:#999;margin:0 0 8px">Same EFAST predictions scored against held-out S2 truth vs PhenoCam. Winner agreement and error correlations test whether PhenoCam ranks fusion methods like the satellite-internal reference.</p>
|
||||
<table class="stat-row-table">
|
||||
${row("Min cloud-free S2 gate", data.min_cloudfree_s2 ?? "—")}
|
||||
${row("Pooled LOOCV dates", agg.n_loocv_dates ?? "—")}
|
||||
${row("Winner agreement rate", fmtStat(agg.winner_agreement_rate))}
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
panel.innerHTML =
|
||||
`<div class="stat-summary">Is PhenoCam GCC a valid reference for ranking fusion accuracy? · ${data.n_sites_total ?? "—"} site(s) · r threshold ${threshold}</div>` +
|
||||
`<div class="stat-grid">${aggregateCard}${reprCard}${concordanceCard}</div>` +
|
||||
gccDroppedSitesList(data);
|
||||
updateStatsMeta();
|
||||
}
|
||||
|
||||
async function loadGccSuitabilityPanel() {
|
||||
const panel = qs("#statsTabGcc");
|
||||
panel.innerHTML = '<div class="stat-nodata">Loading…</div>';
|
||||
try {
|
||||
const data = await fetch(`data/gcc_suitability/${currentYear}.json`)
|
||||
.then(r => { if (!r.ok) throw new Error(); return r.json(); });
|
||||
gccSuitabilityData = data;
|
||||
gccSuitabilityYear = currentYear;
|
||||
renderGccSuitabilityPanel(data);
|
||||
} catch {
|
||||
gccSuitabilityData = null;
|
||||
gccSuitabilityYear = null;
|
||||
panel.innerHTML =
|
||||
'<div class="stat-nodata">No GCC suitability file found — run 7-gcc-suitability.py first.</div>';
|
||||
updateStatsMeta();
|
||||
}
|
||||
}
|
||||
|
||||
const STATS_TAB_PANELS = {
|
||||
comparison: "#statsTabComparison",
|
||||
gcc: "#statsTabGcc",
|
||||
|
|
@ -736,6 +872,9 @@ function switchStatsTab(tab, updateHash = true) {
|
|||
if (tab === "comparison") {
|
||||
if (statsYear !== currentYear || !statsData) loadStatsPanel();
|
||||
else updateStatsMeta();
|
||||
} else if (tab === "gcc") {
|
||||
if (gccSuitabilityYear !== currentYear || !gccSuitabilityData) loadGccSuitabilityPanel();
|
||||
else updateStatsMeta();
|
||||
} else {
|
||||
updateStatsMeta();
|
||||
}
|
||||
|
|
@ -783,9 +922,12 @@ async function init() {
|
|||
currentYear = +yearSel.value;
|
||||
statsData = null;
|
||||
statsYear = null;
|
||||
gccSuitabilityData = null;
|
||||
gccSuitabilityYear = null;
|
||||
buildSiteList();
|
||||
if (worldOverlayOpen) buildWorldMap();
|
||||
if (statsOverlayOpen && statsTab === "comparison") loadStatsPanel();
|
||||
if (statsOverlayOpen && statsTab === "gcc") loadGccSuitabilityPanel();
|
||||
});
|
||||
|
||||
qs("#worldMapBtn").addEventListener("click", () => openWorldOverlay());
|
||||
|
|
@ -797,6 +939,11 @@ async function init() {
|
|||
if (!link) return;
|
||||
pickSiteFromStats(link.dataset.site);
|
||||
});
|
||||
qs("#statsTabGcc").addEventListener("click", e => {
|
||||
const link = e.target.closest(".stat-site-link");
|
||||
if (!link) return;
|
||||
pickSiteFromStats(link.dataset.site);
|
||||
});
|
||||
qs("#worldClose").addEventListener("click", () => closeWorldOverlay());
|
||||
qs("#statsClose").addEventListener("click", () => closeStatsOverlay());
|
||||
qs("#worldOverlay").addEventListener("click", e => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue