Files
BTC-Portfolio/btc-portfolio/frontend/src/components/BTCHistoryChart.js
T
Jonathan 541b5f57d2 Add 1-year BTC daily price history chart
New GET /history endpoint fetches 365 days of BTC/EUR data from
CoinGecko, deduplicates by date, and joins the user's purchases.
BTCHistoryChart component renders the price line with orange dot
markers on purchase dates and a dashed cyan avg buy price line.
Tooltip shows purchase details on marked dates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:18:40 +01:00

122 lines
3.6 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' },
loading: { color: '#666', padding: '1rem 0' },
saveBtn: { marginTop: '0.75rem', background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.4rem 1rem', cursor: 'pointer', fontSize: '0.85rem' },
};
export default function BTCHistoryChart({ history, stats }) {
const chartRef = useRef(null);
if (!history || history.prices.length === 0) {
return (
<div style={styles.card}>
<div style={styles.title}>1-Year BTC Price (EUR)</div>
<div style={styles.loading}>Loading price history</div>
</div>
);
}
const { prices, purchases } = history;
const averagePrice = stats?.average_price ?? 0;
const purchaseDateSet = new Set(purchases.map(p => p.date));
const pointRadii = prices.map(p => purchaseDateSet.has(p.date) ? 7 : 0);
const pointColors = prices.map(p => purchaseDateSet.has(p.date) ? '#f7931a' : 'transparent');
const pointBorderColors = prices.map(p => purchaseDateSet.has(p.date) ? '#fff' : 'transparent');
const data = {
labels: prices.map(p => p.date),
datasets: [
{
label: 'BTC Price (€)',
data: prices.map(p => p.price),
borderColor: '#f7931a',
backgroundColor: 'rgba(247,147,26,0.08)',
fill: true,
tension: 0.2,
pointRadius: pointRadii,
pointBackgroundColor: pointColors,
pointBorderColor: pointBorderColors,
pointBorderWidth: 2,
pointHoverRadius: 6,
},
...(averagePrice > 0 ? [{
label: `Avg Buy Price (€${averagePrice.toLocaleString()})`,
data: prices.map(() => averagePrice),
borderColor: '#4fc3f7',
backgroundColor: 'transparent',
borderDash: [8, 4],
pointRadius: 0,
pointHoverRadius: 0,
tension: 0,
}] : []),
],
};
const options = {
responsive: true,
plugins: {
legend: { labels: { color: '#aaa' } },
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
afterBody: (items) => {
const date = items[0]?.label;
const purchase = purchases.find(p => p.date === date);
if (purchase) {
return [`Purchased: €${purchase.amount_eur.toLocaleString()} @ €${purchase.price_eur.toLocaleString()}`];
}
return [];
},
},
},
},
scales: {
x: {
ticks: { color: '#666', maxTicksLimit: 12, maxRotation: 0 },
grid: { color: '#2a2a2a' },
},
y: {
ticks: { color: '#666' },
grid: { color: '#2a2a2a' },
},
},
};
const handleSave = () => {
const chart = chartRef.current;
if (!chart) return;
const a = document.createElement('a');
a.href = chart.toBase64Image();
a.download = 'btc-history.png';
a.click();
};
return (
<div style={styles.card}>
<div style={styles.title}>1-Year BTC Price (EUR)</div>
<Line ref={chartRef} data={data} options={options} />
<br />
<button style={styles.saveBtn} onClick={handleSave}>Save as PNG</button>
</div>
);
}