刷新数据 2026-09-15 11:30
This commit is contained in:
+49
-10
@@ -111,6 +111,13 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
||||
<div class="chart" id="spend-chart"><div class="tip" hidden></div></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>每日安装</h2>
|
||||
<p class="desc">按包堆叠,悬停查看当天每个包的数据;与上方消耗图共用时间范围</p>
|
||||
<div class="legend" id="installs-legend"></div>
|
||||
<div class="chart" id="installs-chart"><div class="tip" hidden></div></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>余额走势</h2>
|
||||
<p class="desc">每天日终余额 = 截至当天的累计到账 − 累计消耗,圆点是到账日</p>
|
||||
@@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user