// Generated, do not edit. sha256:80acd8f6f37bf4f64acd4a1259ffb1f609bdfe3d99f67a807eaad2154af68875 import { loadCharting, readPalette } from '../ui/chart-setup.js'; import { hexToRgb } from '../ui/charting.js'; import { t } from '../i18n/index.js'; import { fmtBaseSigned, fmtPct, colouredSigns } from '../ui/format.js'; let tipEl = null; const hide = () => tipEl?.classList.remove('is-visible'); const tone = v => (v > 0.005 ? 't-pos' : v < -0.005 ? 't-neg' : ''); function row(label, amount, pct, extra = '') { const r = document.createElement('span'); r.className = `ring-tip-row ${extra}`.trim(); const name = document.createElement('span'); name.className = 'ring-tip-pct'; name.textContent = label; const value = document.createElement('span'); value.className = `val ${tone(amount)}`; value.textContent = fmtBaseSigned(amount, colouredSigns()) + (pct == null ? '' : ` (${fmtPct(pct, { fine: true, signs: colouredSigns() })})`); r.append(name, value); return r; } function tip({ chart, tooltip }, points, labels) { if (!tipEl || !tipEl.isConnected) { tipEl = document.createElement('div'); tipEl.className = 'ring-tip'; tipEl.setAttribute('role', 'tooltip'); document.body.appendChild(tipEl); window.addEventListener('scroll', hide, { capture: true, passive: true }); } if (!tooltip.opacity || !tooltip.dataPoints?.length) { hide(); return; } const n = tooltip.dataPoints[0].dataIndex; const p = points[n]; const head = document.createElement('span'); head.className = 'ring-tip-label'; head.textContent = labels[n]; const rows = [head]; if (p.day != null) rows.push(row(t('wealth.returnDay'), p.day, p.pct)); for (const c of p.top || []) rows.push(row(c.label, c.day, c.pct, 'ring-tip-row--sub')); rows.push(row(t('wealth.returnSoFar'), p.cumulative, null)); if (p.carried) { const note = document.createElement('span'); note.className = 'ring-tip-row ring-tip-row--sub t-muted'; note.textContent = t('wealth.returnCarried'); rows.push(note); } tipEl.replaceChildren(...rows); const box = chart.canvas.getBoundingClientRect(); tipEl.style.left = `${Math.round(box.left + tooltip.caretX)}px`; tipEl.style.top = `${Math.round(box.top + tooltip.caretY)}px`; tipEl.classList.add('is-visible'); } const rgba = (hex, a) => { const c = hexToRgb(hex); return c ? `rgba(${c.r},${c.g},${c.b},${a})` : null; }; function gradientFill(green, red, pal) { return { id: 'return-gradient-fill', afterLayout(chart) { const { top, bottom } = chart.chartArea || {}; const zero = chart.scales?.y?.getPixelForValue(0); if (![top, bottom, zero].every(Number.isFinite)) return; const upTop = rgba(green, 0.42), upZero = rgba(green, 0.04); const downZero = rgba(red, 0.04), downBottom = rgba(red, 0.42); if (!upTop || !downBottom) return; const { ctx } = chart; const up = ctx.createLinearGradient(0, top, 0, Math.max(zero, top + 1)); up.addColorStop(0, upTop); up.addColorStop(1, upZero); const down = ctx.createLinearGradient(0, Math.min(zero, bottom - 1), 0, bottom); down.addColorStop(0, downZero); down.addColorStop(1, downBottom); chart.data.datasets[0].fill = { target: 'origin', above: up, below: down }; }, }; } function hoverLine(pal) { return { id: 'return-hover-line', afterDatasetsDraw(chart) { const active = chart.tooltip?.getActiveElements?.() || []; if (!active.length) return; const x = active[0].element.x; const { top, bottom } = chart.chartArea; const { ctx } = chart; ctx.save(); ctx.strokeStyle = pal.textSec; ctx.globalAlpha = 0.6; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(Math.round(x) + 0.5, top); ctx.lineTo(Math.round(x) + 0.5, bottom); ctx.stroke(); ctx.restore(); }, }; } export async function drawReturnChart(canvas, points, labels) { const Chart = await loadCharting(); const pal = readPalette(); const green = pal.sigGreen, red = pal.sigRed; const chart = new Chart(canvas, { type: 'line', plugins: [hoverLine(pal), gradientFill(green, red, pal)], data: { labels, datasets: [{ data: points.map(p => p.cumulative), borderColor: green, borderWidth: 2, pointRadius: 0, pointHoverRadius: 3, segment: { borderColor: c => (c.p1.parsed.y < 0 ? red : green), borderDash: c => (points[c.p0DataIndex]?.carried || points[c.p1DataIndex]?.carried ? [4, 4] : undefined), }, pointHoverBackgroundColor: c => ((c.parsed?.y ?? 0) < 0 ? red : green), cubicInterpolationMode: 'monotone', fill: { target: 'origin', above: pal.greenZone, below: pal.redZone }, }], }, options: { responsive: true, maintainAspectRatio: false, animation: false, interaction: { mode: 'index', intersect: false }, layout: { padding: { top: 6, right: 0, bottom: 6, left: 0 } }, scales: { x: { display: false }, y: { beginAtZero: true, ticks: { display: false }, border: { display: false }, afterFit: s => { s.width = 0; }, grid: { color: c => (c.tick?.value === 0 ? pal.textSec : 'transparent') } }, }, plugins: { legend: { display: false }, tooltip: { enabled: false, external: ctx => tip(ctx, points, labels) }, }, }, }); canvas.addEventListener('mouseleave', hide); const destroy = chart.destroy.bind(chart); chart.destroy = () => { hide(); destroy(); }; return chart; }