Files
BTC-Portfolio/btc-portfolio/frontend/src/components/PortfolioChart.js
T
Jonathan 5bb67d6663 Add purchase date picker and sells feature
- Purchase form now includes a date picker (defaults to today)
- New Sell model, CRUD endpoints (/sells), and stats integration
- AddSell and SellList components added to dashboard
- Portfolio chart updated to reflect sells over time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:52:24 +02:00

162 lines
4.7 KiB
JavaScript

import React, { useRef } from 'react';
import {
Chart as ChartJS,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Tooltip,
Legend,
Filler,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, Filler);
const styles = {
card: { background: '#1a1a1a', padding: '1.5rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem' },
title: { fontSize: '1.1rem', fontWeight: 700, marginBottom: '1rem', color: '#f7931a' },
saveBtn: { marginTop: '0.75rem', background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.4rem 1rem', cursor: 'pointer', fontSize: '0.85rem' },
};
function toDateKey(date) {
return date.toISOString().split('T')[0];
}
function priceOn(date, priceMap, currentPrice, isToday, sortedPurchases) {
if (isToday) return currentPrice || 0;
// Try candle history (walk back up to 7 days)
for (let i = 0; i <= 7; i++) {
const d = new Date(date);
d.setDate(d.getDate() - i);
const p = priceMap[toDateKey(d)];
if (p) return p;
}
// Fall back to most recent purchase price up to this date
let fallback = null;
for (const p of sortedPurchases) {
const pd = new Date(p.created_at);
pd.setHours(0, 0, 0, 0);
if (pd <= date) fallback = p.price_eur;
}
return fallback;
}
export default function PortfolioChart({ purchases, sells, stats, btcHistory }) {
const chartRef = useRef(null);
if (!purchases || purchases.length === 0) return null;
const sorted = [...purchases].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const sortedSells = [...(sells || [])].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const today = new Date();
today.setHours(0, 0, 0, 0);
// Build price lookup from candle history
const priceMap = {};
(btcHistory || []).forEach(({ date, close }) => { priceMap[date] = close; });
const firstDate = new Date(sorted[0].created_at);
firstDate.setHours(0, 0, 0, 0);
// Generate biweekly dates from first purchase to today
const dates = [];
const cursor = new Date(firstDate);
while (cursor <= today) {
dates.push(new Date(cursor));
cursor.setDate(cursor.getDate() + 7);
}
if (toDateKey(dates[dates.length - 1]) !== toDateKey(today)) {
dates.push(new Date(today));
}
const labels = [];
const portfolioValues = [];
const investedValues = [];
dates.forEach(date => {
const isToday = toDateKey(date) === toDateKey(today);
const price = priceOn(date, priceMap, stats?.current_price, isToday, sorted);
if (price === null) return; // no price data available, skip
// Cumulative BTC and invested up to this date
let cumBtc = 0;
let cumInvested = 0;
sorted.forEach(p => {
const pDate = new Date(p.created_at);
pDate.setHours(0, 0, 0, 0);
if (pDate <= date) {
cumBtc += p.amount_eur / p.price_eur;
cumInvested += p.amount_eur;
}
});
sortedSells.forEach(s => {
const sDate = new Date(s.created_at);
sDate.setHours(0, 0, 0, 0);
if (sDate <= date) {
cumBtc -= s.btc_amount;
cumInvested -= s.btc_amount * s.price_eur;
}
});
if (cumBtc === 0) return; // no purchases yet at this date
labels.push(date.toLocaleDateString('en-GB'));
portfolioValues.push(parseFloat((cumBtc * price).toFixed(2)));
investedValues.push(parseFloat(cumInvested.toFixed(2)));
});
const data = {
labels,
datasets: [
{
label: 'Portfolio Value (€)',
data: portfolioValues,
borderColor: '#f7931a',
backgroundColor: 'rgba(247,147,26,0.1)',
fill: true,
tension: 0.3,
},
{
label: 'Total Invested (€)',
data: investedValues,
borderColor: '#4fc3f7',
backgroundColor: 'transparent',
borderDash: [6, 3],
tension: 0.3,
},
],
};
const options = {
responsive: true,
plugins: {
legend: { labels: { color: '#aaa' } },
tooltip: { mode: 'index', intersect: false },
},
scales: {
x: { ticks: { color: '#666' }, grid: { color: '#2a2a2a' } },
y: { ticks: { color: '#666' }, grid: { color: '#2a2a2a' } },
},
};
const handleSave = () => {
const chart = chartRef.current;
if (!chart) return;
const url = chart.toBase64Image();
const a = document.createElement('a');
a.href = url;
a.download = 'btc-portfolio.png';
a.click();
};
return (
<div style={styles.card}>
<div style={styles.title}>Portfolio Chart</div>
<Line ref={chartRef} data={data} options={options} />
<br />
<button style={styles.saveBtn} onClick={handleSave}>Save as PNG</button>
</div>
);
}