diff --git a/index.html b/index.html index 52a2619..acff50b 100644 --- a/index.html +++ b/index.html @@ -111,6 +111,13 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
按包堆叠,悬停查看当天每个包的数据;与上方消耗图共用时间范围
+ +每天日终余额 = 截至当天的累计到账 − 累计消耗,圆点是到账日
@@ -321,20 +328,20 @@ function renderTiles(S, d) { let RANGE = '7'; -function drawSpend(S, d) { - const wrap = $('#spend-chart'); +function drawStacked(S, d, opts) { + const wrap = $(opts.wrapSel); wrap.querySelector('svg')?.remove(); hideTip(wrap); const start = RANGE === 'all' ? d.range.start : addDays(S.end, -(Number(RANGE) - 1)); const dates = dayRange(start < d.range.start ? d.range.start : start, S.end); const stacks = dates.map(dt => { const acc = S.series.map(() => 0); - S.rowsOn(dt).forEach(r => { acc[S.seriesOf[r.pkg]] += r.spend; }); + S.rowsOn(dt).forEach(r => { acc[S.seriesOf[r.pkg]] += opts.valueOf(r); }); return acc.map(r2); }); const totals = stacks.map(s => r2(s.reduce((a, b) => a + b, 0))); - $('#spend-legend').replaceChildren(...S.series + $(opts.legendSel).replaceChildren(...S.series .filter((s, i) => stacks.some(st => st[i] > 0)) .map(s => { const e = h('span'); @@ -349,12 +356,12 @@ function drawSpend(S, d) { const y = v => m.t + ph - (v - sc.lo) / (sc.hi - sc.lo) * ph; const n = dates.length, band = pw / n, bw = Math.max(2, Math.min(24, band * 0.6)); const clampX = x => Math.min(Math.max(x, m.l + 18), W - m.r - 18); - const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: W, height: H, role: 'img', 'aria-label': '每日消耗,按包堆叠' }); + const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: W, height: H, role: 'img', 'aria-label': opts.ariaLabel }); wrap.prepend(svg); const hover = el('rect', { class: 'hover-band', y: m.t, height: ph, width: band, visibility: 'hidden' }, svg); sc.ticks.forEach(t => { el('line', { x1: m.l, x2: W - m.r, y1: y(t), y2: y(t), class: t === 0 ? 'axis' : 'grid' }, svg); - el('text', { x: m.l - 8, y: y(t) + 4, 'text-anchor': 'end', class: 'tick' }, svg).textContent = usd(t, sc.dp); + el('text', { x: m.l - 8, y: y(t) + 4, 'text-anchor': 'end', class: 'tick' }, svg).textContent = opts.fmt(t, sc.dp); }); // 表格数据和 MIDAS 数据的分界线 @@ -384,7 +391,7 @@ function drawSpend(S, d) { el('text', { x: clampX(cx), y: H - m.b + 18, 'text-anchor': 'middle', class: 'tick' }, svg).textContent = shortDate(dt); } if (totals[i] > 0 && (i === lastI || (i === maxI && lastI - maxI >= 3 * step))) { - el('text', { x: clampX(cx), y: y(totals[i]) - 7, 'text-anchor': 'middle', class: 'val' }, svg).textContent = usd(totals[i]); + el('text', { x: clampX(cx), y: y(totals[i]) - 7, 'text-anchor': 'middle', class: 'val' }, svg).textContent = opts.fmt(totals[i]); } }); dates.forEach((dt, i) => { @@ -393,12 +400,26 @@ function drawSpend(S, d) { hit.addEventListener('pointerenter', () => { hover.setAttribute('x', cx - band / 2); hover.setAttribute('visibility', 'visible'); - showTip(wrap, cx, y(totals[i]), spendTip(S, dt, totals[i])); + showTip(wrap, cx, y(totals[i]), opts.tip(S, dt, totals[i])); }); hit.addEventListener('pointerleave', () => { hover.setAttribute('visibility', 'hidden'); hideTip(wrap); }); }); } +function drawSpend(S, d) { + drawStacked(S, d, { + wrapSel: '#spend-chart', legendSel: '#spend-legend', valueOf: r => r.spend, + fmt: usd, ariaLabel: '每日消耗,按包堆叠', tip: spendTip, + }); +} + +function drawInstalls(S, d) { + drawStacked(S, d, { + wrapSel: '#installs-chart', legendSel: '#installs-legend', valueOf: r => r.installs, + fmt: v => int(v), ariaLabel: '每日安装,按包堆叠', tip: installsTip, + }); +} + function spendTip(S, dt, total) { const f = document.createDocumentFragment(); f.append(h('div', 'tip-title', dt)); @@ -418,6 +439,24 @@ function spendTip(S, dt, total) { return f; } +function installsTip(S, dt, total) { + const f = document.createDocumentFragment(); + f.append(h('div', 'tip-title', dt)); + const rows = S.rowsOn(dt).filter(r => r.installs > 0).sort((a, b) => b.installs - a.installs); + if (!rows.length) f.append(h('div', 'tip-sub', '当天没有安装')); + rows.forEach(r => { + const row = h('div', 'tip-row'); + row.append(swatch(S.colorOf(r.pkg)), h('span', 'tip-name', r.pkg), h('span', 'tip-val', int(r.installs))); + f.append(row); + }); + if (rows.length > 1) { + const tot = h('div', 'tip-row tip-total'); + tot.append(h('span', 'tip-name', '合计'), h('span', 'tip-val', int(total))); + f.append(tot); + } + return f; +} + function drawBalance(S) { const wrap = $('#balance-chart'); wrap.querySelector('svg')?.remove(); @@ -519,14 +558,14 @@ function renderDaily(S, d) { } let S, DATA; -const drawCharts = () => { if (S) { drawSpend(S, DATA); drawBalance(S); } }; +const drawCharts = () => { if (S) { drawSpend(S, DATA); drawInstalls(S, DATA); drawBalance(S); } }; $('#range').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; RANGE = b.dataset.range; [...$('#range').children].forEach(x => x.setAttribute('aria-pressed', String(x === b))); - if (S) drawSpend(S, DATA); + if (S) { drawSpend(S, DATA); drawInstalls(S, DATA); } }); fetch('data.json', { cache: 'no-store' })