fix(invest): mobile chart — fixed height, sane scales, horizontal x labels, compact legend/points, Chinese help

Root cause of the squashed chart: canvas had a fixed height=90 attr and no container, so on narrow viewports Chart.js compressed everything — legend overlapped the plot, x labels tilted 45deg, y ticks degenerated (-1000% scale). Fix: relative h-72/h-80 container + maintainAspectRatio:false, grace 5%, maxTicksLimit, maxRotation:0, smaller points (r2/1.5), compact legend/tooltip, Chinese help text.
This commit is contained in:
OpenClaw Sub-agent
2026-08-02 08:28:08 +10:00
parent 24435c58df
commit 509723c197
2 changed files with 31 additions and 11 deletions
+2 -2
View File
@@ -746,8 +746,8 @@ def _line_dataset(label: str, data: list, color: str, dashed: bool = False, widt
'borderColor': color, 'borderColor': color,
'backgroundColor': color, 'backgroundColor': color,
'borderWidth': width, 'borderWidth': width,
'pointRadius': 4 if not dashed else 3, 'pointRadius': 2 if not dashed else 1.5,
'pointHoverRadius': 7 if not dashed else 5, 'pointHoverRadius': 4 if not dashed else 3,
'tension': 0.3, 'tension': 0.3,
'borderDash': [5, 5] if dashed else [], 'borderDash': [5, 5] if dashed else [],
'fill': False, 'fill': False,
+29 -9
View File
@@ -125,7 +125,7 @@
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between mb-4"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between mb-4">
<div> <div>
<p class="text-xs font-semibold tracking-widest text-stone-500 uppercase">{{ fy_label }} Performance vs Benchmarks</p> <p class="text-xs font-semibold tracking-widest text-stone-500 uppercase">{{ fy_label }} Performance vs Benchmarks</p>
<p id="performanceChartHelp" class="text-xs text-stone-400 mt-1">Toggle between percentage return and dollar-value growth from the baseline date.</p> <p id="performanceChartHelp" class="text-xs text-stone-400 mt-1">切换涨跌幅百分比与金额两种视图。</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<div class="inline-flex rounded-lg border border-stone-200 bg-stone-50 p-1 text-xs font-semibold" role="group" aria-label="Chart period"> <div class="inline-flex rounded-lg border border-stone-200 bg-stone-50 p-1 text-xs font-semibold" role="group" aria-label="Chart period">
@@ -142,7 +142,9 @@
</div> </div>
</div> </div>
</div> </div>
<canvas id="performanceChart" height="90"></canvas> <div class="relative h-72 md:h-80">
<canvas id="performanceChart"></canvas>
</div>
</div> </div>
{% endif %} {% endif %}
@@ -363,14 +365,32 @@
const unit = currentData().unit; const unit = currentData().unit;
return { return {
responsive: true, responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, interaction: { mode: 'index', intersect: false },
plugins: { plugins: {
legend: { position: 'top', align: 'start', labels: { usePointStyle: true, pointStyle: 'rect', pointStyleWidth: 14, padding: 20, font: { size: 12, weight: '600' } } }, legend: {
tooltip: { callbacks: { label: function (ctx) { const v = ctx.parsed.y; if (v === null || v === undefined) return ctx.dataset.label + ': —'; return ctx.dataset.label + ': ' + (unit === 'currency' ? formatCurrency(v) : formatPercent(v)); } } }, position: 'top',
align: 'start',
labels: { usePointStyle: true, pointStyle: 'rect', pointStyleWidth: 10, padding: 10, boxWidth: 10, font: { size: 11, weight: '600' } },
},
tooltip: {
position: 'nearest',
padding: 8,
titleFont: { size: 11 },
bodyFont: { size: 10 },
callbacks: { label: function (ctx) { const v = ctx.parsed.y; if (v === null || v === undefined) return ctx.dataset.label + ': —'; return ctx.dataset.label + ': ' + (unit === 'currency' ? formatCurrency(v) : formatPercent(v)); } },
},
}, },
scales: { scales: {
y: { ticks: { callback: function (v) { return unit === 'currency' ? formatCurrency(Number(v)) : formatPercent(Number(v)); }, font: { size: 11 } }, grid: { color: '#f5f5f4' } }, y: {
x: { grid: { display: false }, ticks: { font: { size: 11 } } }, grace: '5%',
ticks: { maxTicksLimit: 6, callback: function (v) { return unit === 'currency' ? formatCurrency(Number(v)) : formatPercent(Number(v)); }, font: { size: 10 } },
grid: { color: '#f5f5f4' },
},
x: {
grid: { display: false },
ticks: { autoSkip: true, maxRotation: 0, maxTicksLimit: 8, font: { size: 10 } },
},
}, },
}; };
} }
@@ -386,10 +406,10 @@
chart.options = chartOptions(); chart.options = chartOptions();
chart.update(); chart.update();
if (help) { if (help) {
const baseline = currentData().baseline || 'the baseline date'; const baseline = currentData().baseline || '基准日';
help.textContent = currentMode === 'value' help.textContent = currentMode === 'value'
? `Dollar mode: actual total portfolio value vs SPY/QQQ benchmark value from ${baseline}.` ? `金额视图:组合总市值 vs SPY/QQQ 基准(自 ${baseline}`
: `Percentage mode: growth/decline since ${baseline}; cash-flow-adjusted metrics are shown in the cards above.`; : `涨跌幅视图:自 ${baseline} 以来的涨跌幅;卡片上方为现金流调整指标`;
} }
} }