FinMK / frontend /src /components /ForecastChart.jsx
Kumar
Refactor: Exclude PDF and CSV files from Git to fix HF push error
24e6f5b
import { useState, useEffect } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { TrendingUp, Info } from 'lucide-react';
import api from '../api/axios';
import { useSettings } from '../context/SettingsContext';
const ForecastChart = () => {
const { currencySymbol } = useSettings();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchForecast = async () => {
try {
const response = await api.get('analytics/forecast/');
if (Array.isArray(response.data)) {
setData(response.data);
} else {
throw new Error("Invalid format");
}
setLoading(false);
} catch (err) {
console.error("Forecast Error:", err);
const errMsg = err.response?.data?.error || "AI Engine Initializing...";
setError(errMsg);
setLoading(false);
}
};
fetchForecast();
}, []);
if (loading) return (
<div className="glass-panel" style={{ height: '300px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div className="spinner"></div>
</div>
);
if (error || !data.length) return (
<div className="glass-panel" style={{ height: '300px', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: 'var(--text-muted)' }}>
<Info size={32} style={{ marginBottom: '1rem', opacity: 0.5 }} />
<p>{error || "No forecast data available yet."}</p>
</div>
);
// Calculate total predicted spend safely
const totalPredicted = Array.isArray(data) ? data.reduce((acc, curr) => acc + (curr.amount || 0), 0) : 0;
return (
<div className="glass-panel" style={{
padding: '1.5rem',
position: 'relative',
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.3)',
boxShadow: '0 0 20px rgba(99, 102, 241, 0.1)'
}}>
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, height: '2px',
background: 'linear-gradient(90deg, transparent, #6366f1, transparent)'
}} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}>
<div>
<h3 style={{ margin: 0, fontSize: '1.1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '1.2rem' }}>🔮</span> Smart Forecast (Chronos Bolt)
</h3>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', margin: '0.25rem 0 0 0' }}>
AI-predicted spending for next 30 days
</p>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Projected Total</div>
<div style={{ fontSize: '1.4rem', fontWeight: 'bold', color: '#818cf8', display: 'flex', alignItems: 'center', gap: '0.5rem', justifyContent: 'flex-end' }}>
{currencySymbol}{totalPredicted.toLocaleString(undefined, { maximumFractionDigits: 0 })}
<TrendingUp size={16} />
</div>
</div>
</div>
<div style={{ height: '400px', width: '100%' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<defs>
<linearGradient id="colorHigh" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.1} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<XAxis
dataKey="date"
stroke="var(--text-muted)"
fontSize={10}
tickFormatter={(str) => {
const d = new Date(str);
return `${d.getDate()}/${d.getMonth() + 1}`;
}}
interval={4}
/>
<YAxis stroke="var(--text-muted)" fontSize={10} tickFormatter={(val) => `${currencySymbol}${val}`} />
<Tooltip
contentStyle={{
backgroundColor: 'rgba(15, 23, 42, 0.9)',
border: '1px solid rgba(99, 102, 241, 0.3)',
borderRadius: '0.5rem',
boxShadow: '0 4px 20px rgba(0,0,0,0.5)'
}}
itemStyle={{ color: '#e2e8f0' }}
formatter={(value, name) => {
if (name === 'high') return [`${currencySymbol}${value}`, 'Upper Bound (P90)'];
if (name === 'amount') return [`${currencySymbol}${value}`, 'Wait Forecast (P50)'];
if (name === 'low') return [`${currencySymbol}${value}`, 'Lower Bound (P10)'];
return [value, name];
}}
labelStyle={{ color: '#818cf8', fontWeight: 'bold', marginBottom: '0.5rem' }}
/>
{/* Confidence Interval Area */}
{/* We stack to create the band? No, simple Area is easier. */}
{/* Actually, representing Low/High as area requires specific processing or multiple areas.
Simplest visual: Area for Amount, and Lines for Low/High,
OR a stacked approach: Low (invisible), (High-Low) as range.
Let's try a transparent range.
*/}
<Area
type="monotone"
dataKey="high"
stroke="none"
fill="#6366f1"
fillOpacity={0.1}
/>
{/* We need to mask the bottom part if we want a band, but standard Area is 0-to-val.
Recharts Area 'baseValue' isn't dynamic.
Better visual: Just show the main line and a faint area for 'high' to give an impression of ceiling.
Or use `Area` with `dataKey="amount"` as the main visual.
*/}
<Area
type="monotone"
dataKey="amount"
stroke="#818cf8"
strokeWidth={3}
fill="url(#colorHigh)"
activeDot={{ r: 6, strokeWidth: 0 }}
/>
<Area
type="monotone"
dataKey="low"
stroke="#4f46e5"
strokeDasharray="3 3"
fill="none"
strokeOpacity={0.5}
/>
<Area
type="monotone"
dataKey="high"
stroke="#4f46e5"
strokeDasharray="3 3"
fill="none"
strokeOpacity={0.5}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem', fontSize: '0.75rem', color: 'var(--text-muted)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#818cf8' }} />
Predicted
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<div style={{ width: '8px', height: '2px', background: '#4f46e5', borderTop: '1px dashed transparent' }} />
Confidence Range
</div>
</div>
</div>
);
};
export default ForecastChart;