From 724c8e67aaf3da849b1991e703821bd5a05f86d9 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Thu, 12 Mar 2026 16:59:16 +0100 Subject: [PATCH 01/10] add the average monthly storage over the last 10 years for each reservoir --- front/src/app/embalse/[embalse]/page.tsx | 23 ++++++++++- .../api/embalse-historial.api-model.ts | 17 ++++++++ .../api/embalse-historial.api.ts | 39 +++++++++++++++++++ .../embalse-historial.component.tsx | 12 ++++++ .../embalse-historial.mapper.ts | 20 ++++++++++ .../embalse-historial.pod.tsx | 15 +++++++ .../embalse-historial.repository.ts | 23 +++++++++++ .../embalse-historial/embalse-historial.vm.ts | 15 +++++++ front/src/pods/embalse-historial/index.ts | 3 ++ 9 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 front/src/pods/embalse-historial/api/embalse-historial.api-model.ts create mode 100644 front/src/pods/embalse-historial/api/embalse-historial.api.ts create mode 100644 front/src/pods/embalse-historial/embalse-historial.component.tsx create mode 100644 front/src/pods/embalse-historial/embalse-historial.mapper.ts create mode 100644 front/src/pods/embalse-historial/embalse-historial.pod.tsx create mode 100644 front/src/pods/embalse-historial/embalse-historial.repository.ts create mode 100644 front/src/pods/embalse-historial/embalse-historial.vm.ts create mode 100644 front/src/pods/embalse-historial/index.ts diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index 361c200..ea129c9 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -6,6 +6,11 @@ import { getEmbalseBySlugCached, } from "@/pods/embalse"; import { mapEmbalseToReservoirData } from "@/pods/embalse/embalse.mapper"; +import { + EmbalseHistorialPod, + getPromedioHistoricoPorMeses, + ReservoirHistoryModel, +} from "@/pods/embalse-historial"; export const revalidate = 300; // ISR: regenerar cada 5 minutos @@ -34,6 +39,22 @@ export default async function EmbalseDetallePage({ params }: Props) { if (!embalseDoc) { notFound(); } + const reservoirData = mapEmbalseToReservoirData(embalseDoc, embalseInfo); - return ; + + /** + * Obtiene historial de agua embalsada del último año por meses según nombre de embalse recibido. + */ + + const reservoirHistoryLastYear: ReservoirHistoryModel = + await getPromedioHistoricoPorMeses(embalseDoc.nombre); + + return ( + <> + + + + ); } diff --git a/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts b/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts new file mode 100644 index 0000000..d4bd827 --- /dev/null +++ b/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts @@ -0,0 +1,17 @@ +interface MonthHistoryData { + mes: number; + promedio_agua_actual: number; +} + +interface ReservoirHistoryMetadata { + generatedAt: string; + periodoInicio: string; + periodoFin: string; +} + +export interface ReservoirHistoryModel { + _id: string; + embalse: string; + metadata: ReservoirHistoryMetadata; + meses: MonthHistoryData[]; +} diff --git a/front/src/pods/embalse-historial/api/embalse-historial.api.ts b/front/src/pods/embalse-historial/api/embalse-historial.api.ts new file mode 100644 index 0000000..4dd4e9b --- /dev/null +++ b/front/src/pods/embalse-historial/api/embalse-historial.api.ts @@ -0,0 +1,39 @@ +import "server-only"; +import { unstable_cache } from "next/cache"; +import { ReservoirHistoryModel } from "../embalse-historial.vm"; +import { getHistorialPromedioPorMeses } from "../embalse-historial.repository"; +import { mapEmbalseHistorialToLookup } from "../embalse-historial.mapper"; + +/** + * Cached version of getPromedioHistoricoPorMeses. + * Revalidates every 5 minutes. + */ +const getHistoricalAverageByMonthsCached = unstable_cache( + async (name: string): Promise => { + const statisticsReservoir = await getHistorialPromedioPorMeses(name); + + if (!statisticsReservoir) { + throw new Error("Empty historico embalse - skip cache"); + } + const reservoirStatisticsLastYear: ReservoirHistoryModel = + mapEmbalseHistorialToLookup(statisticsReservoir); + return reservoirStatisticsLastYear; + }, + ["embalses-historico-por-meses"], + { revalidate: 300 }, +); + +export const getPromedioHistoricoPorMeses = async ( + name: string, +): Promise => { + try { + return await getHistoricalAverageByMonthsCached(name); + } catch (error) { + console.warn( + "getHistoricalAverageByMonths: MongoDB not available or empty, returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + return; + } +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.component.tsx b/front/src/pods/embalse-historial/embalse-historial.component.tsx new file mode 100644 index 0000000..b6b4669 --- /dev/null +++ b/front/src/pods/embalse-historial/embalse-historial.component.tsx @@ -0,0 +1,12 @@ +"use client"; +import React from "react"; +import { ReservoirHistoryModel } from "./embalse-historial.vm"; + +interface Props { + statisticsLastYear: ReservoirHistoryModel; +} +export const EmbalseHistorialComponent: React.FC = (props) => { + const { statisticsLastYear } = props; + + return
EmbalseHistorialComponent
; +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.mapper.ts b/front/src/pods/embalse-historial/embalse-historial.mapper.ts new file mode 100644 index 0000000..f9e17ba --- /dev/null +++ b/front/src/pods/embalse-historial/embalse-historial.mapper.ts @@ -0,0 +1,20 @@ +import { ReservoirHistoryModel } from "./embalse-historial.vm"; +import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api/embalse-historial.api-model"; + +export const mapEmbalseHistorialToLookup = ( + apiData: ReservoirHistoryModelApi, +): ReservoirHistoryModel => { + return { + id: apiData._id, + metadata: { + lastUpdate: apiData.metadata.generatedAt, + startDate: apiData.metadata.periodoInicio, + endDate: apiData.metadata.periodoFin, + }, + reservoir: apiData.embalse, + months: apiData.meses.map((mes) => ({ + month: mes.mes, + average: mes.promedio_agua_actual, + })), + }; +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.pod.tsx b/front/src/pods/embalse-historial/embalse-historial.pod.tsx new file mode 100644 index 0000000..17e10e7 --- /dev/null +++ b/front/src/pods/embalse-historial/embalse-historial.pod.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { ReservoirHistoryModel } from "./embalse-historial.vm"; +import { EmbalseHistorialComponent } from "./embalse-historial.component"; + +interface Props { + reservoirHistoryLastYear: ReservoirHistoryModel; +} + +export const EmbalseHistorialPod: React.FC = ({ + reservoirHistoryLastYear, +}) => { + return ( + + ); +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.repository.ts b/front/src/pods/embalse-historial/embalse-historial.repository.ts new file mode 100644 index 0000000..c2eb70b --- /dev/null +++ b/front/src/pods/embalse-historial/embalse-historial.repository.ts @@ -0,0 +1,23 @@ +"use server"; + +import { getDb } from "@/lib/mongodb"; +import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api/embalse-historial.api-model"; + +export const getHistorialPromedioPorMeses = async ( + name: string, +): Promise => { + try { + const db = await getDb(); + const statisticsLastYear = await db + .collection("embalsesPromedioHistoricoPorMeses") + .findOne({ embalse: name }); + + return statisticsLastYear; + } catch (error) { + console.warn( + "getHistorialPromedioPorMeses: MongoDB not available (build time?), returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + } +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.vm.ts b/front/src/pods/embalse-historial/embalse-historial.vm.ts new file mode 100644 index 0000000..f8695cd --- /dev/null +++ b/front/src/pods/embalse-historial/embalse-historial.vm.ts @@ -0,0 +1,15 @@ +interface MonthHistoryData { + month: number; + average: number; +} +interface ReservoirHistoryMetadata { + lastUpdate: string; + startDate: string; + endDate: string; +} +export interface ReservoirHistoryModel { + id: string; + reservoir: string; + metadata: ReservoirHistoryMetadata; + months: MonthHistoryData[]; +} diff --git a/front/src/pods/embalse-historial/index.ts b/front/src/pods/embalse-historial/index.ts new file mode 100644 index 0000000..d4c6174 --- /dev/null +++ b/front/src/pods/embalse-historial/index.ts @@ -0,0 +1,3 @@ +export * from "./embalse-historial.pod"; +export * from "./api/embalse-historial.api"; +export * from "./embalse-historial.vm"; From 18c540802307eadb463ca75131c9150b1bcdb475 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Fri, 13 Mar 2026 13:42:41 +0100 Subject: [PATCH 02/10] feat: add initial version of reservoir history chart --- front/src/app/embalse/[embalse]/page.tsx | 3 +- .../embalse-historial/component/chart.tsx | 105 ++++++++++++++++++ .../embalse-historial.component.tsx | 14 ++- .../embalse-historial.pod.tsx | 7 +- 4 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 front/src/pods/embalse-historial/component/chart.tsx diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index ea129c9..3728edf 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -51,10 +51,11 @@ export default async function EmbalseDetallePage({ params }: Props) { return ( <> - + ); } diff --git a/front/src/pods/embalse-historial/component/chart.tsx b/front/src/pods/embalse-historial/component/chart.tsx new file mode 100644 index 0000000..c192546 --- /dev/null +++ b/front/src/pods/embalse-historial/component/chart.tsx @@ -0,0 +1,105 @@ +"use client"; +import * as d3 from "d3"; +import { useEffect, useRef } from "react"; +import { ReservoirHistoryModel } from "../embalse-historial.vm"; + +interface Props { + data: ReservoirHistoryModel; + maxCapacity: number; +} +interface MonthsAverage { + month: number; + average: number; +} +const monthsName: string[] = [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre", +]; +const monthsNumber: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + +export const Chart: React.FC = ({ data, maxCapacity }) => { + const months: MonthsAverage[] = data.months; + console.log("DATA: ", months); + console.log("maxCapacity: ", maxCapacity); + console.log("CUENTA: ", (data.months[0].average * 100) / maxCapacity); + + 63 - (100 % 12); + + // Se obtiene la capacidad máxima alcanzada que se utilizará en eje Y con un pequeño margen (* 1.2) + // const maxCapacity = Math.max(...months.map((month) => month.average)); + + const handleDates = (date: string) => { + const [year, month] = date.split("-").map(Number); + return new Date(Date.UTC(year, month, 1)); + }; + + const startDate = handleDates(data.metadata.startDate); + const endDate = handleDates(data.metadata.endDate); + + // Declare the chart dimensions and margins. + const width = 640; + const height = 400; + const marginTop = 20; + const marginRight = 20; + const marginBottom = 30; + const marginLeft = 40; + const svgRef = useRef(null); + + useEffect(() => { + if (!months || months.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); // Limpiar SVG antes de redibujar + + // Declara escala de posición horizontal (X) + const x = d3 + .scaleBand() + .domain(monthsNumber) + .range([marginLeft, width - marginRight]) + .padding(0.4); + + // Declara escala de posición vertical (Y) + const y = d3 + .scaleLinear() + .domain([0, 100]) + .range([height - marginBottom, marginTop]); + + // Dibuja las barras + svg + .append("g") + .attr("fill", "#3DA9B7") + .selectAll("rect") + .data(months) + .enter() + .append("rect") + .attr("width", 30) // Ancho de las barras + .attr("height", (d) => y(0) - y(d.average)) + .attr("x", (d) => x(d.month)) + .attr("y", (d) => y(d.average)); + + // Agrega Eje X + svg + .append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(d3.axisBottom(x).tickFormat((d) => monthsName[d - 1])); + + // Agrega Eje Y + svg + .append("g") + .attr("transform", `translate(${marginLeft},0)`) + .call(d3.axisLeft(y)); + }, [data]); + + // Return the SVG element. + return ; +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.component.tsx b/front/src/pods/embalse-historial/embalse-historial.component.tsx index b6b4669..0087eaf 100644 --- a/front/src/pods/embalse-historial/embalse-historial.component.tsx +++ b/front/src/pods/embalse-historial/embalse-historial.component.tsx @@ -1,12 +1,22 @@ "use client"; import React from "react"; import { ReservoirHistoryModel } from "./embalse-historial.vm"; +import { Chart } from "./component/chart"; interface Props { statisticsLastYear: ReservoirHistoryModel; + maxCapacity: number; } export const EmbalseHistorialComponent: React.FC = (props) => { - const { statisticsLastYear } = props; + const { statisticsLastYear, maxCapacity } = props; - return
EmbalseHistorialComponent
; + return ( + <> + + + ); }; diff --git a/front/src/pods/embalse-historial/embalse-historial.pod.tsx b/front/src/pods/embalse-historial/embalse-historial.pod.tsx index 17e10e7..b32920a 100644 --- a/front/src/pods/embalse-historial/embalse-historial.pod.tsx +++ b/front/src/pods/embalse-historial/embalse-historial.pod.tsx @@ -4,12 +4,17 @@ import { EmbalseHistorialComponent } from "./embalse-historial.component"; interface Props { reservoirHistoryLastYear: ReservoirHistoryModel; + maxCapacity: number; } export const EmbalseHistorialPod: React.FC = ({ reservoirHistoryLastYear, + maxCapacity, }) => { return ( - + ); }; From c07e4f08e6ea208e5368ddec0287fc591d583e31 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Tue, 17 Mar 2026 10:50:44 +0100 Subject: [PATCH 03/10] fix percentage calculation (max capacity) and style component --- front/src/app/embalse/[embalse]/page.tsx | 7 +- .../embalse-historial/component/chart.tsx | 78 ++++++------------- .../embalse-historial/component/chart.vm.ts | 11 +++ .../embalse-historial/component/constants.ts | 23 ++++++ .../embalse-historial.component.tsx | 8 +- front/src/pods/embalse/embalse.pod.tsx | 17 +++- 6 files changed, 82 insertions(+), 62 deletions(-) create mode 100644 front/src/pods/embalse-historial/component/chart.vm.ts create mode 100644 front/src/pods/embalse-historial/component/constants.ts diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index 3728edf..293a901 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -7,7 +7,6 @@ import { } from "@/pods/embalse"; import { mapEmbalseToReservoirData } from "@/pods/embalse/embalse.mapper"; import { - EmbalseHistorialPod, getPromedioHistoricoPorMeses, ReservoirHistoryModel, } from "@/pods/embalse-historial"; @@ -45,17 +44,15 @@ export default async function EmbalseDetallePage({ params }: Props) { /** * Obtiene historial de agua embalsada del último año por meses según nombre de embalse recibido. */ - const reservoirHistoryLastYear: ReservoirHistoryModel = await getPromedioHistoricoPorMeses(embalseDoc.nombre); return ( <> - - ); } diff --git a/front/src/pods/embalse-historial/component/chart.tsx b/front/src/pods/embalse-historial/component/chart.tsx index c192546..1509b72 100644 --- a/front/src/pods/embalse-historial/component/chart.tsx +++ b/front/src/pods/embalse-historial/component/chart.tsx @@ -1,58 +1,20 @@ "use client"; import * as d3 from "d3"; import { useEffect, useRef } from "react"; -import { ReservoirHistoryModel } from "../embalse-historial.vm"; +import { ChartModel, MonthsAverage } from "./chart.vm"; +import { + monthsNumber, + monthsName, + width, + height, + marginTop, + marginRight, + marginLeft, + marginBottom, +} from "./constants"; -interface Props { - data: ReservoirHistoryModel; - maxCapacity: number; -} -interface MonthsAverage { - month: number; - average: number; -} -const monthsName: string[] = [ - "Enero", - "Febrero", - "Marzo", - "Abril", - "Mayo", - "Junio", - "Julio", - "Agosto", - "Septiembre", - "Octubre", - "Noviembre", - "Diciembre", -]; -const monthsNumber: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - -export const Chart: React.FC = ({ data, maxCapacity }) => { +export const Chart: React.FC = ({ data, maxCapacity, title }) => { const months: MonthsAverage[] = data.months; - console.log("DATA: ", months); - console.log("maxCapacity: ", maxCapacity); - console.log("CUENTA: ", (data.months[0].average * 100) / maxCapacity); - - 63 - (100 % 12); - - // Se obtiene la capacidad máxima alcanzada que se utilizará en eje Y con un pequeño margen (* 1.2) - // const maxCapacity = Math.max(...months.map((month) => month.average)); - - const handleDates = (date: string) => { - const [year, month] = date.split("-").map(Number); - return new Date(Date.UTC(year, month, 1)); - }; - - const startDate = handleDates(data.metadata.startDate); - const endDate = handleDates(data.metadata.endDate); - - // Declare the chart dimensions and margins. - const width = 640; - const height = 400; - const marginTop = 20; - const marginRight = 20; - const marginBottom = 30; - const marginLeft = 40; const svgRef = useRef(null); useEffect(() => { @@ -83,9 +45,9 @@ export const Chart: React.FC = ({ data, maxCapacity }) => { .enter() .append("rect") .attr("width", 30) // Ancho de las barras - .attr("height", (d) => y(0) - y(d.average)) + .attr("height", (d) => y(0) - y((d.average * 100) / maxCapacity)) // Establece el formato de los datos en % .attr("x", (d) => x(d.month)) - .attr("y", (d) => y(d.average)); + .attr("y", (d) => y((d.average * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 // Agrega Eje X svg @@ -101,5 +63,15 @@ export const Chart: React.FC = ({ data, maxCapacity }) => { }, [data]); // Return the SVG element. - return ; + return ( +
+

+ {title} +

+ +
+ ); }; diff --git a/front/src/pods/embalse-historial/component/chart.vm.ts b/front/src/pods/embalse-historial/component/chart.vm.ts new file mode 100644 index 0000000..0461b24 --- /dev/null +++ b/front/src/pods/embalse-historial/component/chart.vm.ts @@ -0,0 +1,11 @@ +import { ReservoirHistoryModel } from "../embalse-historial.vm"; + +export interface ChartModel { + data: ReservoirHistoryModel; + maxCapacity: number; + title: string; +} +export interface MonthsAverage { + month: number; + average: number; +} diff --git a/front/src/pods/embalse-historial/component/constants.ts b/front/src/pods/embalse-historial/component/constants.ts new file mode 100644 index 0000000..fd06bf8 --- /dev/null +++ b/front/src/pods/embalse-historial/component/constants.ts @@ -0,0 +1,23 @@ +// Declare the chart dimensions and margins. +export const width = 640; +export const height = 400; +export const marginTop = 20; +export const marginRight = 20; +export const marginBottom = 30; +export const marginLeft = 40; + +export const monthsName: string[] = [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre", +]; +export const monthsNumber: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; diff --git a/front/src/pods/embalse-historial/embalse-historial.component.tsx b/front/src/pods/embalse-historial/embalse-historial.component.tsx index 0087eaf..57b7f72 100644 --- a/front/src/pods/embalse-historial/embalse-historial.component.tsx +++ b/front/src/pods/embalse-historial/embalse-historial.component.tsx @@ -11,12 +11,16 @@ export const EmbalseHistorialComponent: React.FC = (props) => { const { statisticsLastYear, maxCapacity } = props; return ( - <> +
- +
); }; diff --git a/front/src/pods/embalse/embalse.pod.tsx b/front/src/pods/embalse/embalse.pod.tsx index 65c7a9a..96eb292 100644 --- a/front/src/pods/embalse/embalse.pod.tsx +++ b/front/src/pods/embalse/embalse.pod.tsx @@ -1,11 +1,24 @@ import React from "react"; import { Embalse } from "./embalse.component"; import { ReservoirData, ReservoirInfo } from "./embalse.vm"; +import { + EmbalseHistorialPod, + ReservoirHistoryModel, +} from "../embalse-historial"; interface Props { reservoirData: ReservoirData; + reservoirHistoryLastYear: ReservoirHistoryModel; } export const EmbalsePod: React.FC = (props) => { - const { reservoirData } = props; - return ; + const { reservoirData, reservoirHistoryLastYear } = props; + return ( + <> + + + + ); }; From e4941aa7b6095385ca2185d148a08e89a6950965 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Wed, 18 Mar 2026 13:02:24 +0100 Subject: [PATCH 04/10] feat: implement chart layout with temporary values --- .../embalse-historial/component/chart.tsx | 139 +++++++++++++----- .../embalse-historial/component/chart.vm.ts | 1 + .../embalse-historial/component/constants.ts | 34 ++--- .../embalse-historial.component.tsx | 2 +- 4 files changed, 116 insertions(+), 60 deletions(-) diff --git a/front/src/pods/embalse-historial/component/chart.tsx b/front/src/pods/embalse-historial/component/chart.tsx index 1509b72..453521e 100644 --- a/front/src/pods/embalse-historial/component/chart.tsx +++ b/front/src/pods/embalse-historial/component/chart.tsx @@ -1,77 +1,136 @@ -"use client"; import * as d3 from "d3"; import { useEffect, useRef } from "react"; -import { ChartModel, MonthsAverage } from "./chart.vm"; -import { - monthsNumber, - monthsName, - width, - height, - marginTop, - marginRight, - marginLeft, - marginBottom, -} from "./constants"; +import { ChartModel } from "./chart.vm"; +import { color, sizeChart as s } from "./constants"; export const Chart: React.FC = ({ data, maxCapacity, title }) => { - const months: MonthsAverage[] = data.months; const svgRef = useRef(null); - useEffect(() => { - if (!months || months.length === 0) return; + const reservoirData = { currentVolume: 30 }; // TODO: Provisional actual state + useEffect(() => { + //SVG const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); // Limpiar SVG antes de redibujar + svg.selectAll("*").remove(); - // Declara escala de posición horizontal (X) + //ESCALA X const x = d3 .scaleBand() - .domain(monthsNumber) - .range([marginLeft, width - marginRight]) - .padding(0.4); + .domain([data.reservoir]) + .range([s.margin.left, s.width - s.margin.right]) + .padding(0.2); - // Declara escala de posición vertical (Y) + // ESCALA Y const y = d3 .scaleLinear() .domain([0, 100]) - .range([height - marginBottom, marginTop]); + .range([s.height - s.margin.bottom, s.margin.top]); + + // Agrega Eje Y + svg + .append("g") + .attr("transform", `translate(${s.margin.left},0)`) + .call(d3.axisLeft(y)); - // Dibuja las barras + // BARRA DE NIVEL ACTUAL svg .append("g") - .attr("fill", "#3DA9B7") + .attr("fill", color.actualAverage) .selectAll("rect") - .data(months) + .data([data]) .enter() .append("rect") - .attr("width", 30) // Ancho de las barras - .attr("height", (d) => y(0) - y((d.average * 100) / maxCapacity)) // Establece el formato de los datos en % - .attr("x", (d) => x(d.month)) - .attr("y", (d) => y((d.average * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 + .attr("width", x.bandwidth()) // Ancho de las barras + .attr( + "height", + (d) => y(0) - y((reservoirData.currentVolume * 100) / maxCapacity), + ) + .attr("x", (d) => x(data.reservoir)) + .attr("y", (d) => y((reservoirData.currentVolume * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 - // Agrega Eje X + // TODO: Media % Último año svg - .append("g") - .attr("transform", `translate(0,${height - marginBottom})`) - .call(d3.axisBottom(x).tickFormat((d) => monthsName[d - 1])); + .append("line") + .attr("y1", y(40)) + .attr("y2", y(40)) + .attr("x1", x(data.reservoir) - s.margin.left / 2) + .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) + .attr("stroke", color.averageLastYear) + .attr("stroke-width", 2.5) + .attr("stroke-dasharray", "3,2"); - // Agrega Eje Y + // Media 10 años: línea vertical discontinua gris + svg + .append("line") + .attr("y1", y(20)) + .attr("y2", y(20)) + .attr("x1", x(data.reservoir) - s.margin.left / 2) + .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) + .attr("stroke", color.averageLast10Years) + .attr("stroke-width", 2.5) + .attr("stroke-dasharray", "3,2"); + + // Etiqueta Eje X svg .append("g") - .attr("transform", `translate(${marginLeft},0)`) - .call(d3.axisLeft(y)); - }, [data]); + .attr("transform", `translate(0,${s.height - s.margin.bottom})`) + .call(d3.axisBottom(x).tickFormat(() => data.reservoir)); - // Return the SVG element. + // Etiqueta Eje Y (%) + svg + .append("g") + .attr("transform", `translate(${s.margin.left},0)`) + .call((g) => g.select(".domain").remove()) + .call((g) => + g + .append("text") + .attr("x", -s.margin.left) + .attr("y", s.margin.top / 2) + .attr("fill", "currentColor") + .attr("text-anchor", "start") + .text("(%)"), + ); + }, []); return (

{title}

- +
+ +
+
+
+ + ----- + + Agua embalsada: {reservoirData.currentVolume} hm3 +
+
+ + ----- + + Media año anterior: X hm3 +
+
+ + ----- + + Media histórica: X hm3 +
+
); }; diff --git a/front/src/pods/embalse-historial/component/chart.vm.ts b/front/src/pods/embalse-historial/component/chart.vm.ts index 0461b24..aef43f7 100644 --- a/front/src/pods/embalse-historial/component/chart.vm.ts +++ b/front/src/pods/embalse-historial/component/chart.vm.ts @@ -1,3 +1,4 @@ +import { ReservoirData } from "@/pods/embalse/embalse.vm"; import { ReservoirHistoryModel } from "../embalse-historial.vm"; export interface ChartModel { diff --git a/front/src/pods/embalse-historial/component/constants.ts b/front/src/pods/embalse-historial/component/constants.ts index fd06bf8..bb039b5 100644 --- a/front/src/pods/embalse-historial/component/constants.ts +++ b/front/src/pods/embalse-historial/component/constants.ts @@ -1,23 +1,19 @@ // Declare the chart dimensions and margins. -export const width = 640; -export const height = 400; +export const width = 400; +export const height = 300; export const marginTop = 20; export const marginRight = 20; -export const marginBottom = 30; -export const marginLeft = 40; +export const marginBottom = 20; +export const marginLeft = 20; -export const monthsName: string[] = [ - "Enero", - "Febrero", - "Marzo", - "Abril", - "Mayo", - "Junio", - "Julio", - "Agosto", - "Septiembre", - "Octubre", - "Noviembre", - "Diciembre", -]; -export const monthsNumber: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; +export const sizeChart = { + width: 200, + height: 340, + margin: { top: 30, right: 30, bottom: 30, left: 30 }, +}; + +export const color = { + actualAverage: "#53D9ED", + averageLastYear: "#6904BB", + averageLast10Years: "#AA0000", +}; diff --git a/front/src/pods/embalse-historial/embalse-historial.component.tsx b/front/src/pods/embalse-historial/embalse-historial.component.tsx index 57b7f72..3b0db2f 100644 --- a/front/src/pods/embalse-historial/embalse-historial.component.tsx +++ b/front/src/pods/embalse-historial/embalse-historial.component.tsx @@ -19,7 +19,7 @@ export const EmbalseHistorialComponent: React.FC = (props) => { key={statisticsLastYear.id} data={statisticsLastYear} maxCapacity={maxCapacity} - title="Promedio por meses de los últimos 10 años" + title="Promedio" /> ); From 5e287d2faec862db65948d8f6b433317b58820a7 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Wed, 18 Mar 2026 21:00:10 +0100 Subject: [PATCH 05/10] refactor chart history component and add current value and average last year --- front/src/app/embalse/[embalse]/page.tsx | 18 ++-- .../api/embalse-historial.api-model.ts | 17 ---- .../api/embalse-historial.api.ts | 39 --------- .../embalse-historial.component.tsx | 26 ------ .../embalse-historial.mapper.ts | 20 ----- .../embalse-historial.pod.tsx | 20 ----- .../embalse-historial.repository.ts | 23 ----- .../embalse-historial/embalse-historial.vm.ts | 15 ---- front/src/pods/embalse-historial/index.ts | 3 - .../src/pods/embalse/api/embalse.api-model.ts | 18 ++++ front/src/pods/embalse/api/embalse.api.ts | 41 ++++++++- .../embalse/components/chart/chart-legend.tsx | 43 ++++++++++ .../components/chart}/chart.tsx | 83 ++++++++----------- .../components/chart}/chart.vm.ts | 6 +- .../components/chart}/constants.ts | 0 .../pods/embalse/components/chart/index.ts | 1 + front/src/pods/embalse/components/index.ts | 1 + front/src/pods/embalse/embalse.component.tsx | 22 ++++- front/src/pods/embalse/embalse.mapper.ts | 25 +++++- front/src/pods/embalse/embalse.pod.tsx | 13 +-- front/src/pods/embalse/embalse.repository.ts | 21 +++++ front/src/pods/embalse/embalse.vm.ts | 16 ++++ front/src/pods/embalse/provisionalHelper.ts | 7 ++ 23 files changed, 244 insertions(+), 234 deletions(-) delete mode 100644 front/src/pods/embalse-historial/api/embalse-historial.api-model.ts delete mode 100644 front/src/pods/embalse-historial/api/embalse-historial.api.ts delete mode 100644 front/src/pods/embalse-historial/embalse-historial.component.tsx delete mode 100644 front/src/pods/embalse-historial/embalse-historial.mapper.ts delete mode 100644 front/src/pods/embalse-historial/embalse-historial.pod.tsx delete mode 100644 front/src/pods/embalse-historial/embalse-historial.repository.ts delete mode 100644 front/src/pods/embalse-historial/embalse-historial.vm.ts delete mode 100644 front/src/pods/embalse-historial/index.ts create mode 100644 front/src/pods/embalse/components/chart/chart-legend.tsx rename front/src/pods/{embalse-historial/component => embalse/components/chart}/chart.tsx (57%) rename front/src/pods/{embalse-historial/component => embalse/components/chart}/chart.vm.ts (54%) rename front/src/pods/{embalse-historial/component => embalse/components/chart}/constants.ts (100%) create mode 100644 front/src/pods/embalse/components/chart/index.ts create mode 100644 front/src/pods/embalse/provisionalHelper.ts diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index 293a901..8132d3e 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -4,12 +4,12 @@ import { EmbalsePod, getReservoirInfoBySlugCached, getEmbalseBySlugCached, + getHistoricalAverageByMonths, } from "@/pods/embalse"; -import { mapEmbalseToReservoirData } from "@/pods/embalse/embalse.mapper"; import { - getPromedioHistoricoPorMeses, - ReservoirHistoryModel, -} from "@/pods/embalse-historial"; + mapHistoricalReservoirToViewModel, + mapEmbalseToReservoirData, +} from "@/pods/embalse/embalse.mapper"; export const revalidate = 300; // ISR: regenerar cada 5 minutos @@ -44,14 +44,18 @@ export default async function EmbalseDetallePage({ params }: Props) { /** * Obtiene historial de agua embalsada del último año por meses según nombre de embalse recibido. */ - const reservoirHistoryLastYear: ReservoirHistoryModel = - await getPromedioHistoricoPorMeses(embalseDoc.nombre); + const historicalAverageByMonths = await getHistoricalAverageByMonths( + embalseDoc.nombre, + ); + const reservoirHistory = mapHistoricalReservoirToViewModel( + historicalAverageByMonths, + ); return ( <> ); diff --git a/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts b/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts deleted file mode 100644 index d4bd827..0000000 --- a/front/src/pods/embalse-historial/api/embalse-historial.api-model.ts +++ /dev/null @@ -1,17 +0,0 @@ -interface MonthHistoryData { - mes: number; - promedio_agua_actual: number; -} - -interface ReservoirHistoryMetadata { - generatedAt: string; - periodoInicio: string; - periodoFin: string; -} - -export interface ReservoirHistoryModel { - _id: string; - embalse: string; - metadata: ReservoirHistoryMetadata; - meses: MonthHistoryData[]; -} diff --git a/front/src/pods/embalse-historial/api/embalse-historial.api.ts b/front/src/pods/embalse-historial/api/embalse-historial.api.ts deleted file mode 100644 index 4dd4e9b..0000000 --- a/front/src/pods/embalse-historial/api/embalse-historial.api.ts +++ /dev/null @@ -1,39 +0,0 @@ -import "server-only"; -import { unstable_cache } from "next/cache"; -import { ReservoirHistoryModel } from "../embalse-historial.vm"; -import { getHistorialPromedioPorMeses } from "../embalse-historial.repository"; -import { mapEmbalseHistorialToLookup } from "../embalse-historial.mapper"; - -/** - * Cached version of getPromedioHistoricoPorMeses. - * Revalidates every 5 minutes. - */ -const getHistoricalAverageByMonthsCached = unstable_cache( - async (name: string): Promise => { - const statisticsReservoir = await getHistorialPromedioPorMeses(name); - - if (!statisticsReservoir) { - throw new Error("Empty historico embalse - skip cache"); - } - const reservoirStatisticsLastYear: ReservoirHistoryModel = - mapEmbalseHistorialToLookup(statisticsReservoir); - return reservoirStatisticsLastYear; - }, - ["embalses-historico-por-meses"], - { revalidate: 300 }, -); - -export const getPromedioHistoricoPorMeses = async ( - name: string, -): Promise => { - try { - return await getHistoricalAverageByMonthsCached(name); - } catch (error) { - console.warn( - "getHistoricalAverageByMonths: MongoDB not available or empty, returning empty array.", - "Error:", - error instanceof Error ? error.message : error, - ); - return; - } -}; diff --git a/front/src/pods/embalse-historial/embalse-historial.component.tsx b/front/src/pods/embalse-historial/embalse-historial.component.tsx deleted file mode 100644 index 3b0db2f..0000000 --- a/front/src/pods/embalse-historial/embalse-historial.component.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; -import React from "react"; -import { ReservoirHistoryModel } from "./embalse-historial.vm"; -import { Chart } from "./component/chart"; - -interface Props { - statisticsLastYear: ReservoirHistoryModel; - maxCapacity: number; -} -export const EmbalseHistorialComponent: React.FC = (props) => { - const { statisticsLastYear, maxCapacity } = props; - - return ( -
- -
- ); -}; diff --git a/front/src/pods/embalse-historial/embalse-historial.mapper.ts b/front/src/pods/embalse-historial/embalse-historial.mapper.ts deleted file mode 100644 index f9e17ba..0000000 --- a/front/src/pods/embalse-historial/embalse-historial.mapper.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ReservoirHistoryModel } from "./embalse-historial.vm"; -import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api/embalse-historial.api-model"; - -export const mapEmbalseHistorialToLookup = ( - apiData: ReservoirHistoryModelApi, -): ReservoirHistoryModel => { - return { - id: apiData._id, - metadata: { - lastUpdate: apiData.metadata.generatedAt, - startDate: apiData.metadata.periodoInicio, - endDate: apiData.metadata.periodoFin, - }, - reservoir: apiData.embalse, - months: apiData.meses.map((mes) => ({ - month: mes.mes, - average: mes.promedio_agua_actual, - })), - }; -}; diff --git a/front/src/pods/embalse-historial/embalse-historial.pod.tsx b/front/src/pods/embalse-historial/embalse-historial.pod.tsx deleted file mode 100644 index b32920a..0000000 --- a/front/src/pods/embalse-historial/embalse-historial.pod.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { ReservoirHistoryModel } from "./embalse-historial.vm"; -import { EmbalseHistorialComponent } from "./embalse-historial.component"; - -interface Props { - reservoirHistoryLastYear: ReservoirHistoryModel; - maxCapacity: number; -} - -export const EmbalseHistorialPod: React.FC = ({ - reservoirHistoryLastYear, - maxCapacity, -}) => { - return ( - - ); -}; diff --git a/front/src/pods/embalse-historial/embalse-historial.repository.ts b/front/src/pods/embalse-historial/embalse-historial.repository.ts deleted file mode 100644 index c2eb70b..0000000 --- a/front/src/pods/embalse-historial/embalse-historial.repository.ts +++ /dev/null @@ -1,23 +0,0 @@ -"use server"; - -import { getDb } from "@/lib/mongodb"; -import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api/embalse-historial.api-model"; - -export const getHistorialPromedioPorMeses = async ( - name: string, -): Promise => { - try { - const db = await getDb(); - const statisticsLastYear = await db - .collection("embalsesPromedioHistoricoPorMeses") - .findOne({ embalse: name }); - - return statisticsLastYear; - } catch (error) { - console.warn( - "getHistorialPromedioPorMeses: MongoDB not available (build time?), returning empty array.", - "Error:", - error instanceof Error ? error.message : error, - ); - } -}; diff --git a/front/src/pods/embalse-historial/embalse-historial.vm.ts b/front/src/pods/embalse-historial/embalse-historial.vm.ts deleted file mode 100644 index f8695cd..0000000 --- a/front/src/pods/embalse-historial/embalse-historial.vm.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface MonthHistoryData { - month: number; - average: number; -} -interface ReservoirHistoryMetadata { - lastUpdate: string; - startDate: string; - endDate: string; -} -export interface ReservoirHistoryModel { - id: string; - reservoir: string; - metadata: ReservoirHistoryMetadata; - months: MonthHistoryData[]; -} diff --git a/front/src/pods/embalse-historial/index.ts b/front/src/pods/embalse-historial/index.ts deleted file mode 100644 index d4c6174..0000000 --- a/front/src/pods/embalse-historial/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./embalse-historial.pod"; -export * from "./api/embalse-historial.api"; -export * from "./embalse-historial.vm"; diff --git a/front/src/pods/embalse/api/embalse.api-model.ts b/front/src/pods/embalse/api/embalse.api-model.ts index eaf2219..1f59614 100644 --- a/front/src/pods/embalse/api/embalse.api-model.ts +++ b/front/src/pods/embalse/api/embalse.api-model.ts @@ -11,3 +11,21 @@ export interface ReservoirInfo { description: string; mapUrl?: string; } + +interface MonthHistoryData { + mes: number; + promedio_agua_actual: number; +} + +interface ReservoirHistoryMetadata { + generatedAt: string; + periodoInicio: string; + periodoFin: string; +} + +export interface ReservoirHistoryModel { + _id: string; + embalse: string; + metadata: ReservoirHistoryMetadata; + meses: MonthHistoryData[]; +} diff --git a/front/src/pods/embalse/api/embalse.api.ts b/front/src/pods/embalse/api/embalse.api.ts index 4b9e539..814523d 100644 --- a/front/src/pods/embalse/api/embalse.api.ts +++ b/front/src/pods/embalse/api/embalse.api.ts @@ -2,8 +2,12 @@ import "server-only"; import { unstable_cache } from "next/cache"; import type { ReservoirInfo } from "./embalse.api-model"; import { contentIslandClient } from "@/lib"; -import { getEmbalseBySlug } from "../embalse.repository"; +import { + getEmbalseBySlug, + getHistorialPromedioPorMeses, +} from "../embalse.repository"; import type { Embalse } from "db-model"; +import { ReservoirHistoryModel } from "./embalse.api-model"; /** * Cached version of getReservoirInfoBySlug. @@ -46,3 +50,38 @@ export const getEmbalseBySlugCached = unstable_cache( ["embalse-by-slug"], { revalidate: 60 }, ); + +/** + * Function for historical average. + * + * Cached version of getHistoricalAverageByMonths. + * Revalidates every 60 minutes. + **/ +const getHistoricalAverageByMonthsCached = unstable_cache( + async (name: string): Promise => { + const statisticsReservoir = await getHistorialPromedioPorMeses(name); + + if (!statisticsReservoir) { + throw new Error("Empty historico embalse - skip cache"); + } + + return statisticsReservoir; + }, + ["embalses-historico-por-meses"], + { revalidate: 3600 }, +); + +export const getHistoricalAverageByMonths = async ( + name: string, +): Promise => { + try { + return await getHistoricalAverageByMonthsCached(name); + } catch (error) { + console.warn( + "getHistoricalAverageByMonths: MongoDB not available or empty, returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + return; + } +}; diff --git a/front/src/pods/embalse/components/chart/chart-legend.tsx b/front/src/pods/embalse/components/chart/chart-legend.tsx new file mode 100644 index 0000000..f4cd663 --- /dev/null +++ b/front/src/pods/embalse/components/chart/chart-legend.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { color } from "./constants"; + +interface Props { + currentLevel: number; + averageLastYear: number; + averageHistory: number; +} +export const ChartLegend: React.FC = (props) => { + const { currentLevel, averageLastYear, averageHistory } = props; + + return ( +
+
+
+
Agua embalsada: {currentLevel} Hm³
+
+
+
+ ----- +
+ Media año anterior: {averageLastYear.toFixed(2)} Hm³ +
+
+
+ ----- +
+ Media histórica: {averageHistory} Hm³ +
+
+ ); +}; diff --git a/front/src/pods/embalse-historial/component/chart.tsx b/front/src/pods/embalse/components/chart/chart.tsx similarity index 57% rename from front/src/pods/embalse-historial/component/chart.tsx rename to front/src/pods/embalse/components/chart/chart.tsx index 453521e..3288d71 100644 --- a/front/src/pods/embalse-historial/component/chart.tsx +++ b/front/src/pods/embalse/components/chart/chart.tsx @@ -1,19 +1,29 @@ +"use client"; import * as d3 from "d3"; import { useEffect, useRef } from "react"; import { ChartModel } from "./chart.vm"; import { color, sizeChart as s } from "./constants"; +import { ChartLegend } from "./chart-legend"; -export const Chart: React.FC = ({ data, maxCapacity, title }) => { - const svgRef = useRef(null); +export const ChartHistory: React.FC = ({ + data, + currentLevel, + maxCapacity, + averageLastYear, + averageHistory, + title, +}) => { + console.log("DATA: ", data); + const percentageLastYear = (averageLastYear * 100) / maxCapacity; - const reservoirData = { currentVolume: 30 }; // TODO: Provisional actual state + const svgRef = useRef(null); useEffect(() => { - //SVG + // Declara SVG const svg = d3.select(svgRef.current); svg.selectAll("*").remove(); - //ESCALA X + // ESCALA X const x = d3 .scaleBand() .domain([data.reservoir]) @@ -23,7 +33,7 @@ export const Chart: React.FC = ({ data, maxCapacity, title }) => { // ESCALA Y const y = d3 .scaleLinear() - .domain([0, 100]) + .domain([0, 105]) .range([s.height - s.margin.bottom, s.margin.top]); // Agrega Eje Y @@ -41,34 +51,31 @@ export const Chart: React.FC = ({ data, maxCapacity, title }) => { .enter() .append("rect") .attr("width", x.bandwidth()) // Ancho de las barras - .attr( - "height", - (d) => y(0) - y((reservoirData.currentVolume * 100) / maxCapacity), - ) + .attr("height", (d) => y(0) - y((currentLevel * 100) / maxCapacity)) .attr("x", (d) => x(data.reservoir)) - .attr("y", (d) => y((reservoirData.currentVolume * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 + .attr("y", (d) => y((currentLevel * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 // TODO: Media % Último año svg .append("line") - .attr("y1", y(40)) - .attr("y2", y(40)) + .attr("y1", y((averageLastYear * 100) / maxCapacity)) + .attr("y2", y((averageLastYear * 100) / maxCapacity)) .attr("x1", x(data.reservoir) - s.margin.left / 2) .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) .attr("stroke", color.averageLastYear) .attr("stroke-width", 2.5) - .attr("stroke-dasharray", "3,2"); + .attr("stroke-dasharray", "7"); // Media 10 años: línea vertical discontinua gris svg .append("line") - .attr("y1", y(20)) - .attr("y2", y(20)) + .attr("y1", y((averageHistory * 100) / maxCapacity)) + .attr("y2", y((averageHistory * 100) / maxCapacity)) .attr("x1", x(data.reservoir) - s.margin.left / 2) .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) .attr("stroke", color.averageLast10Years) .attr("stroke-width", 2.5) - .attr("stroke-dasharray", "3,2"); + .attr("stroke-dasharray", "7"); // Etiqueta Eje X svg @@ -87,49 +94,27 @@ export const Chart: React.FC = ({ data, maxCapacity, title }) => { .attr("x", -s.margin.left) .attr("y", s.margin.top / 2) .attr("fill", "currentColor") - .attr("text-anchor", "start") - .text("(%)"), + .attr("text-anchor", "start"), ); }, []); return (

{title}

-
- -
-
-
- - ----- - - Agua embalsada: {reservoirData.currentVolume} hm3 -
-
- - ----- - - Media año anterior: X hm3 -
+
- - ----- - - Media histórica: X hm3 +
+
); diff --git a/front/src/pods/embalse-historial/component/chart.vm.ts b/front/src/pods/embalse/components/chart/chart.vm.ts similarity index 54% rename from front/src/pods/embalse-historial/component/chart.vm.ts rename to front/src/pods/embalse/components/chart/chart.vm.ts index aef43f7..a45857d 100644 --- a/front/src/pods/embalse-historial/component/chart.vm.ts +++ b/front/src/pods/embalse/components/chart/chart.vm.ts @@ -1,9 +1,11 @@ -import { ReservoirData } from "@/pods/embalse/embalse.vm"; -import { ReservoirHistoryModel } from "../embalse-historial.vm"; +import { ReservoirHistoryModel } from "@/pods/embalse/embalse.vm"; export interface ChartModel { data: ReservoirHistoryModel; + currentLevel: number; maxCapacity: number; + averageLastYear: number; + averageHistory: number; title: string; } export interface MonthsAverage { diff --git a/front/src/pods/embalse-historial/component/constants.ts b/front/src/pods/embalse/components/chart/constants.ts similarity index 100% rename from front/src/pods/embalse-historial/component/constants.ts rename to front/src/pods/embalse/components/chart/constants.ts diff --git a/front/src/pods/embalse/components/chart/index.ts b/front/src/pods/embalse/components/chart/index.ts new file mode 100644 index 0000000..cbfa750 --- /dev/null +++ b/front/src/pods/embalse/components/chart/index.ts @@ -0,0 +1 @@ +export * from "./chart"; diff --git a/front/src/pods/embalse/components/index.ts b/front/src/pods/embalse/components/index.ts index 0843170..f62974a 100644 --- a/front/src/pods/embalse/components/index.ts +++ b/front/src/pods/embalse/components/index.ts @@ -2,3 +2,4 @@ export * from "./reservoir-card-detail"; export * from "./reservoir-card-gauge"; export * from "./reservoir-card-info.component"; export * from "./reservoir-gauge"; +export * from "./chart"; diff --git a/front/src/pods/embalse/embalse.component.tsx b/front/src/pods/embalse/embalse.component.tsx index a1566af..93189d9 100644 --- a/front/src/pods/embalse/embalse.component.tsx +++ b/front/src/pods/embalse/embalse.component.tsx @@ -3,10 +3,13 @@ import { ReservoirCardDetail, ReservoirCardGauge, ReservoirCardInfo, + ChartHistory, } from "./components"; -import { ReservoirData } from "./embalse.vm"; +import { ReservoirData, ReservoirHistoryModel } from "./embalse.vm"; +import { getAverageByMonths } from "./provisionalHelper"; interface Props { reservoirData: ReservoirData; + statisticsLastYear: ReservoirHistoryModel; } /** La prop name de ReservoirCardGauge ahora recibe reservoirData.nombre (el nombre real del embalse desde la BD). @@ -14,7 +17,12 @@ interface Props { */ export const Embalse: React.FC = (props) => { - const { reservoirData } = props; + const { reservoirData, statisticsLastYear } = props; + const averageLastYear = getAverageByMonths( + statisticsLastYear.months.map((month) => month.average), + reservoirData.totalCapacity, + ); + console.log("reservoirData.totalCapacity: ", reservoirData.totalCapacity); return (
@@ -45,6 +53,16 @@ export const Embalse: React.FC = (props) => {
+
+ +
); }; diff --git a/front/src/pods/embalse/embalse.mapper.ts b/front/src/pods/embalse/embalse.mapper.ts index 1b5fa9e..9201fb0 100644 --- a/front/src/pods/embalse/embalse.mapper.ts +++ b/front/src/pods/embalse/embalse.mapper.ts @@ -12,8 +12,13 @@ */ import type { Embalse } from "db-model"; -import type { ReservoirData, ReservoirInfo } from "./embalse.vm"; +import type { + ReservoirData, + ReservoirHistoryModel, + ReservoirInfo, +} from "./embalse.vm"; import * as apiModel from "./api"; +import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api"; function formatDate(date: Date | string | null | undefined): string { if (!date) return ""; @@ -61,3 +66,21 @@ const mapReservoirInfoFromContentIslandToViewModel = ( authorUrl: embalseInfo.authorUrl ?? "", description: embalseInfo.description ?? "", }); + +export const mapHistoricalReservoirToViewModel = ( + apiData: ReservoirHistoryModelApi, +): ReservoirHistoryModel => { + return { + id: apiData._id, + metadata: { + lastUpdate: apiData.metadata.generatedAt, + startDate: apiData.metadata.periodoInicio, + endDate: apiData.metadata.periodoFin, + }, + reservoir: apiData.embalse, + months: apiData.meses.map((mes) => ({ + month: mes.mes, + average: mes.promedio_agua_actual, + })), + }; +}; diff --git a/front/src/pods/embalse/embalse.pod.tsx b/front/src/pods/embalse/embalse.pod.tsx index 96eb292..3fa481e 100644 --- a/front/src/pods/embalse/embalse.pod.tsx +++ b/front/src/pods/embalse/embalse.pod.tsx @@ -1,10 +1,6 @@ import React from "react"; import { Embalse } from "./embalse.component"; -import { ReservoirData, ReservoirInfo } from "./embalse.vm"; -import { - EmbalseHistorialPod, - ReservoirHistoryModel, -} from "../embalse-historial"; +import { ReservoirData, ReservoirHistoryModel } from "./embalse.vm"; interface Props { reservoirData: ReservoirData; @@ -14,10 +10,9 @@ export const EmbalsePod: React.FC = (props) => { const { reservoirData, reservoirHistoryLastYear } = props; return ( <> - - ); diff --git a/front/src/pods/embalse/embalse.repository.ts b/front/src/pods/embalse/embalse.repository.ts index 6e3ec70..269c3da 100644 --- a/front/src/pods/embalse/embalse.repository.ts +++ b/front/src/pods/embalse/embalse.repository.ts @@ -2,7 +2,9 @@ import { getDb } from "@/lib/mongodb"; import type { Embalse } from "db-model"; +import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api"; import { mapEmbalse } from "./embalse.repository.mapper"; + export async function getEmbalseBySlug(slug: string): Promise { //conecta con BD y trae datos en base al slug const db = await getDb(); @@ -15,3 +17,22 @@ export async function getEmbalseBySlug(slug: string): Promise { const mappedEmbalse = mapEmbalse(embalse); return mappedEmbalse; } + +export const getHistorialPromedioPorMeses = async ( + name: string, +): Promise => { + try { + const db = await getDb(); + const statisticsLastYear = await db + .collection("embalsesPromedioHistoricoPorMeses") + .findOne({ embalse: name }); + + return statisticsLastYear; + } catch (error) { + console.warn( + "getHistorialPromedioPorMeses: MongoDB not available (build time?), returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + } +}; diff --git a/front/src/pods/embalse/embalse.vm.ts b/front/src/pods/embalse/embalse.vm.ts index 58362db..0a5e7bc 100644 --- a/front/src/pods/embalse/embalse.vm.ts +++ b/front/src/pods/embalse/embalse.vm.ts @@ -53,3 +53,19 @@ export const createEmptyEmbalseInfo = (): ReservoirInfo => ({ authorUrl: "", description: "", }); + +interface MonthHistoryData { + month: number; + average: number; +} +interface ReservoirHistoryMetadata { + lastUpdate: string; + startDate: string; + endDate: string; +} +export interface ReservoirHistoryModel { + id: string; + reservoir: string; + metadata: ReservoirHistoryMetadata; + months: MonthHistoryData[]; +} diff --git a/front/src/pods/embalse/provisionalHelper.ts b/front/src/pods/embalse/provisionalHelper.ts new file mode 100644 index 0000000..64f0407 --- /dev/null +++ b/front/src/pods/embalse/provisionalHelper.ts @@ -0,0 +1,7 @@ +export const getAverageByMonths = ( + arr: number[], + totalCapacity: number, +): number => { + const result = arr.reduce((total, value) => total + value, 0); + return result / arr.length; +}; From 5dea11825140a227f8e8d6d775d99a5c38a93ca8 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Tue, 24 Mar 2026 18:11:13 +0100 Subject: [PATCH 06/10] refactor: optimize database queries and fix minor bugs --- front/src/app/embalse/[embalse]/page.tsx | 32 ++-- .../src/pods/embalse/api/embalse.api-model.ts | 24 ++- front/src/pods/embalse/api/embalse.api.ts | 78 ++++++--- .../embalse/components/chart/chart-legend.tsx | 77 +++++--- .../pods/embalse/components/chart/chart.tsx | 164 ++++++++++++------ .../pods/embalse/components/chart/chart.vm.ts | 17 +- .../embalse/components/chart/constants.ts | 15 ++ .../pods/embalse/components/chart/index.ts | 1 + front/src/pods/embalse/embalse.component.tsx | 30 ++-- front/src/pods/embalse/embalse.mapper.ts | 32 ++-- front/src/pods/embalse/embalse.pod.tsx | 15 +- front/src/pods/embalse/embalse.repository.ts | 84 ++++++++- front/src/pods/embalse/embalse.vm.ts | 19 +- 13 files changed, 401 insertions(+), 187 deletions(-) diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index 8132d3e..d49c4c4 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -4,11 +4,13 @@ import { EmbalsePod, getReservoirInfoBySlugCached, getEmbalseBySlugCached, - getHistoricalAverageByMonths, + getAverageLastYearByMonthCached, + getAverageHistoricalByMonthCached, } from "@/pods/embalse"; import { - mapHistoricalReservoirToViewModel, + mapReservoirLastYearToViewModel, mapEmbalseToReservoirData, + mapHistoricalReservoirToViewModel, } from "@/pods/embalse/embalse.mapper"; export const revalidate = 300; // ISR: regenerar cada 5 minutos @@ -34,28 +36,34 @@ export default async function EmbalseDetallePage({ params }: Props) { const { embalse } = await params; const embalseDoc = await getEmbalseBySlugCached(embalse); const embalseInfo = await getReservoirInfoBySlugCached(embalse); + const actualYear = new Date().getFullYear(); + const actualMonth = new Date().getMonth(); // return month 0-11 if (!embalseDoc) { notFound(); } - const reservoirData = mapEmbalseToReservoirData(embalseDoc, embalseInfo); - /** - * Obtiene historial de agua embalsada del último año por meses según nombre de embalse recibido. - */ - const historicalAverageByMonths = await getHistoricalAverageByMonths( + const dataMappedOneYearAgo = await getAverageLastYearByMonthCached( embalseDoc.nombre, - ); - const reservoirHistory = mapHistoricalReservoirToViewModel( - historicalAverageByMonths, - ); + actualMonth + 1, + ).then(mapReservoirLastYearToViewModel); + + // Add last year to dataOneYearAgo object + const dataOneYearAgo = { ...dataMappedOneYearAgo, year: actualYear - 1 }; + + const averageHistoricalData = await getAverageHistoricalByMonthCached( + embalseDoc.nombre, + actualMonth + 1, + actualYear - 10, // 10 years ago + ).then(mapHistoricalReservoirToViewModel); return ( <> ); diff --git a/front/src/pods/embalse/api/embalse.api-model.ts b/front/src/pods/embalse/api/embalse.api-model.ts index 1f59614..79e7c58 100644 --- a/front/src/pods/embalse/api/embalse.api-model.ts +++ b/front/src/pods/embalse/api/embalse.api-model.ts @@ -12,20 +12,30 @@ export interface ReservoirInfo { mapUrl?: string; } -interface MonthHistoryData { +interface MonthLastYearData { mes: number; promedio_agua_actual: number; } -interface ReservoirHistoryMetadata { +export interface ReservoirLastYearModel { + mes: number; + promedio_agua_actual: number; +} + +// Historical reservoir data +interface MonthHistorical { + año: number; + mes: number; + promedio_agua_actual: number; +} +interface ReservoirHistoricalMetadata { generatedAt: string; periodoInicio: string; periodoFin: string; } - -export interface ReservoirHistoryModel { - _id: string; +export interface HistoricalAverageReservoir { embalse: string; - metadata: ReservoirHistoryMetadata; - meses: MonthHistoryData[]; + mes: number; + año: number; + promedio_agua_actual: number; } diff --git a/front/src/pods/embalse/api/embalse.api.ts b/front/src/pods/embalse/api/embalse.api.ts index 814523d..03b09c1 100644 --- a/front/src/pods/embalse/api/embalse.api.ts +++ b/front/src/pods/embalse/api/embalse.api.ts @@ -1,13 +1,17 @@ import "server-only"; import { unstable_cache } from "next/cache"; -import type { ReservoirInfo } from "./embalse.api-model"; +import type { + ReservoirInfo, + ReservoirLastYearModel, + HistoricalAverageReservoir, +} from "./embalse.api-model"; import { contentIslandClient } from "@/lib"; import { getEmbalseBySlug, - getHistorialPromedioPorMeses, + getAverageLastYearByMonth, + getAverageHistoricalByMonth, } from "../embalse.repository"; import type { Embalse } from "db-model"; -import { ReservoirHistoryModel } from "./embalse.api-model"; /** * Cached version of getReservoirInfoBySlug. @@ -57,31 +61,55 @@ export const getEmbalseBySlugCached = unstable_cache( * Cached version of getHistoricalAverageByMonths. * Revalidates every 60 minutes. **/ -const getHistoricalAverageByMonthsCached = unstable_cache( - async (name: string): Promise => { - const statisticsReservoir = await getHistorialPromedioPorMeses(name); +export const getAverageLastYearByMonthCached = unstable_cache( + async (name: string, month: number): Promise => { + try { + const statisticsReservoir = await getAverageLastYearByMonth(name, month); - if (!statisticsReservoir) { - throw new Error("Empty historico embalse - skip cache"); - } + if (!statisticsReservoir) { + throw new Error("Empty data last year by month - skip cache"); + } - return statisticsReservoir; + return statisticsReservoir; + } catch (error) { + console.warn( + "getAverageLastYearByMonthCached: MongoDB not available or empty, returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + return; + } }, - ["embalses-historico-por-meses"], + ["reservoir-last-year"], { revalidate: 3600 }, ); -export const getHistoricalAverageByMonths = async ( - name: string, -): Promise => { - try { - return await getHistoricalAverageByMonthsCached(name); - } catch (error) { - console.warn( - "getHistoricalAverageByMonths: MongoDB not available or empty, returning empty array.", - "Error:", - error instanceof Error ? error.message : error, - ); - return; - } -}; +export const getAverageHistoricalByMonthCached = unstable_cache( + async ( + reservoirName: string, + month: number, + year: number, + ): Promise => { + try { + const historicalStatistics = await getAverageHistoricalByMonth( + reservoirName, + month, + year, + ); + + if (!historicalStatistics) { + throw new Error("Empty historical data by month and year"); + } + return historicalStatistics; + } catch (error) { + console.warn( + "getAverageHistoricalByMonthCached: MongoDB not available or empty, returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + return; + } + }, + ["reservoir-last-ten-year"], + { revalidate: 3600 }, +); diff --git a/front/src/pods/embalse/components/chart/chart-legend.tsx b/front/src/pods/embalse/components/chart/chart-legend.tsx index f4cd663..ffba789 100644 --- a/front/src/pods/embalse/components/chart/chart-legend.tsx +++ b/front/src/pods/embalse/components/chart/chart-legend.tsx @@ -1,43 +1,72 @@ import React from "react"; -import { color } from "./constants"; +import { color, monthsNames } from "./constants"; interface Props { currentLevel: number; - averageLastYear: number; - averageHistory: number; + monthOneYearAgo: number; + yearOneYearAgo: number; + averageOneYearAgo: number; + monthTenYearsAgo: number; + yearTenYearsAgo: number; + averageTenYearsAgo: number; } export const ChartLegend: React.FC = (props) => { - const { currentLevel, averageLastYear, averageHistory } = props; + const { + currentLevel, + monthOneYearAgo, + yearOneYearAgo, + averageOneYearAgo, + monthTenYearsAgo, + yearTenYearsAgo, + averageTenYearsAgo, + } = props; return ( -
-
+
+
-
Agua embalsada: {currentLevel} Hm³
-
-
-
- ----- +
+ Agua embalsada: + {currentLevel} Hm³
- Media año anterior: {averageLastYear.toFixed(2)} Hm³
-
-
- ----- + {averageOneYearAgo && ( +
+
+ ---- +
+
+ + Media en {monthsNames[monthOneYearAgo - 1]} de {yearOneYearAgo}: + + {averageOneYearAgo} Hm³ +
- Media histórica: {averageHistory} Hm³ -
+ )} + {averageTenYearsAgo && ( +
+
+ ······· +
+
+ + Media en {monthsNames[monthTenYearsAgo - 1]} de {yearTenYearsAgo}: + + {averageTenYearsAgo} Hm³ +
+
+ )}
); }; diff --git a/front/src/pods/embalse/components/chart/chart.tsx b/front/src/pods/embalse/components/chart/chart.tsx index 3288d71..7a7f770 100644 --- a/front/src/pods/embalse/components/chart/chart.tsx +++ b/front/src/pods/embalse/components/chart/chart.tsx @@ -5,19 +5,31 @@ import { ChartModel } from "./chart.vm"; import { color, sizeChart as s } from "./constants"; import { ChartLegend } from "./chart-legend"; -export const ChartHistory: React.FC = ({ - data, +export const HistoryChart: React.FC = ({ + titleChart, + reservoirName, currentLevel, maxCapacity, - averageLastYear, - averageHistory, - title, + dataOneYearAgo, + dataTenYearsAgo, }) => { - console.log("DATA: ", data); - const percentageLastYear = (averageLastYear * 100) / maxCapacity; - const svgRef = useRef(null); + /** + * Muestra etiqueta dentro de la barra si el porcentaje + * del nivel del embalse es superior a 10%. + * Si es inferior, muestra la etiqueta encima de la barra. + */ + const percentageActual = (currentLevel * 100) / maxCapacity; + const isOutSide = percentageActual < 10; + + const showLabelActualLevel = (y) => { + if (percentageActual < 10) { + return y(percentageActual) - 8; + } else { + return y(percentageActual) + 20; + } + }; useEffect(() => { // Declara SVG const svg = d3.select(svgRef.current); @@ -26,7 +38,7 @@ export const ChartHistory: React.FC = ({ // ESCALA X const x = d3 .scaleBand() - .domain([data.reservoir]) + .domain([reservoirName]) .range([s.margin.left, s.width - s.margin.right]) .padding(0.2); @@ -36,53 +48,13 @@ export const ChartHistory: React.FC = ({ .domain([0, 105]) .range([s.height - s.margin.bottom, s.margin.top]); - // Agrega Eje Y + // EJE Y + /** svg .append("g") .attr("transform", `translate(${s.margin.left},0)`) .call(d3.axisLeft(y)); - // BARRA DE NIVEL ACTUAL - svg - .append("g") - .attr("fill", color.actualAverage) - .selectAll("rect") - .data([data]) - .enter() - .append("rect") - .attr("width", x.bandwidth()) // Ancho de las barras - .attr("height", (d) => y(0) - y((currentLevel * 100) / maxCapacity)) - .attr("x", (d) => x(data.reservoir)) - .attr("y", (d) => y((currentLevel * 100) / maxCapacity)); // Muestra % de la media según capacidad máxima del embalse y no los Hm3 - - // TODO: Media % Último año - svg - .append("line") - .attr("y1", y((averageLastYear * 100) / maxCapacity)) - .attr("y2", y((averageLastYear * 100) / maxCapacity)) - .attr("x1", x(data.reservoir) - s.margin.left / 2) - .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) - .attr("stroke", color.averageLastYear) - .attr("stroke-width", 2.5) - .attr("stroke-dasharray", "7"); - - // Media 10 años: línea vertical discontinua gris - svg - .append("line") - .attr("y1", y((averageHistory * 100) / maxCapacity)) - .attr("y2", y((averageHistory * 100) / maxCapacity)) - .attr("x1", x(data.reservoir) - s.margin.left / 2) - .attr("x2", x(data.reservoir) * 2 + s.margin.left + s.margin.right) - .attr("stroke", color.averageLast10Years) - .attr("stroke-width", 2.5) - .attr("stroke-dasharray", "7"); - - // Etiqueta Eje X - svg - .append("g") - .attr("transform", `translate(0,${s.height - s.margin.bottom})`) - .call(d3.axisBottom(x).tickFormat(() => data.reservoir)); - // Etiqueta Eje Y (%) svg .append("g") @@ -94,8 +66,85 @@ export const ChartHistory: React.FC = ({ .attr("x", -s.margin.left) .attr("y", s.margin.top / 2) .attr("fill", "currentColor") - .attr("text-anchor", "start"), + .attr("text-anchor", "start") + .text("(%)"), ); + **/ + + // NIVEL ACTUAL + const barGroup = svg + .append("g") + .attr("fill", color.actualAverage) + .selectAll("g") + .data([reservoirName]) + .enter() + .append("g"); + + const barX = x(reservoirName); + const barY = y(percentageActual); + const barWidth = x.bandwidth(); // Ancho de las barras + const barHeight = y(0) - barY; + const radius = 10; // Border-radius + + // Barra de nivel actual con esquinas redondeadas + barGroup + .append("rect") + .attr("x", barX) + .attr("y", barY) + .attr("width", barWidth) + .attr("height", barHeight) + .attr("rx", radius); + + // Muestra una segunda barra con la mitad de altura. Elimina esquinas inferiores redondeadas + barGroup + .append("rect") + .attr("x", barX) + .attr("y", barY + barHeight / 2) + .attr("width", barWidth) // Ancho de las barras + .attr("height", barHeight / 2); + + // Label que indica nivel actual en Hm³ + barGroup + .append("text") + .attr("x", (d) => x(reservoirName) + x.bandwidth() / 2) + .attr("y", (d) => showLabelActualLevel(y)) + .attr("text-anchor", "middle") + .attr("font-size", "16px") + .attr("fill", isOutSide ? "#333" : "#FFF") + .style("font-weight", "900") + .text(`${currentLevel} Hm³`); + + // MEDIA AÑO ANTERIOR + dataOneYearAgo && + svg + .append("line") + .attr("y1", y((dataOneYearAgo.average * 100) / maxCapacity)) + .attr("y2", y((dataOneYearAgo.average * 100) / maxCapacity)) + .attr("x1", x(reservoirName) - s.margin.left / 2) + .attr("x2", x(reservoirName) * 2 + s.margin.left + s.margin.right) + .attr("stroke", color.averageLastYear) + .attr("stroke-width", 5) // Grosor línea + .attr("stroke-dasharray", "12"); // Espaciado entre guiones + + // MEDIA HACE 10 AÑOS + dataTenYearsAgo && + svg + .append("line") + .attr("y1", y((dataTenYearsAgo.average * 100) / maxCapacity)) + .attr("y2", y((dataTenYearsAgo.average * 100) / maxCapacity)) + .attr("x1", x(reservoirName) - s.margin.left / 2) + .attr("x2", x(reservoirName) * 2 + s.margin.left + s.margin.right) + .attr("stroke", color.averageLast10Years) + .attr("stroke-width", 5) // Grosor línea + .attr("stroke-dasharray", "3"); // Espaciado entre guiones + + // Línea y Etiqueta Eje X + svg + .append("g") + .attr("transform", `translate(0,${s.height - s.margin.bottom})`) + .call(d3.axisBottom(x).tickSize(0)) // sin marcas + .call((ax) => ax.selectAll("text").remove()) // sin etiquetas + .call((ax) => ax.select(".domain").attr("stroke", "#aaa")); // Muestra línea Eje X sin mostrar ningún nombre }, []); return (
= ({ aria-labelledby="gauge-title" >

- {title} + {titleChart}

diff --git a/front/src/pods/embalse/components/chart/chart.vm.ts b/front/src/pods/embalse/components/chart/chart.vm.ts index a45857d..d2c4687 100644 --- a/front/src/pods/embalse/components/chart/chart.vm.ts +++ b/front/src/pods/embalse/components/chart/chart.vm.ts @@ -1,14 +1,13 @@ -import { ReservoirHistoryModel } from "@/pods/embalse/embalse.vm"; +import { + DataLastYearModel, + HistoricalAverageReservoir, +} from "@/pods/embalse/embalse.vm"; export interface ChartModel { - data: ReservoirHistoryModel; + titleChart: string; + reservoirName: string; currentLevel: number; maxCapacity: number; - averageLastYear: number; - averageHistory: number; - title: string; -} -export interface MonthsAverage { - month: number; - average: number; + dataOneYearAgo?: DataLastYearModel; + dataTenYearsAgo?: HistoricalAverageReservoir; } diff --git a/front/src/pods/embalse/components/chart/constants.ts b/front/src/pods/embalse/components/chart/constants.ts index bb039b5..1acd04f 100644 --- a/front/src/pods/embalse/components/chart/constants.ts +++ b/front/src/pods/embalse/components/chart/constants.ts @@ -17,3 +17,18 @@ export const color = { averageLastYear: "#6904BB", averageLast10Years: "#AA0000", }; + +export const monthsNames = [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre", +]; diff --git a/front/src/pods/embalse/components/chart/index.ts b/front/src/pods/embalse/components/chart/index.ts index cbfa750..a023a23 100644 --- a/front/src/pods/embalse/components/chart/index.ts +++ b/front/src/pods/embalse/components/chart/index.ts @@ -1 +1,2 @@ export * from "./chart"; +export * from "./chart.vm"; diff --git a/front/src/pods/embalse/embalse.component.tsx b/front/src/pods/embalse/embalse.component.tsx index 93189d9..883361a 100644 --- a/front/src/pods/embalse/embalse.component.tsx +++ b/front/src/pods/embalse/embalse.component.tsx @@ -3,13 +3,17 @@ import { ReservoirCardDetail, ReservoirCardGauge, ReservoirCardInfo, - ChartHistory, + HistoryChart, } from "./components"; -import { ReservoirData, ReservoirHistoryModel } from "./embalse.vm"; -import { getAverageByMonths } from "./provisionalHelper"; +import { + ReservoirData, + DataLastYearModel, + HistoricalAverageReservoir, +} from "./embalse.vm"; interface Props { reservoirData: ReservoirData; - statisticsLastYear: ReservoirHistoryModel; + dataOneYearAgo: DataLastYearModel; + dataTenYearsAgo: HistoricalAverageReservoir; } /** La prop name de ReservoirCardGauge ahora recibe reservoirData.nombre (el nombre real del embalse desde la BD). @@ -17,12 +21,8 @@ interface Props { */ export const Embalse: React.FC = (props) => { - const { reservoirData, statisticsLastYear } = props; - const averageLastYear = getAverageByMonths( - statisticsLastYear.months.map((month) => month.average), - reservoirData.totalCapacity, - ); - console.log("reservoirData.totalCapacity: ", reservoirData.totalCapacity); + const { reservoirData, dataOneYearAgo, dataTenYearsAgo } = props; + return (
@@ -54,13 +54,13 @@ export const Embalse: React.FC = (props) => {
-
diff --git a/front/src/pods/embalse/embalse.mapper.ts b/front/src/pods/embalse/embalse.mapper.ts index 9201fb0..ed8f4f5 100644 --- a/front/src/pods/embalse/embalse.mapper.ts +++ b/front/src/pods/embalse/embalse.mapper.ts @@ -14,11 +14,11 @@ import type { Embalse } from "db-model"; import type { ReservoirData, - ReservoirHistoryModel, + DataLastYearModel, ReservoirInfo, + HistoricalAverageReservoir, } from "./embalse.vm"; import * as apiModel from "./api"; -import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api"; function formatDate(date: Date | string | null | undefined): string { if (!date) return ""; @@ -67,20 +67,22 @@ const mapReservoirInfoFromContentIslandToViewModel = ( description: embalseInfo.description ?? "", }); +export const mapReservoirLastYearToViewModel = ( + apiData: apiModel.ReservoirLastYearModel, +): DataLastYearModel => { + return { + month: apiData.mes, + average: apiData.promedio_agua_actual, + }; +}; + export const mapHistoricalReservoirToViewModel = ( - apiData: ReservoirHistoryModelApi, -): ReservoirHistoryModel => { + apiData: apiModel.HistoricalAverageReservoir, +): HistoricalAverageReservoir => { return { - id: apiData._id, - metadata: { - lastUpdate: apiData.metadata.generatedAt, - startDate: apiData.metadata.periodoInicio, - endDate: apiData.metadata.periodoFin, - }, - reservoir: apiData.embalse, - months: apiData.meses.map((mes) => ({ - month: mes.mes, - average: mes.promedio_agua_actual, - })), + nameReservoir: apiData.embalse, + year: apiData.año, + month: apiData.mes, + average: apiData.promedio_agua_actual, }; }; diff --git a/front/src/pods/embalse/embalse.pod.tsx b/front/src/pods/embalse/embalse.pod.tsx index 3fa481e..1289e2e 100644 --- a/front/src/pods/embalse/embalse.pod.tsx +++ b/front/src/pods/embalse/embalse.pod.tsx @@ -1,18 +1,25 @@ import React from "react"; import { Embalse } from "./embalse.component"; -import { ReservoirData, ReservoirHistoryModel } from "./embalse.vm"; +import { + DataLastYearModel, + HistoricalAverageReservoir, + ReservoirData, +} from "./embalse.vm"; interface Props { reservoirData: ReservoirData; - reservoirHistoryLastYear: ReservoirHistoryModel; + dataOneYearAgo: DataLastYearModel; + dataTenYearsAgo: HistoricalAverageReservoir; } + export const EmbalsePod: React.FC = (props) => { - const { reservoirData, reservoirHistoryLastYear } = props; + const { reservoirData, dataOneYearAgo, dataTenYearsAgo } = props; return ( <> ); diff --git a/front/src/pods/embalse/embalse.repository.ts b/front/src/pods/embalse/embalse.repository.ts index 269c3da..d97312c 100644 --- a/front/src/pods/embalse/embalse.repository.ts +++ b/front/src/pods/embalse/embalse.repository.ts @@ -2,7 +2,7 @@ import { getDb } from "@/lib/mongodb"; import type { Embalse } from "db-model"; -import { ReservoirHistoryModel as ReservoirHistoryModelApi } from "./api"; +import * as ApiModel from "./api"; import { mapEmbalse } from "./embalse.repository.mapper"; export async function getEmbalseBySlug(slug: string): Promise { @@ -18,21 +18,87 @@ export async function getEmbalseBySlug(slug: string): Promise { return mappedEmbalse; } -export const getHistorialPromedioPorMeses = async ( - name: string, -): Promise => { +/** + * Obtain the monthly average for the last year. + * @param name:string Reservoir name + * @returns ReservoirHistoryModelApi: + */ +export const getAverageLastYearByMonth = async ( + reservoirName: string, + month: number, +): Promise => { try { const db = await getDb(); - const statisticsLastYear = await db - .collection("embalsesPromedioHistoricoPorMeses") - .findOne({ embalse: name }); + const results = await db + .collection( + "embalsesPromedioHistoricoPorMeses", + ) + .aggregate([ + { $match: { embalse: reservoirName } }, + { $unwind: "$meses" }, + { $match: { "meses.mes": month } }, + { + $project: { + mes: "$meses.mes", + promedio_agua_actual: "$meses.promedio_agua_actual", + }, + }, + { $limit: 1 }, + ]) + .next(); - return statisticsLastYear; + return results; } catch (error) { console.warn( - "getHistorialPromedioPorMeses: MongoDB not available (build time?), returning empty array.", + "getAverageByMonths: MongoDB not available (build time?), returning empty array.", "Error:", error instanceof Error ? error.message : error, ); } }; + +/** + * Obtain the average monthly and annual values ​​received per parameter + * + * @param name:string Reservoir name + * @param month:number Month number (1-12) + * @param year:number Year number (yyyy) + * @returns HistoricalAverageReservoir: + */ +export const getAverageHistoricalByMonth = async ( + reservoirName: string, + month: number, + year: number, +): Promise => { + try { + const db = await getDb(); + const results = await db + .collection( + "embalsesPromedioHistoricoDiezAnios", + ) + .aggregate([ + { $match: { embalse: reservoirName } }, + { $unwind: "$meses" }, + { $match: { "meses.año": year, "meses.mes": month } }, + { + $project: { + _id: 0, + embalse: 1, + año: "$meses.año", + mes: "$meses.mes", + promedio_agua_actual: "$meses.promedio_agua_actual", + }, + }, + ]) + .next(); + + return results; + } catch (error) { + console.warn( + "getAverageHistorical: MongoDB not available (build time?), returning empty array.", + "Error:", + error instanceof Error ? error.message : error, + ); + return; + } +}; diff --git a/front/src/pods/embalse/embalse.vm.ts b/front/src/pods/embalse/embalse.vm.ts index 0a5e7bc..2490f67 100644 --- a/front/src/pods/embalse/embalse.vm.ts +++ b/front/src/pods/embalse/embalse.vm.ts @@ -54,18 +54,15 @@ export const createEmptyEmbalseInfo = (): ReservoirInfo => ({ description: "", }); -interface MonthHistoryData { +export interface DataLastYearModel { + year?: number; month: number; average: number; } -interface ReservoirHistoryMetadata { - lastUpdate: string; - startDate: string; - endDate: string; -} -export interface ReservoirHistoryModel { - id: string; - reservoir: string; - metadata: ReservoirHistoryMetadata; - months: MonthHistoryData[]; + +export interface HistoricalAverageReservoir { + nameReservoir: string; + month: number; + year: number; + average: number; } From 9ea5195f5929b1693c3b45dbfa154e7a8aed1008 Mon Sep 17 00:00:00 2001 From: josemi1189 Date: Fri, 27 Mar 2026 19:07:59 +0100 Subject: [PATCH 07/10] refactor(ui): improve styles and adjust chart layout for responsive screens --- front/src/app/globals.css | 4 +- .../embalse/components/chart/chart-legend.tsx | 50 ++--- .../components/chart/chart.constants.ts | 23 +++ .../components/chart/chart.helpers.tsx | 44 +++++ .../pods/embalse/components/chart/chart.tsx | 173 ------------------ .../embalse/components/chart/constants.ts | 34 ---- .../components/chart/history-chart.tsx | 128 +++++++++++++ .../pods/embalse/components/chart/index.ts | 2 +- .../components/reservoir-card-gauge.tsx | 82 ++++++++- .../components/gauge-arcs.business.ts | 21 ++- front/src/pods/embalse/embalse.component.tsx | 27 ++- front/src/pods/embalse/provisionalHelper.ts | 7 - 12 files changed, 317 insertions(+), 278 deletions(-) create mode 100644 front/src/pods/embalse/components/chart/chart.constants.ts create mode 100644 front/src/pods/embalse/components/chart/chart.helpers.tsx delete mode 100644 front/src/pods/embalse/components/chart/chart.tsx delete mode 100644 front/src/pods/embalse/components/chart/constants.ts create mode 100644 front/src/pods/embalse/components/chart/history-chart.tsx delete mode 100644 front/src/pods/embalse/provisionalHelper.ts diff --git a/front/src/app/globals.css b/front/src/app/globals.css index f0275c1..3c11b57 100644 --- a/front/src/app/globals.css +++ b/front/src/app/globals.css @@ -24,8 +24,10 @@ /* Title color */ --color-title: #051c1f; - /* Graphic total water */ + /* Colors of reservoir graphics */ --color-total-water: #26d6ed; + --line-average-last-year: #6904bb; + --line-average-last-ten-years: #952e00; /* Accesible visited link color */ --color-visited-link: #257782; diff --git a/front/src/pods/embalse/components/chart/chart-legend.tsx b/front/src/pods/embalse/components/chart/chart-legend.tsx index ffba789..a7cf76f 100644 --- a/front/src/pods/embalse/components/chart/chart-legend.tsx +++ b/front/src/pods/embalse/components/chart/chart-legend.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { color, monthsNames } from "./constants"; +import { monthsNames } from "./chart.constants"; +import { ReferenceLine } from "./chart.helpers"; interface Props { currentLevel: number; @@ -22,48 +23,33 @@ export const ChartLegend: React.FC = (props) => { } = props; return ( -
-
-
-
- Agua embalsada: - {currentLevel} Hm³ +
+
+
+
+ Embalsada: + {currentLevel} Hm³
{averageOneYearAgo && ( -
-
- ---- -
-
+
+
+
- Media en {monthsNames[monthOneYearAgo - 1]} de {yearOneYearAgo}: + {monthsNames[monthOneYearAgo - 1]} de {yearOneYearAgo}: - {averageOneYearAgo} Hm³ + {averageOneYearAgo} Hm³
)} {averageTenYearsAgo && ( -
-
- ······· -
-
+
+
+
- Media en {monthsNames[monthTenYearsAgo - 1]} de {yearTenYearsAgo}: + {monthsNames[monthTenYearsAgo - 1]} de {yearTenYearsAgo}: - {averageTenYearsAgo} Hm³ + {averageTenYearsAgo} Hm³
)} diff --git a/front/src/pods/embalse/components/chart/chart.constants.ts b/front/src/pods/embalse/components/chart/chart.constants.ts new file mode 100644 index 0000000..0ba2aac --- /dev/null +++ b/front/src/pods/embalse/components/chart/chart.constants.ts @@ -0,0 +1,23 @@ +// Declare the chart dimensions and margins. + +export const sizeChart = { + width: 200, + height: 180, + margin: { top: 0, right: 30, bottom: 0, left: 30 }, + radius: 10, +}; + +export const monthsNames = [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre", +]; diff --git a/front/src/pods/embalse/components/chart/chart.helpers.tsx b/front/src/pods/embalse/components/chart/chart.helpers.tsx new file mode 100644 index 0000000..21e48d3 --- /dev/null +++ b/front/src/pods/embalse/components/chart/chart.helpers.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { sizeChart as s } from "./chart.constants"; + +interface barRoundedTopProps { + x: number; + y: number; + width: number; + height: number; + fill: string; +} +export const BarRoundedTop: React.FC = ({ + x, + y, + width, + height, + fill, +}): React.ReactNode => { + return ( + + {/* Barra según porcentaje con esquinas redondeadas */} + + {/* Barra inferior sin redondeo para aplanar la base */} + + + ); +}; + +export const ReferenceLine: React.FC<{ + yPos: number; + x1: number; + x2: number; + stroke: string; + dashArray: string; +}> = ({ yPos, x1, x2, stroke, dashArray }) => ( + +); diff --git a/front/src/pods/embalse/components/chart/chart.tsx b/front/src/pods/embalse/components/chart/chart.tsx deleted file mode 100644 index 7a7f770..0000000 --- a/front/src/pods/embalse/components/chart/chart.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client"; -import * as d3 from "d3"; -import { useEffect, useRef } from "react"; -import { ChartModel } from "./chart.vm"; -import { color, sizeChart as s } from "./constants"; -import { ChartLegend } from "./chart-legend"; - -export const HistoryChart: React.FC = ({ - titleChart, - reservoirName, - currentLevel, - maxCapacity, - dataOneYearAgo, - dataTenYearsAgo, -}) => { - const svgRef = useRef(null); - - /** - * Muestra etiqueta dentro de la barra si el porcentaje - * del nivel del embalse es superior a 10%. - * Si es inferior, muestra la etiqueta encima de la barra. - */ - const percentageActual = (currentLevel * 100) / maxCapacity; - const isOutSide = percentageActual < 10; - - const showLabelActualLevel = (y) => { - if (percentageActual < 10) { - return y(percentageActual) - 8; - } else { - return y(percentageActual) + 20; - } - }; - useEffect(() => { - // Declara SVG - const svg = d3.select(svgRef.current); - svg.selectAll("*").remove(); - - // ESCALA X - const x = d3 - .scaleBand() - .domain([reservoirName]) - .range([s.margin.left, s.width - s.margin.right]) - .padding(0.2); - - // ESCALA Y - const y = d3 - .scaleLinear() - .domain([0, 105]) - .range([s.height - s.margin.bottom, s.margin.top]); - - // EJE Y - /** - svg - .append("g") - .attr("transform", `translate(${s.margin.left},0)`) - .call(d3.axisLeft(y)); - - // Etiqueta Eje Y (%) - svg - .append("g") - .attr("transform", `translate(${s.margin.left},0)`) - .call((g) => g.select(".domain").remove()) - .call((g) => - g - .append("text") - .attr("x", -s.margin.left) - .attr("y", s.margin.top / 2) - .attr("fill", "currentColor") - .attr("text-anchor", "start") - .text("(%)"), - ); - **/ - - // NIVEL ACTUAL - const barGroup = svg - .append("g") - .attr("fill", color.actualAverage) - .selectAll("g") - .data([reservoirName]) - .enter() - .append("g"); - - const barX = x(reservoirName); - const barY = y(percentageActual); - const barWidth = x.bandwidth(); // Ancho de las barras - const barHeight = y(0) - barY; - const radius = 10; // Border-radius - - // Barra de nivel actual con esquinas redondeadas - barGroup - .append("rect") - .attr("x", barX) - .attr("y", barY) - .attr("width", barWidth) - .attr("height", barHeight) - .attr("rx", radius); - - // Muestra una segunda barra con la mitad de altura. Elimina esquinas inferiores redondeadas - barGroup - .append("rect") - .attr("x", barX) - .attr("y", barY + barHeight / 2) - .attr("width", barWidth) // Ancho de las barras - .attr("height", barHeight / 2); - - // Label que indica nivel actual en Hm³ - barGroup - .append("text") - .attr("x", (d) => x(reservoirName) + x.bandwidth() / 2) - .attr("y", (d) => showLabelActualLevel(y)) - .attr("text-anchor", "middle") - .attr("font-size", "16px") - .attr("fill", isOutSide ? "#333" : "#FFF") - .style("font-weight", "900") - .text(`${currentLevel} Hm³`); - - // MEDIA AÑO ANTERIOR - dataOneYearAgo && - svg - .append("line") - .attr("y1", y((dataOneYearAgo.average * 100) / maxCapacity)) - .attr("y2", y((dataOneYearAgo.average * 100) / maxCapacity)) - .attr("x1", x(reservoirName) - s.margin.left / 2) - .attr("x2", x(reservoirName) * 2 + s.margin.left + s.margin.right) - .attr("stroke", color.averageLastYear) - .attr("stroke-width", 5) // Grosor línea - .attr("stroke-dasharray", "12"); // Espaciado entre guiones - - // MEDIA HACE 10 AÑOS - dataTenYearsAgo && - svg - .append("line") - .attr("y1", y((dataTenYearsAgo.average * 100) / maxCapacity)) - .attr("y2", y((dataTenYearsAgo.average * 100) / maxCapacity)) - .attr("x1", x(reservoirName) - s.margin.left / 2) - .attr("x2", x(reservoirName) * 2 + s.margin.left + s.margin.right) - .attr("stroke", color.averageLast10Years) - .attr("stroke-width", 5) // Grosor línea - .attr("stroke-dasharray", "3"); // Espaciado entre guiones - - // Línea y Etiqueta Eje X - svg - .append("g") - .attr("transform", `translate(0,${s.height - s.margin.bottom})`) - .call(d3.axisBottom(x).tickSize(0)) // sin marcas - .call((ax) => ax.selectAll("text").remove()) // sin etiquetas - .call((ax) => ax.select(".domain").attr("stroke", "#aaa")); // Muestra línea Eje X sin mostrar ningún nombre - }, []); - return ( -
-

- {titleChart} -

-
-
- -
- -
-
- ); -}; diff --git a/front/src/pods/embalse/components/chart/constants.ts b/front/src/pods/embalse/components/chart/constants.ts deleted file mode 100644 index 1acd04f..0000000 --- a/front/src/pods/embalse/components/chart/constants.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Declare the chart dimensions and margins. -export const width = 400; -export const height = 300; -export const marginTop = 20; -export const marginRight = 20; -export const marginBottom = 20; -export const marginLeft = 20; - -export const sizeChart = { - width: 200, - height: 340, - margin: { top: 30, right: 30, bottom: 30, left: 30 }, -}; - -export const color = { - actualAverage: "#53D9ED", - averageLastYear: "#6904BB", - averageLast10Years: "#AA0000", -}; - -export const monthsNames = [ - "Enero", - "Febrero", - "Marzo", - "Abril", - "Mayo", - "Junio", - "Julio", - "Agosto", - "Septiembre", - "Octubre", - "Noviembre", - "Diciembre", -]; diff --git a/front/src/pods/embalse/components/chart/history-chart.tsx b/front/src/pods/embalse/components/chart/history-chart.tsx new file mode 100644 index 0000000..4b2ae43 --- /dev/null +++ b/front/src/pods/embalse/components/chart/history-chart.tsx @@ -0,0 +1,128 @@ +import * as d3 from "d3"; +import { ChartModel } from "./chart.vm"; +import { sizeChart as s } from "./chart.constants"; +import { ChartLegend } from "./chart-legend"; +import { BarRoundedTop, ReferenceLine } from "./chart.helpers"; + +export const HistoryChart: React.FC = ({ + titleChart, + reservoirName, + currentLevel, + maxCapacity, + dataOneYearAgo, + dataTenYearsAgo, +}) => { + const percentageActual = (currentLevel * 100) / maxCapacity; + const isOutside = percentageActual < 10; + + // Cálculo de escalas — sin manipulación del DOM + const x = d3 + .scaleBand() + .domain([reservoirName]) + .range([s.margin.left, s.width - s.margin.right]) + .padding(0.2); + + const y = d3 + .scaleLinear() + .domain([0, 105]) + .range([s.height - s.margin.bottom, s.margin.top]); + + const barX = x(reservoirName); + const barWidth = x.bandwidth(); + const barY = y(percentageActual); + const barHeight = y(0) - barY; + + // 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 + const labelY = isOutside ? barY - 8 : barY + 20; + + return ( +
+

+ {titleChart} +

+ + + {/* Indicador de capacidad total (100%) */} + + + {/* Nivel actual */} + + + {/* Etiqueta con el nivel actual en Hm³ */} + + {currentLevel} Hm³ + + + {/* Línea de referencia: mismo mes del año anterior */} + {dataOneYearAgo && ( + + )} + + {/* Línea de referencia: mismo mes hace 10 años */} + {dataTenYearsAgo && ( + + )} + + {/* Eje X */} + + + + +
+ ); +}; diff --git a/front/src/pods/embalse/components/chart/index.ts b/front/src/pods/embalse/components/chart/index.ts index a023a23..542fa0c 100644 --- a/front/src/pods/embalse/components/chart/index.ts +++ b/front/src/pods/embalse/components/chart/index.ts @@ -1,2 +1,2 @@ -export * from "./chart"; +export * from "./history-chart"; export * from "./chart.vm"; diff --git a/front/src/pods/embalse/components/reservoir-card-gauge.tsx b/front/src/pods/embalse/components/reservoir-card-gauge.tsx index 937bbb4..51d7a50 100644 --- a/front/src/pods/embalse/components/reservoir-card-gauge.tsx +++ b/front/src/pods/embalse/components/reservoir-card-gauge.tsx @@ -1,6 +1,9 @@ +"use client"; +import React from "react"; import { ReservoirData } from "../embalse.vm"; import { GaugeChart } from "./reservoir-gauge"; import { GaugeLegend } from "./reservoir-gauge/gauge-chart/components/gauge-legend.component"; +import { HistoryChart } from "./chart"; interface Props { name: string; reservoirData: ReservoirData; @@ -9,20 +12,85 @@ export const ReservoirCardGauge: React.FC = (props) => { const { name, reservoirData } = props; const { currentVolume, totalCapacity, measurementDate } = reservoirData; const percentage = totalCapacity > 0 ? currentVolume / totalCapacity : 0; + const [cardGaugeSelected, setCardGaugeSelected] = + React.useState(true); + + const useIsMobile = () => { + const [isMobile, setIsMobile] = React.useState(false); + React.useEffect(() => { + const mediaQuery = window.matchMedia("(max-width: 768px)"); + setIsMobile(mediaQuery.matches); + + const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); + mediaQuery.addEventListener("change", handler); + + return () => mediaQuery.removeEventListener("change", handler); + }, []); + + return isMobile; + }; + const isMobile = useIsMobile(); + + const handleGraphicDisplay = (e: React.MouseEvent) => { + const action = e.currentTarget.name; + if (action === "currentStatus") { + setCardGaugeSelected(true); + } else if (action === "historicalStatus") { + setCardGaugeSelected(false); + } + }; return (

{name}

- 100 ? 100 : percentage} measurementDate= - {measurementDate} /> - + {isMobile && ( +
+ + +
+ )} + {cardGaugeSelected || !isMobile ? ( + <> + 100 ? 100 : percentage} + measurementDate={measurementDate} + /> + + + ) : ( + + )}
); }; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.ts b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.ts index f96eb17..b80436f 100644 --- a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.ts +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.ts @@ -19,8 +19,6 @@ const createArcGenerator = (endAngle: number) => { .cornerRadius(arcConfig.cornerRadius); }; - - export const calculateFilledAngle = (percentage: number): number => { // Ensure percentage is within valid range [0, 1] const normalized = Math.max(0, Math.min(1, percentage)); @@ -30,7 +28,10 @@ export const calculateFilledAngle = (percentage: number): number => { return arcConfig.startAngle + normalized * totalAngle; }; -export const drawArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams, animate: boolean = false) => { +export const drawArc = ( + { arcGroup, endAngle, fillColor }: DrawArcParams, + animate: boolean = false, +) => { const arcGenerator = createArcGenerator(endAngle); if (animate) { @@ -51,10 +52,12 @@ export const drawArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams, animat } }; - -export const drawAnimatedArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams) => { +export const drawAnimatedArc = ({ + arcGroup, + endAngle, + fillColor, +}: DrawArcParams) => { const arcGeneratorStart = createArcGenerator(arcConfig.startAngle); - arcGroup .append("path") @@ -63,11 +66,11 @@ export const drawAnimatedArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams .transition() .duration(2000) .ease(d3.easeCubicInOut) - .attrTween("d", function() { + .attrTween("d", function () { const interpolate = d3.interpolate(arcConfig.startAngle, endAngle); - return function(t) { + return function (t) { const arcGenerator = createArcGenerator(interpolate(t)); - return arcGenerator(this) || ""; + return arcGenerator(null) || ""; }; }); }; diff --git a/front/src/pods/embalse/embalse.component.tsx b/front/src/pods/embalse/embalse.component.tsx index 883361a..4cb7b70 100644 --- a/front/src/pods/embalse/embalse.component.tsx +++ b/front/src/pods/embalse/embalse.component.tsx @@ -31,15 +31,24 @@ export const Embalse: React.FC = (props) => { reservoirData={reservoirData} />
- +
+ +
{reservoirData.reservoirInfo && ( -
+
)} {reservoirData.datosEmbalse.mapUrl && ( -
+