Add chart start year and quantile band controls
This commit is contained in:
@@ -233,7 +233,7 @@ def seasonal_percentile(series: pd.Series) -> dict[str, Any] | None:
|
||||
raw_pct = 100.0 * (less + 0.5 * equal) / len(sample)
|
||||
latest_year = int(latest_date.year)
|
||||
yearly: dict[str, list[tuple[str, float]]] = {}
|
||||
clipped = series[series.index.year >= latest_year - 8]
|
||||
clipped = series[series.index.year >= 2021]
|
||||
for dt, val in clipped.items():
|
||||
yearly.setdefault(str(int(dt.year)), []).append((dt.strftime("%m-%d"), float(val)))
|
||||
return {
|
||||
@@ -493,6 +493,7 @@ svg {{ width:100%; height:230px; display:block; }}
|
||||
<label class="ctrl">窗口 <input class="mini" id="bandWindow" type="number" min="3" max="90" step="1" value="15"></label>
|
||||
<label class="ctrl">倍数 <input class="mini" id="bandK" type="number" min="0.5" max="6" step="0.5" value="3"></label>
|
||||
<label class="ctrl">去尾 <input class="mini" id="bandTrim" type="number" min="0" max="40" step="5" value="10"></label>
|
||||
<label class="ctrl">图始 <select id="chartStartYear"></select></label>
|
||||
<label class="ctrl"><input id="includeDemand" type="checkbox" checked>需求计分</label>
|
||||
<button class="tabbtn active" id="dashboardTab">面板</button>
|
||||
<button class="tabbtn" id="historyTab">评分走势</button>
|
||||
@@ -547,6 +548,7 @@ const boardSel = document.getElementById("board");
|
||||
const bandWindow = document.getElementById("bandWindow");
|
||||
const bandK = document.getElementById("bandK");
|
||||
const bandTrim = document.getElementById("bandTrim");
|
||||
const chartStartYear = document.getElementById("chartStartYear");
|
||||
const includeDemand = document.getElementById("includeDemand");
|
||||
const dashboardTab = document.getElementById("dashboardTab");
|
||||
const historyTab = document.getElementById("historyTab");
|
||||
@@ -560,6 +562,9 @@ const historyChartWrap = document.getElementById("historyChartWrap");
|
||||
const refreshData = document.getElementById("refreshData");
|
||||
for (const d of dims) dimSel.append(new Option(d, d));
|
||||
for (const b of DATA.boards) boardSel.append(new Option(b, b));
|
||||
const maxChartYear = new Date(DATA.generated_at.slice(0,10)).getFullYear();
|
||||
for (let y=2018; y<=maxChartYear; y++) chartStartYear.append(new Option(String(y), String(y)));
|
||||
chartStartYear.value = chartStartYear.querySelector('option[value="2021"]') ? "2021" : chartStartYear.options[0]?.value;
|
||||
document.getElementById("generated").textContent = DATA.generated_at;
|
||||
document.getElementById("completion").textContent = `${{DATA.completion.count}}/${{DATA.completion.total}}(${{DATA.completion.ratio.toFixed(1)}}%)`;
|
||||
function todayString() {{
|
||||
@@ -582,11 +587,11 @@ function majorityHistoryEndDate() {{
|
||||
}}
|
||||
historyEnd.value = majorityHistoryEndDate();
|
||||
let activeView = "dashboard";
|
||||
document.getElementById("reset").onclick = () => {{ search.value=""; dimSel.value="全部"; boardSel.value="全部"; bandWindow.value="15"; bandK.value="3"; bandTrim.value="10"; includeDemand.checked=true; historyStart.value="2026-01-01"; historyEnd.value = majorityHistoryEndDate(); render(); }};
|
||||
document.getElementById("reset").onclick = () => {{ search.value=""; dimSel.value="全部"; boardSel.value="全部"; bandWindow.value="15"; bandK.value="3"; bandTrim.value="10"; chartStartYear.value="2021"; includeDemand.checked=true; historyStart.value="2026-01-01"; historyEnd.value = majorityHistoryEndDate(); render(); }};
|
||||
dashboardTab.onclick = () => {{ activeView="dashboard"; dashboardTab.classList.add("active"); historyTab.classList.remove("active"); render(); }};
|
||||
historyTab.onclick = () => {{ activeView="history"; historyTab.classList.add("active"); dashboardTab.classList.remove("active"); render(); }};
|
||||
refreshData.onclick = refreshFromLocalProgram;
|
||||
search.oninput = dimSel.onchange = boardSel.onchange = bandWindow.oninput = bandK.oninput = bandTrim.oninput = includeDemand.onchange = historyStart.onchange = historyEnd.onchange = render;
|
||||
search.oninput = dimSel.onchange = boardSel.onchange = bandWindow.oninput = bandK.oninput = bandTrim.oninput = chartStartYear.onchange = includeDemand.onchange = historyStart.onchange = historyEnd.onchange = render;
|
||||
|
||||
function fmt(v, n=1) {{ return v == null || Number.isNaN(v) ? "--" : Number(v).toFixed(n); }}
|
||||
function matches(c) {{
|
||||
@@ -679,6 +684,8 @@ function chartParams() {{
|
||||
window: Math.max(3, Math.min(90, Number(bandWindow.value) || 15)),
|
||||
k: Math.max(0.5, Math.min(6, Number(bandK.value) || 3)),
|
||||
trim: Math.max(0, Math.min(40, Number(bandTrim.value) || 0)) / 100,
|
||||
startYear: Number(chartStartYear.value) || 2021,
|
||||
excludeYear: maxChartYear,
|
||||
}};
|
||||
}}
|
||||
function mean(arr) {{ return arr.reduce((s,v)=>s+v,0) / arr.length; }}
|
||||
@@ -698,6 +705,14 @@ function trimmed(arr, trim) {{
|
||||
if (sorted.length - cut * 2 < 4) return sorted;
|
||||
return sorted.slice(cut, sorted.length - cut);
|
||||
}}
|
||||
function quantile(sorted, q) {{
|
||||
if (!sorted.length) return NaN;
|
||||
const pos = (sorted.length - 1) * Math.max(0, Math.min(1, q));
|
||||
const lo = Math.floor(pos);
|
||||
const hi = Math.ceil(pos);
|
||||
if (lo === hi) return sorted[lo];
|
||||
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
|
||||
}}
|
||||
function smoothBand(band, radius) {{
|
||||
if (!band.length || radius <= 0) return band;
|
||||
const n = band.length;
|
||||
@@ -726,6 +741,7 @@ function buildNormalizedChart(years) {{
|
||||
const params = chartParams();
|
||||
const rawPoints = [];
|
||||
Object.entries(years).forEach(([year, pts]) => {{
|
||||
if (Number(year) < params.startYear) return;
|
||||
pts.forEach(p => {{
|
||||
const value = Number(p[1]);
|
||||
if (Number.isFinite(value)) rawPoints.push({{year, md:p[0], day:parseDay(p[0]), value}});
|
||||
@@ -739,17 +755,23 @@ function buildNormalizedChart(years) {{
|
||||
const z = v => (v - m) / s;
|
||||
const normYears = {{}};
|
||||
Object.entries(years).forEach(([year, pts]) => {{
|
||||
if (Number(year) < params.startYear) return;
|
||||
normYears[year] = pts.map(p => [p[0], z(Number(p[1]))]).filter(p => Number.isFinite(p[1]));
|
||||
}});
|
||||
const band = [];
|
||||
for (let day=1; day<=366; day++) {{
|
||||
const samples = trimmed(rawPoints.filter(p => circularDiff(p.day, day) <= params.window).map(p=>p.value), params.trim);
|
||||
const samples = rawPoints.filter(p => Number(p.year) >= params.startYear && Number(p.year) < params.excludeYear && circularDiff(p.day, day) <= params.window).map(p=>p.value).filter(Number.isFinite).sort((a,b)=>a-b);
|
||||
if (samples.length < 4) continue;
|
||||
const bm = mean(samples);
|
||||
const bs = std(samples);
|
||||
const tail = Math.max(0.02, Math.min(0.45, params.trim || 0.10));
|
||||
const mid = quantile(samples, 0.50);
|
||||
let low = quantile(samples, tail);
|
||||
let high = quantile(samples, 1 - tail);
|
||||
const widthScale = Math.max(0.25, Math.min(2.5, params.k / 3));
|
||||
low = mid - (mid - low) * widthScale;
|
||||
high = mid + (high - mid) * widthScale;
|
||||
const date = new Date(2024, 0, day);
|
||||
const md = `${{String(date.getMonth()+1).padStart(2,"0")}}-${{String(date.getDate()).padStart(2,"0")}}`;
|
||||
band.push([md, z(bm - params.k * bs), z(bm + params.k * bs), z(bm)]);
|
||||
band.push([md, z(low), z(high), z(mid)]);
|
||||
}}
|
||||
const smoothRadius = Math.max(3, Math.min(18, Math.round(params.window * 0.75)));
|
||||
return {{years:normYears, band:smoothBand(band, smoothRadius)}};
|
||||
|
||||
Reference in New Issue
Block a user