Datavision / frontend /src /components /simulator /ForecastTab.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
6.96 kB
import React, { useState, useEffect } from 'react';
import { useUserStore } from '@/store/userStore';
import apiService from '@/services/api';
import SimulatorChart from './SimulatorChart';
import { Download, Maximize2, Calendar } from 'lucide-react';
const ForecastTab: React.FC = () => {
const { isDark } = useUserStore();
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [periods, setPeriods] = useState(12);
const [interval, setInterval] = useState('monthly');
const [variables, setVariables] = useState<Record<string, any>>({});
const [modelInfo, setModelInfo] = useState<any>(null);
const cardBg = isDark ? 'rgba(255,255,255,0.03)' : '#ffffff';
const cardBorder = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';
const textPrimary = isDark ? '#f8fafc' : '#0f172a';
const textMuted = isDark ? '#94a3b8' : '#64748b';
useEffect(() => {
const fetchVars = async () => {
try {
const [varRes, ovRes] = await Promise.all([
apiService.getSimulatorVariables(),
apiService.getSimulatorOverview(),
]);
if (ovRes.data?.model_info) setModelInfo(ovRes.data.model_info);
if (varRes.data) {
const init: Record<string, any> = {};
varRes.data.forEach((v: any) => { init[v.name] = v.current_value; });
setVariables(init);
}
} catch {}
};
fetchVars();
}, []);
useEffect(() => {
if (Object.keys(variables).length === 0) return;
const fetchForecast = async () => {
setLoading(true);
try {
const res = await apiService.getSimulatorForecast(variables, periods, interval);
setData(res.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchForecast();
}, [variables, periods, interval]);
const targetUnit = modelInfo?.target_unit || '';
const targetName = modelInfo?.target_name || modelInfo?.target_column?.replace(/_/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase()) || 'Target';
const formatValue = (val: any) => {
if (typeof val !== 'number' || isNaN(val)) return '0';
if (targetUnit === '%') return `${val.toFixed(1)}%`;
if (targetUnit === '₹') {
if (val >= 10000000) return `₹${(val / 10000000).toFixed(2)} Cr`;
if (val >= 100000) return `₹${(val / 100000).toFixed(1)} L`;
return `₹${val.toLocaleString()}`;
}
return val >= 1000000 ? `${(val / 1000000).toFixed(1)}M` : val.toLocaleString();
};
return (
<div className="space-y-5">
{/* Controls bar */}
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" style={{ color: textMuted }} />
{['3', '6', '12', '24'].map(p => (
<button
key={p}
onClick={() => setPeriods(Number(p))}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
periods === Number(p) ? 'bg-indigo-500/10 text-indigo-500 border border-indigo-500/30' : 'border'
}`}
style={periods !== Number(p) ? { borderColor: cardBorder, color: textMuted } : undefined}
>
{p}M
</button>
))}
<select
value={interval}
onChange={(e) => setInterval(e.target.value)}
className="px-3 py-1.5 rounded-lg text-xs border cursor-pointer outline-none"
style={{ background: cardBg, borderColor: cardBorder, color: textMuted }}
>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="quarterly">Quarterly</option>
</select>
</div>
<div className="flex items-center gap-2">
<button className="p-2 rounded-xl border hover:bg-white/5" style={{ borderColor: cardBorder }}>
<Download className="w-4 h-4" style={{ color: textMuted }} />
</button>
<button className="p-2 rounded-xl border hover:bg-white/5" style={{ borderColor: cardBorder }}>
<Maximize2 className="w-4 h-4" style={{ color: textMuted }} />
</button>
</div>
</div>
{/* Main Chart */}
<div className="rounded-2xl border p-6" style={{ background: cardBg, borderColor: cardBorder }}>
<h3 className="text-sm font-bold mb-4" style={{ color: textPrimary }}>
Prediction Timeline ({targetName})
</h3>
{loading ? (
<div className="flex items-center justify-center h-80">
<div className="w-8 h-8 border-3 border-indigo-500 border-t-transparent rounded-full animate-spin" />
</div>
) : data?.chart_data ? (
<SimulatorChart
data={data.chart_data}
height={380}
lines={[
{ dataKey: 'simulated', color: '#6366f1', name: 'Scenario', type: 'monotone' },
{ dataKey: 'baseline', color: isDark ? '#475569' : '#94a3b8', name: 'Baseline', strokeDasharray: '6 3' },
{ dataKey: 'confidence_upper', color: '#a5b4fc', name: 'Confidence Upper', strokeDasharray: '2 2', dot: false },
{ dataKey: 'confidence_lower', color: '#a5b4fc', name: 'Confidence Lower', strokeDasharray: '2 2', dot: false },
{ dataKey: 'best_case', color: '#22c55e', name: 'Best Case', strokeDasharray: '4 4', dot: false },
{ dataKey: 'worst_case', color: '#ef4444', name: 'Worst Case', strokeDasharray: '4 4', dot: false },
]}
formatY={(v) => formatValue(v)}
/>
) : (
<div className="flex items-center justify-center h-80">
<p className="text-sm" style={{ color: textMuted }}>No forecast data available</p>
</div>
)}
</div>
{/* Summary Cards */}
{data?.summary && (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: `Expected ${targetName}`, value: data.summary.expected, color: '#6366f1' },
{ label: '95% Range', value: data.summary.confidence_range, color: '#818cf8' },
{ label: 'Best Case', value: data.summary.best_case, color: '#22c55e' },
{ label: 'Worst Case', value: data.summary.worst_case, color: '#ef4444' },
{ label: 'Baseline', value: data.summary.baseline, color: isDark ? '#475569' : '#94a3b8' },
].map((item, i) => (
<div key={i} className="rounded-xl border p-4" style={{ background: cardBg, borderColor: cardBorder }}>
<p className="text-[10px] mb-1" style={{ color: textMuted }}>{item.label}</p>
<p className="text-sm font-bold" style={{ color: item.color }}>{item.value}</p>
</div>
))}
</div>
)}
</div>
);
};
export default ForecastTab;