刷新数据 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>
|
<div class="chart" id="spend-chart"><div class="tip" hidden></div></div>
|
||||||
</section>
|
</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">
|
<section class="card">
|
||||||
<h2>余额走势</h2>
|
<h2>余额走势</h2>
|
||||||
<p class="desc">每天日终余额 = 截至当天的累计到账 − 累计消耗,圆点是到账日</p>
|
<p class="desc">每天日终余额 = 截至当天的累计到账 − 累计消耗,圆点是到账日</p>
|
||||||
@@ -321,20 +328,20 @@ function renderTiles(S, d) {
|
|||||||
|
|
||||||
let RANGE = '7';
|
let RANGE = '7';
|
||||||
|
|
||||||
function drawSpend(S, d) {
|
function drawStacked(S, d, opts) {
|
||||||
const wrap = $('#spend-chart');
|
const wrap = $(opts.wrapSel);
|
||||||
wrap.querySelector('svg')?.remove();
|
wrap.querySelector('svg')?.remove();
|
||||||
hideTip(wrap);
|
hideTip(wrap);
|
||||||
const start = RANGE === 'all' ? d.range.start : addDays(S.end, -(Number(RANGE) - 1));
|
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 dates = dayRange(start < d.range.start ? d.range.start : start, S.end);
|
||||||
const stacks = dates.map(dt => {
|
const stacks = dates.map(dt => {
|
||||||
const acc = S.series.map(() => 0);
|
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);
|
return acc.map(r2);
|
||||||
});
|
});
|
||||||
const totals = stacks.map(s => r2(s.reduce((a, b) => a + b, 0)));
|
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))
|
.filter((s, i) => stacks.some(st => st[i] > 0))
|
||||||
.map(s => {
|
.map(s => {
|
||||||
const e = h('span');
|
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 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 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 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);
|
wrap.prepend(svg);
|
||||||
const hover = el('rect', { class: 'hover-band', y: m.t, height: ph, width: band, visibility: 'hidden' }, svg);
|
const hover = el('rect', { class: 'hover-band', y: m.t, height: ph, width: band, visibility: 'hidden' }, svg);
|
||||||
sc.ticks.forEach(t => {
|
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('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 数据的分界线
|
// 表格数据和 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);
|
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))) {
|
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) => {
|
dates.forEach((dt, i) => {
|
||||||
@@ -393,12 +400,26 @@ function drawSpend(S, d) {
|
|||||||
hit.addEventListener('pointerenter', () => {
|
hit.addEventListener('pointerenter', () => {
|
||||||
hover.setAttribute('x', cx - band / 2);
|
hover.setAttribute('x', cx - band / 2);
|
||||||
hover.setAttribute('visibility', 'visible');
|
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); });
|
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) {
|
function spendTip(S, dt, total) {
|
||||||
const f = document.createDocumentFragment();
|
const f = document.createDocumentFragment();
|
||||||
f.append(h('div', 'tip-title', dt));
|
f.append(h('div', 'tip-title', dt));
|
||||||
@@ -418,6 +439,24 @@ function spendTip(S, dt, total) {
|
|||||||
return f;
|
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) {
|
function drawBalance(S) {
|
||||||
const wrap = $('#balance-chart');
|
const wrap = $('#balance-chart');
|
||||||
wrap.querySelector('svg')?.remove();
|
wrap.querySelector('svg')?.remove();
|
||||||
@@ -519,14 +558,14 @@ function renderDaily(S, d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let S, DATA;
|
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 => {
|
$('#range').addEventListener('click', e => {
|
||||||
const b = e.target.closest('button');
|
const b = e.target.closest('button');
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
RANGE = b.dataset.range;
|
RANGE = b.dataset.range;
|
||||||
[...$('#range').children].forEach(x => x.setAttribute('aria-pressed', String(x === b)));
|
[...$('#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' })
|
fetch('data.json', { cache: 'no-store' })
|
||||||
|
|||||||
Reference in New Issue
Block a user