Files
AtreidesAssetsFree/client/wealth/return-chart.js
T

143 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// 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;
}