Adjust shadow band for trend-like indicators

This commit is contained in:
2026-06-10 18:51:32 +08:00
parent 1c25681d8d
commit b4558d2bde
2 changed files with 47 additions and 11 deletions
+23 -5
View File
@@ -721,17 +721,33 @@ function quantile(sorted, q) {{
if (lo === hi) return sorted[lo]; if (lo === hi) return sorted[lo];
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo); return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
}} }}
function robustAnnualSeasonValues(points) {{ function annualMedianValues(points) {{
const byYear = new Map(); const byYear = new Map();
for (const p of points) {{ for (const p of points) {{
if (!byYear.has(p.year)) byYear.set(p.year, []); if (!byYear.has(p.year)) byYear.set(p.year, []);
byYear.get(p.year).push(p.value); byYear.get(p.year).push(p.value);
}} }}
const annual = [...byYear.values()].map(values => {{ return [...byYear.entries()].map(([year, values]) => {{
const sorted = values.filter(Number.isFinite).sort((a,b)=>a-b); const sorted = values.filter(Number.isFinite).sort((a,b)=>a-b);
return quantile(sorted, 0.50); return {{year:Number(year), value:quantile(sorted, 0.50)}};
}}).filter(Number.isFinite).sort((a,b)=>a-b); }}).filter(x => Number.isFinite(x.value)).sort((a,b)=>a.year-b.year);
}}
function trendLikeAnnualLevel(points) {{
const annual = annualMedianValues(points);
if (annual.length < 4) return false;
const xs = annual.map(x=>x.year);
const ys = annual.map(x=>x.value);
const mx = mean(xs), my = mean(ys);
const cov = xs.reduce((s,x,i)=>s+(x-mx)*(ys[i]-my),0);
const vx = xs.reduce((s,x)=>s+(x-mx)*(x-mx),0);
const vy = ys.reduce((s,y)=>s+(y-my)*(y-my),0);
const corr = vx > 0 && vy > 0 ? cov / Math.sqrt(vx * vy) : 0;
return Math.abs(corr) >= 0.70;
}}
function robustAnnualSeasonValues(points, useRobust=true) {{
const annual = annualMedianValues(points).map(x=>x.value).sort((a,b)=>a-b);
if (annual.length < 4) return annual; if (annual.length < 4) return annual;
if (!useRobust) return annual;
const center = quantile(annual, 0.50); const center = quantile(annual, 0.50);
const keepCount = Math.max(3, Math.ceil(annual.length * 0.65)); const keepCount = Math.max(3, Math.ceil(annual.length * 0.65));
return annual return annual
@@ -777,6 +793,8 @@ function buildNormalizedChart(years) {{
}}); }});
const rawValues = rawPoints.map(p=>p.value); const rawValues = rawPoints.map(p=>p.value);
if (!rawValues.length) return {{years:{{}}, band:[]}}; if (!rawValues.length) return {{years:{{}}, band:[]}};
const bandUniverse = rawPoints.filter(p => Number(p.year) >= params.startYear && Number(p.year) < params.excludeYear);
const trendLike = trendLikeAnnualLevel(bandUniverse);
const m = mean(rawValues); const m = mean(rawValues);
let s = std(rawValues); let s = std(rawValues);
if (!Number.isFinite(s) || s === 0) s = 1; if (!Number.isFinite(s) || s === 0) s = 1;
@@ -788,7 +806,7 @@ function buildNormalizedChart(years) {{
}}); }});
const band = []; const band = [];
for (let day=1; day<=366; day++) {{ for (let day=1; day<=366; day++) {{
const samples = robustAnnualSeasonValues(rawPoints.filter(p => Number(p.year) >= params.startYear && Number(p.year) < params.excludeYear && circularDiff(p.day, day) <= params.window)); const samples = robustAnnualSeasonValues(bandUniverse.filter(p => circularDiff(p.day, day) <= params.window), !trendLike);
if (samples.length < 3) continue; if (samples.length < 3) continue;
const tail = Math.max(0.02, Math.min(0.45, params.trim || 0.10)); const tail = Math.max(0.02, Math.min(0.45, params.trim || 0.10));
const mid = quantile(samples, 0.50); const mid = quantile(samples, 0.50);
File diff suppressed because one or more lines are too long