Imporve the predicciones page

This commit is contained in:
Urtzi Alfaro
2025-09-20 22:11:05 +02:00
parent abe7cf2444
commit 38d314e28d
14 changed files with 1659 additions and 364 deletions

View File

@@ -88,9 +88,15 @@ const DemandChart: React.FC<DemandChartProps> = ({
// Process forecast data for chart
const chartData = useMemo(() => {
console.log('🔍 Processing forecast data for chart:', data);
const processedData: ChartDataPoint[] = data.map(forecast => {
// Convert forecast_date to a proper date format for the chart
const forecastDate = new Date(forecast.forecast_date);
const dateString = forecastDate.toISOString().split('T')[0];
return {
date: forecast.forecast_date,
date: dateString,
actualDemand: undefined, // Not available in current forecast response
predictedDemand: forecast.predicted_demand,
confidenceLower: forecast.confidence_lower,
@@ -99,23 +105,32 @@ const DemandChart: React.FC<DemandChartProps> = ({
};
});
console.log('📊 Processed chart data:', processedData);
return processedData.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
}, [data]);
// Filter data based on selected period
const filteredData = useMemo(() => {
console.log('🔍 Filtering data - selected period:', selectedPeriod);
console.log('🔍 Chart data before filtering:', chartData);
if (!selectedPeriod.start || !selectedPeriod.end) {
console.log('📊 No period filter, returning all chart data');
return chartData;
}
return chartData.filter(point => {
const filtered = chartData.filter(point => {
const pointDate = new Date(point.date);
return pointDate >= selectedPeriod.start! && pointDate <= selectedPeriod.end!;
});
console.log('📊 Filtered data:', filtered);
return filtered;
}, [chartData, selectedPeriod]);
// Update zoomed data when filtered data changes
useEffect(() => {
console.log('🔍 Setting zoomed data from filtered data:', filteredData);
setZoomedData(filteredData);
}, [filteredData]);
@@ -221,8 +236,11 @@ const DemandChart: React.FC<DemandChartProps> = ({
);
}
// Empty state
if (zoomedData.length === 0) {
// Use filteredData if zoomedData is empty but we have data
const displayData = zoomedData.length > 0 ? zoomedData : filteredData;
// Empty state - only show if we truly have no data
if (displayData.length === 0 && chartData.length === 0) {
return (
<Card className={className}>
<CardHeader>
@@ -286,7 +304,7 @@ const DemandChart: React.FC<DemandChartProps> = ({
)}
{/* Reset zoom */}
{zoomedData.length !== filteredData.length && (
{displayData.length !== filteredData.length && (
<Button variant="ghost" size="sm" onClick={handleResetZoom}>
Restablecer Zoom
</Button>
@@ -298,28 +316,57 @@ const DemandChart: React.FC<DemandChartProps> = ({
<CardBody padding="lg">
<div style={{ width: '100%', height }}>
<ResponsiveContainer>
<ComposedChart data={zoomedData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis
dataKey="date"
<ComposedChart data={displayData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}>
<defs>
<linearGradient id="demandGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0.05}/>
</linearGradient>
<linearGradient id="confidenceGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0.02}/>
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="2 2"
stroke="#e5e7eb"
strokeOpacity={0.5}
horizontal={true}
vertical={false}
/>
<XAxis
dataKey="date"
stroke="#6b7280"
fontSize={12}
fontSize={11}
tickMargin={8}
angle={-45}
textAnchor="end"
height={80}
interval={0}
tickFormatter={(value) => {
const date = new Date(value);
return timeframe === 'weekly'
? date.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' })
: timeframe === 'monthly'
? date.toLocaleDateString('es-ES', { month: 'short', year: '2-digit' })
: date.getFullYear().toString();
return date.toLocaleDateString('es-ES', {
month: 'short',
day: 'numeric',
weekday: displayData.length <= 7 ? 'short' : undefined
});
}}
/>
<YAxis
stroke="#6b7280"
fontSize={12}
tickFormatter={(value) => `${value}`}
<YAxis
stroke="#6b7280"
fontSize={11}
width={60}
tickFormatter={(value) => value.toFixed(0)}
domain={['dataMin - 5', 'dataMax + 5']}
/>
<Tooltip
content={<CustomTooltip />}
cursor={{ stroke: '#10b981', strokeWidth: 1, strokeOpacity: 0.5 }}
/>
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="line"
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
{/* Confidence interval area */}
{showConfidenceInterval && (
@@ -328,9 +375,9 @@ const DemandChart: React.FC<DemandChartProps> = ({
dataKey="confidenceUpper"
stackId={1}
stroke="none"
fill="#10b98120"
fillOpacity={0.3}
name="Intervalo de Confianza Superior"
fill="url(#confidenceGradient)"
fillOpacity={0.4}
name="Límite Superior"
/>
)}
{showConfidenceInterval && (
@@ -340,20 +387,40 @@ const DemandChart: React.FC<DemandChartProps> = ({
stackId={1}
stroke="none"
fill="#ffffff"
name="Intervalo de Confianza Inferior"
name="Límite Inferior"
/>
)}
{/* Background area for main prediction */}
<Area
type="monotone"
dataKey="predictedDemand"
stroke="none"
fill="url(#demandGradient)"
fillOpacity={0.2}
name="Área de Demanda"
/>
{/* Predicted demand line */}
<Line
type="monotone"
dataKey="predictedDemand"
stroke="#10b981"
strokeWidth={3}
dot={true}
dotSize={6}
activeDot={{ r: 8, stroke: '#10b981', strokeWidth: 2 }}
dot={{
fill: '#10b981',
strokeWidth: 2,
stroke: '#ffffff',
r: 4
}}
activeDot={{
r: 6,
stroke: '#10b981',
strokeWidth: 3,
fill: '#ffffff'
}}
name="Demanda Predicha"
connectNulls={false}
/>
</ComposedChart>
</ResponsiveContainer>
@@ -386,18 +453,46 @@ const DemandChart: React.FC<DemandChartProps> = ({
</div>
)}
{/* Chart legend */}
<div className="flex items-center justify-center gap-6 mt-4 text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-green-500"></div>
<span className="text-text-secondary">Demanda Predicha</span>
</div>
{showConfidenceInterval && (
{/* Enhanced Chart legend and insights */}
<div className="mt-6 space-y-4">
<div className="flex items-center justify-center gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-2 bg-green-500 bg-opacity-20"></div>
<span className="text-text-secondary">Intervalo de Confianza</span>
<div className="w-4 h-0.5 bg-green-500 rounded"></div>
<span className="text-text-secondary">Demanda Predicha</span>
</div>
)}
{showConfidenceInterval && (
<div className="flex items-center gap-2">
<div className="w-4 h-2 bg-green-500 bg-opacity-20 rounded"></div>
<span className="text-text-secondary">Intervalo de Confianza</span>
</div>
)}
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-text-secondary">Puntos de Datos</span>
</div>
</div>
{/* Quick stats */}
<div className="grid grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
<div className="text-center">
<div className="text-lg font-bold text-green-600">
{Math.min(...displayData.map(d => d.predictedDemand || 0)).toFixed(1)}
</div>
<div className="text-xs text-gray-500">Mínimo</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-blue-600">
{(displayData.reduce((sum, d) => sum + (d.predictedDemand || 0), 0) / displayData.length).toFixed(1)}
</div>
<div className="text-xs text-gray-500">Promedio</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-orange-600">
{Math.max(...displayData.map(d => d.predictedDemand || 0)).toFixed(1)}
</div>
<div className="text-xs text-gray-500">Máximo</div>
</div>
</div>
</div>
</CardBody>
</Card>