Refine archive cumulative PnL chart proportions and axis layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-10 12:14:17 +08:00
parent 8714bb6fd3
commit e4893a6306
3 changed files with 121 additions and 80 deletions
+69 -58
View File
@@ -661,15 +661,21 @@
if (!series.length) {
return '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
}
const w = 400;
const h = 110;
const padL = 36;
const padR = 8;
const padT = 8;
const padB = 22;
const w = 320;
const h = 96;
const padL = 6;
const padR = 6;
const padT = 10;
const padB = 10;
const vals = series.map(function (s) { return s.cum; });
const minV = Math.min(0, ...vals);
const maxV = Math.max(0, ...vals);
const rawMin = Math.min.apply(null, vals);
const rawMax = Math.max.apply(null, vals);
let minV = Math.min(0, rawMin);
let maxV = Math.max(0, rawMax);
const span = maxV - minV || Math.max(Math.abs(rawMax), Math.abs(rawMin), 1);
const yPad = span * 0.14;
minV -= yPad;
maxV += yPad;
const range = maxV - minV || 1;
const innerW = w - padL - padR;
const innerH = h - padT - padB;
@@ -677,78 +683,83 @@
return padT + innerH - ((v - minV) / range) * innerH;
};
const zeroY = yOf(0);
const showZero = rawMin < -0.0001 || rawMax > 0.0001;
const pts = series.map(function (s, i) {
const x = padL + (i / Math.max(series.length - 1, 1)) * innerW;
const y = yOf(s.cum);
return { x: x, y: y };
return { x: x, y: y, day: s.day, pnl: s.pnl, cum: s.cum };
});
const linePts = pts.map(function (p) { return p.x.toFixed(1) + "," + p.y.toFixed(1); }).join(" ");
const areaPts =
linePts +
" " +
pts[pts.length - 1].x.toFixed(1) +
"," +
zeroY.toFixed(1) +
" " +
pts[0].x.toFixed(1) +
"," +
zeroY.toFixed(1);
const last = series[series.length - 1];
const lastCls = last.cum >= 0 ? "pnl-pos" : "pnl-neg";
const lineCls = last.cum >= 0 ? "archive-cum-line--up" : "archive-cum-line--down";
const sign = last.cum > 0 ? "+" : "";
const fmtAxis = function (v) {
const a = Math.abs(v);
if (a >= 100) return (v > 0 ? "+" : "") + v.toFixed(0);
if (a >= 10) return (v > 0 ? "+" : "") + v.toFixed(1);
return (v > 0 ? "+" : "") + v.toFixed(2);
if (a >= 100) return (v > 0 ? "+" : "") + v.toFixed(0) + "U";
if (a >= 10) return (v > 0 ? "+" : "") + v.toFixed(1) + "U";
return (v > 0 ? "+" : "") + v.toFixed(2) + "U";
};
const yTicks = [maxV, 0, minV].filter(function (v, i, arr) {
return arr.indexOf(v) === i;
});
const yLabels = yTicks
const yTop = fmtAxis(maxV);
const yMid = showZero ? "0" : fmtAxis((maxV + minV) / 2);
const yBot = fmtAxis(minV);
const gridLines = [maxV, showZero ? 0 : null, minV]
.filter(function (v, i, arr) {
return v != null && arr.indexOf(v) === i;
})
.map(function (v) {
const y = yOf(v).toFixed(1);
const cls = v === 0 ? "archive-cum-grid archive-cum-grid--zero" : "archive-cum-grid";
return '<line x1="' + padL + '" y1="' + y + '" x2="' + (w - padR) + '" y2="' + y + '" class="' + cls + '" />';
})
.join("");
const dots = pts
.map(function (p, i) {
const isLast = i === pts.length - 1;
const dotCls = p.cum >= 0 ? "archive-cum-dot--up" : "archive-cum-dot--down";
const r = isLast ? 4.2 : 2.6;
const dayPnl = p.pnl > 0 ? "+" + p.pnl.toFixed(2) : p.pnl.toFixed(2);
const cumPnl = p.cum > 0 ? "+" + p.cum.toFixed(2) : p.cum.toFixed(2);
return (
'<text x="' +
(padL - 4) +
'" y="' +
(yOf(v) + 3.5).toFixed(1) +
'" class="archive-cum-ylabel" text-anchor="end">' +
esc(fmtAxis(v)) +
"</text>"
'<circle cx="' + p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="' + r + '" class="archive-cum-dot ' + dotCls + (isLast ? " archive-cum-dot--last" : "") + '">' +
'<title>' + esc(p.day.slice(5)) + " 当日" + dayPnl + "U · 累计" + cumPnl + "U</title>" +
"</circle>"
);
})
.join("");
const xLabels =
'<text x="' +
padL +
'" y="' +
(h - 4) +
'" class="archive-cum-xlabel" text-anchor="start">' +
esc(series[0].day.slice(5)) +
"</text>" +
(series.length > 1
? '<text x="' +
(padL + innerW) +
'" y="' +
(h - 4) +
'" class="archive-cum-xlabel" text-anchor="end">' +
esc(series[series.length - 1].day.slice(5)) +
"</text>"
: "");
const dayCount = series.length;
const peak = fmtAxis(rawMax);
const trough = fmtAxis(rawMin);
const xStart = esc(series[0].day.slice(5));
const xEnd = esc(series[series.length - 1].day.slice(5));
return (
'<div class="archive-cum-wrap">' +
'<div class="archive-cum-head">' +
'<span class="archive-viz-block-title">累计盈亏</span>' +
'<span class="archive-cum-end ' + lastCls + '">' + sign + last.cum.toFixed(2) + "U</span>" +
"</div>" +
'<svg class="archive-cum-chart" viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" aria-hidden="true">' +
yLabels +
xLabels +
'<line x1="' + padL + '" y1="' + zeroY.toFixed(1) + '" x2="' + (w - padR) + '" y2="' + zeroY.toFixed(1) + '" class="archive-cum-zero" />' +
'<polygon points="' + areaPts + '" class="archive-cum-area ' + lineCls + '" />' +
'<polyline points="' + linePts + '" class="archive-cum-line ' + lineCls + '" />' +
'<circle cx="' + pts[pts.length - 1].x.toFixed(1) + '" cy="' + pts[pts.length - 1].y.toFixed(1) + '" r="3" class="archive-cum-dot ' + lineCls + '" />' +
"</svg>"
'<div class="archive-cum-body">' +
'<div class="archive-cum-yaxis" aria-hidden="true">' +
'<span>' + esc(yTop) + "</span>" +
'<span>' + esc(yMid) + "</span>" +
'<span>' + esc(yBot) + "</span>" +
"</div>" +
'<div class="archive-cum-plot">' +
'<svg class="archive-cum-chart" viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="xMidYMid meet" aria-hidden="true">' +
gridLines +
'<polyline points="' + linePts + '" class="archive-cum-line ' + lineCls + '" vector-effect="non-scaling-stroke" />' +
dots +
"</svg>" +
'<div class="archive-cum-xaxis" aria-hidden="true">' +
"<span>" + xStart + "</span>" +
(series.length > 1 ? "<span>" + xEnd + "</span>" : "") +
"</div>" +
"</div>" +
"</div>" +
'<div class="archive-cum-foot muted">' +
dayCount + " 个交易日 · 区间高 " + esc(peak) + " · 低 " + esc(trough) +
"</div>" +
"</div>"
);
}