Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions front/src/pods/embalse/components/chart/chart.helpers.tsx
Original file line number Diff line number Diff line change
@@ -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<barRoundedTopProps> = ({

export const BarRoundedTop: React.FC<BarRoundedTopProps> = ({
x,
y,
width,
height,
fill,
bottomY = y,
delay = "0ms",
}): React.ReactNode => {
return (
<g fill={fill}>
{/* Barra según porcentaje con esquinas redondeadas */}
<rect x={x} y={y} width={width} height={height} rx={s.radius} />
{/* Barra inferior sin redondeo para aplanar la base */}
<rect x={x} y={y + height / 2} width={width} height={height / 2} />
// Agregamos opacidad inicial a 0 al grupo para el fade-in
<g fill={fill} opacity="0">
<animate
attributeName="opacity"
from="0"
to="1"
dur="400ms"
begin={delay}
fill="freeze"
/>

{/* Barra principal (redondeada) */}
{/* Empieza en bottomY con altura 0 */}
<rect x={x} y={bottomY} width={width} height={0} rx={s.radius}>
<animate
attributeName="y"
from={bottomY}
to={y}
dur="800ms"
begin={delay}
fill="freeze"
/>
<animate
attributeName="height"
from="0"
to={height}
dur="800ms"
begin={delay}
fill="freeze"
/>
</rect>

{/* Barra inferior (cuadrada) para tapar el radio inferior */}
{/* También empieza en bottomY con altura 0 y crece sincrónicamente */}
<rect x={x} y={bottomY} width={width} height={0}>
<animate
attributeName="y"
from={bottomY}
to={y + height / 2}
dur="800ms"
begin={delay} // Mismo delay para que no se desfase
fill="freeze"
/>
<animate
attributeName="height"
from="0"
to={height / 2}
dur="800ms"
begin={delay} // Misma duración
fill="freeze"
/>
</rect>
</g>
);
};
Expand All @@ -31,14 +83,45 @@ export const ReferenceLine: React.FC<{
x2: number;
stroke: string;
dashArray: string;
}> = ({ yPos, x1, x2, stroke, dashArray }) => (
<line
y1={yPos}
y2={yPos}
x1={x1}
x2={x2}
stroke={stroke}
strokeWidth={5}
strokeDasharray={dashArray}
/>
bottomY?: number;
delay?: string;
}> = ({ yPos, x1, x2, stroke, dashArray, bottomY = yPos, delay = "0ms" }) => (
// Quitamos el opacity={0} del <g> para no bloquear la animación de los hijos
<g>
<line
y1={bottomY}
y2={bottomY}
x1={x1}
x2={x2}
stroke={stroke}
strokeWidth={5}
strokeDasharray={dashArray}
opacity="0" // La opacidad inicial se la damos directamente a la línea
>
<animate
attributeName="y1"
from={bottomY}
to={yPos}
dur="800ms"
begin={delay}
fill="freeze"
/>
<animate
attributeName="y2"
from={bottomY}
to={yPos}
dur="800ms"
begin={delay}
fill="freeze"
/>
<animate
attributeName="opacity"
from="0"
to="1"
dur="600ms"
begin={delay}
fill="freeze"
/>
</line>
</g>
);
158 changes: 72 additions & 86 deletions front/src/pods/embalse/components/chart/history-chart.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,55 +10,15 @@ export const HistoryChart: React.FC<ChartModel> = ({
dataOneYearAgo,
dataTenYearsAgo,
}) => {
const [animationKey, setAnimationKey] = useState(0);
const [animProgress, setAnimProgress] = useState(0);
const [labelVisible, setLabelVisible] = useState(false);
const rafRef = useRef<number | null>(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])
Expand All @@ -72,97 +30,125 @@ export const HistoryChart: React.FC<ChartModel> = ({
.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 (
<section
className="card bg-base-100 mx-auto w-full items-center rounded-2xl md:gap-4 md:p-4 md:shadow-lg"
aria-labelledby="gauge-title"
onClick={() => {
if (!window.matchMedia("(min-width: 768px)").matches) {
startAnimation();
}
}}
>
<h2 id="gauge-title" className="text-center">
{titleChart}
</h2>

<svg width={s.width} height={s.height}>
{/* Indicador de capacidad total (100%) - fijo, ocupa todo el alto */}
{/* Indicador de capacidad total (100%) Animado */}
<rect
x={barX}
y={y(100)}
width={barWidth}
height={y(0) - y(100)}
height={bgBarHeight}
rx={s.radius}
fill="var(--color-total-water)"
/>
>
{/* Anima la posición Y desde la base hacia arriba */}
<animate
attributeName="y"
from={y(0)}
to={y(100)}
dur="800ms"
begin="0ms"
fill="freeze"
/>
{/* Anima la altura desde 0 hasta la altura total */}
<animate
attributeName="height"
from="0"
to={bgBarHeight}
dur="800ms"
begin="0ms"
fill="freeze"
/>
</rect>

{/* Nivel actual - animado creciendo de abajo hacia arriba */}
{/* Nivel actual */}
<BarRoundedTop
x={barX}
y={animBarY}
y={barY}
width={barWidth}
height={animBarHeight}
height={barHeight}
bottomY={y(0)}
delay="300ms"
fill="var(--color-primary)"
/>

{/* Línea de referencia: mismo mes del año anterior */}
{/* Línea de referencia: año anterior */}
{dataOneYearAgo && (
<ReferenceLine
yPos={y(
(dataOneYearAgo.average * 100) / reservoirData.totalCapacity,
Math.max(
0,
(dataOneYearAgo.average * 100) / reservoirData.totalCapacity,
),
)}
x1={refX1}
x2={refX2}
stroke={"var(--line-average-last-year)"}
dashArray="12"
delay="600ms"
/>
)}

{/* Línea de referencia: mismo mes hace 10 años */}
{/* Línea de referencia: hace 10 años */}
{dataTenYearsAgo && (
<ReferenceLine
yPos={y(
(dataTenYearsAgo.average * 100) / reservoirData.totalCapacity,
Math.max(
0,
(dataTenYearsAgo.average * 100) / reservoirData.totalCapacity,
),
)}
x1={refX1}
x2={refX2}
stroke={"var(--line-average-last-ten-years)"}
dashArray="4"
delay="800ms"
/>
)}
{/* Etiqueta con el nivel actual en Hm³ */}
{showLabel && (
<text
x={barX + barWidth / 2}
y={labelY}
textAnchor="middle"
fontSize="16px"
fill="var(--color-brand-100)"
fontWeight="900"
>
{reservoirData.currentVolume} Hm³
</text>
)}

{/* Etiqueta con el nivel actual (Le añadimos fade-in para que no salga antes que la barra) */}
<text
x={barX + barWidth / 2}
y={labelY}
textAnchor="middle"
fontSize="16px"
fill="var(--color-brand-100)"
fontWeight="900"
opacity="0"
>
{reservoirData.currentVolume} Hm³
<animate
attributeName="opacity"
from="0"
to="1"
dur="400ms"
begin="800ms"
fill="freeze"
/>
</text>

{/* Eje X */}
<line
Expand Down
Loading