diff --git a/front/src/pods/embalse/components/chart/chart.helpers.tsx b/front/src/pods/embalse/components/chart/chart.helpers.tsx index 21e48d3..263b527 100644 --- a/front/src/pods/embalse/components/chart/chart.helpers.tsx +++ b/front/src/pods/embalse/components/chart/chart.helpers.tsx @@ -1,26 +1,78 @@ import React from "react"; import { sizeChart as s } from "./chart.constants"; -interface barRoundedTopProps { +interface BarRoundedTopProps { x: number; y: number; width: number; height: number; + bottomY: number; fill: string; + delay?: string; } -export const BarRoundedTop: React.FC = ({ + +export const BarRoundedTop: React.FC = ({ x, y, width, height, fill, + bottomY = y, + delay = "0ms", }): React.ReactNode => { return ( - - {/* Barra según porcentaje con esquinas redondeadas */} - - {/* Barra inferior sin redondeo para aplanar la base */} - + // Agregamos opacidad inicial a 0 al grupo para el fade-in + + + + {/* Barra principal (redondeada) */} + {/* Empieza en bottomY con altura 0 */} + + + + + + {/* Barra inferior (cuadrada) para tapar el radio inferior */} + {/* También empieza en bottomY con altura 0 y crece sincrónicamente */} + + + + ); }; @@ -31,14 +83,45 @@ export const ReferenceLine: React.FC<{ x2: number; stroke: string; dashArray: string; -}> = ({ yPos, x1, x2, stroke, dashArray }) => ( - + bottomY?: number; + delay?: string; +}> = ({ yPos, x1, x2, stroke, dashArray, bottomY = yPos, delay = "0ms" }) => ( + // Quitamos el opacity={0} del para no bloquear la animación de los hijos + + + + + + + ); diff --git a/front/src/pods/embalse/components/chart/history-chart.tsx b/front/src/pods/embalse/components/chart/history-chart.tsx index 639ef93..7b4b03c 100644 --- a/front/src/pods/embalse/components/chart/history-chart.tsx +++ b/front/src/pods/embalse/components/chart/history-chart.tsx @@ -1,5 +1,3 @@ -"use client"; -import { useState, useEffect, useRef } from "react"; import * as d3 from "d3"; import { ChartModel } from "./chart.vm"; import { sizeChart as s } from "./chart.constants"; @@ -12,55 +10,15 @@ export const HistoryChart: React.FC = ({ dataOneYearAgo, dataTenYearsAgo, }) => { - const [animationKey, setAnimationKey] = useState(0); - const [animProgress, setAnimProgress] = useState(0); - const [labelVisible, setLabelVisible] = useState(false); - const rafRef = useRef(null); - - const startAnimation = () => { - if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); - setAnimProgress(0); - setLabelVisible(false); - setAnimationKey((k) => k + 1); - }; - - useEffect(() => { - startAnimation(); - const mq = window.matchMedia("(min-width: 768px)"); - const handler = (e: MediaQueryListEvent) => { - if (e.matches) startAnimation(); - }; - mq.addEventListener("change", handler); - return () => mq.removeEventListener("change", handler); - }, []); - - useEffect(() => { - if (animationKey === 0) return; - const duration = 1200; - const start = performance.now(); - const tick = (now: number) => { - const t = Math.min((now - start) / duration, 1); - const eased = 1 - Math.pow(1 - t, 3); - setAnimProgress(eased); - if (t < 1) { - rafRef.current = requestAnimationFrame(tick); - } else { - setLabelVisible(true); - } - }; - rafRef.current = requestAnimationFrame(tick); - return () => { - if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); - }; - }, [animationKey]); - + // 1. Aseguramos límites lógicos (0% - 100%) let percentageActual = (reservoirData.currentVolume * 100) / reservoirData.totalCapacity; - if (percentageActual > 100) { - percentageActual = 100; - } + if (percentageActual > 100) percentageActual = 100; + if (percentageActual < 0) percentageActual = 0; + const isOutside = percentageActual < 10; - // Cálculo de escalas + + // 2. Cálculo de escalas const x = d3 .scaleBand() .domain([reservoirData.nombre]) @@ -72,97 +30,125 @@ export const HistoryChart: React.FC = ({ .domain([0, 105]) .range([s.height - s.margin.bottom, s.margin.top]); - const barX = x(reservoirData.nombre); + const barX = x(reservoirData.nombre) || 0; const barWidth = x.bandwidth(); + + // 3. Cálculos de altura base (protegidos con Math.max para evitar negativos) const barY = y(percentageActual); - const barHeight = y(0) - barY; + const barHeight = Math.max(0, y(0) - barY); + const bgBarHeight = Math.max(0, y(0) - y(100)); // Altura total para el 100% // Extremos compartidos por las líneas de referencia const refX1 = barX - s.margin.left / 2; const refX2 = barX * 2 + s.margin.left + s.margin.right; - // Etiqueta: encima de la barra si el nivel es muy bajo (<10%), dentro si no + // Etiqueta const labelY = isOutside ? barY - 8 : barY + 20; - // Animación de la barra oscura (nivel actual) que crece de abajo hacia arriba. - // Si la animación no ha arrancado aún (móvil antes de tocar), mostrar estado final estático. - const progress = animationKey === 0 ? 1 : animProgress; - const animBarHeight = progress * barHeight; - const animBarY = y(0) - animBarHeight; - const showLabel = animationKey === 0 ? true : labelVisible; - return (
{ - if (!window.matchMedia("(min-width: 768px)").matches) { - startAnimation(); - } - }} >

{titleChart}

- {/* Indicador de capacidad total (100%) - fijo, ocupa todo el alto */} + {/* Indicador de capacidad total (100%) Animado */} + > + {/* Anima la posición Y desde la base hacia arriba */} + + {/* Anima la altura desde 0 hasta la altura total */} + + - {/* Nivel actual - animado creciendo de abajo hacia arriba */} + {/* Nivel actual */} - {/* Línea de referencia: mismo mes del año anterior */} + {/* Línea de referencia: año anterior */} {dataOneYearAgo && ( )} - {/* Línea de referencia: mismo mes hace 10 años */} + {/* Línea de referencia: hace 10 años */} {dataTenYearsAgo && ( )} - {/* Etiqueta con el nivel actual en Hm³ */} - {showLabel && ( - - {reservoirData.currentVolume} Hm³ - - )} + + {/* Etiqueta con el nivel actual (Le añadimos fade-in para que no salga antes que la barra) */} + + {reservoirData.currentVolume} Hm³ + + {/* Eje X */}