diff --git a/front/package.json b/front/package.json index e521b3f..d7ccf80 100644 --- a/front/package.json +++ b/front/package.json @@ -9,6 +9,7 @@ "dependencies": { "@fontsource/nunito-sans": "^5.2.7", "@tailwindcss/postcss": "^4.1.17", + "d3": "^7.9.0", "next": "^15.4.1", "postcss": "^8.5.6", "react": "^19.1.0", diff --git a/front/src/app/embalse/[embalse]/page.tsx b/front/src/app/embalse/[embalse]/page.tsx index a50d99c..86a6d43 100644 --- a/front/src/app/embalse/[embalse]/page.tsx +++ b/front/src/app/embalse/[embalse]/page.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import { EmbalsePod } from "@/pods/embalse"; interface Props { params: Promise<{ embalse: string }>; @@ -6,9 +6,6 @@ interface Props { export default async function EmbalseDetallePage({ params }: Props) { const { embalse } = await params; - return ( -
-

Detalle del embalse: {embalse}

-
- ); + + return ; } diff --git a/front/src/app/globals.css b/front/src/app/globals.css index 7025098..f0275c1 100644 --- a/front/src/app/globals.css +++ b/front/src/app/globals.css @@ -24,6 +24,9 @@ /* Title color */ --color-title: #051c1f; + /* Graphic total water */ + --color-total-water: #26d6ed; + /* Accesible visited link color */ --color-visited-link: #257782; diff --git a/front/src/app/layout.tsx b/front/src/app/layout.tsx index c4137ee..7b72e35 100644 --- a/front/src/app/layout.tsx +++ b/front/src/app/layout.tsx @@ -1,6 +1,6 @@ import React from "react"; import "./globals.css"; -import { FooterComponent, HeaderComponent } from "./layouts"; +import { FooterComponent, HeaderComponent } from "../layouts"; interface Props { children: React.ReactNode; diff --git a/front/src/app/layouts/footer.component.tsx b/front/src/layouts/footer.component.tsx similarity index 100% rename from front/src/app/layouts/footer.component.tsx rename to front/src/layouts/footer.component.tsx diff --git a/front/src/app/layouts/header.component.tsx b/front/src/layouts/header.component.tsx similarity index 100% rename from front/src/app/layouts/header.component.tsx rename to front/src/layouts/header.component.tsx diff --git a/front/src/app/layouts/index.ts b/front/src/layouts/index.ts similarity index 100% rename from front/src/app/layouts/index.ts rename to front/src/layouts/index.ts diff --git a/front/src/pods/embalse/components/index.ts b/front/src/pods/embalse/components/index.ts new file mode 100644 index 0000000..0843170 --- /dev/null +++ b/front/src/pods/embalse/components/index.ts @@ -0,0 +1,4 @@ +export * from "./reservoir-card-detail"; +export * from "./reservoir-card-gauge"; +export * from "./reservoir-card-info.component"; +export * from "./reservoir-gauge"; diff --git a/front/src/pods/embalse/components/reservoir-card-detail.tsx b/front/src/pods/embalse/components/reservoir-card-detail.tsx new file mode 100644 index 0000000..d2a3661 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-card-detail.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { DatosEmbalse } from "../embalse.vm"; + +interface Props { + datosEmbalse: DatosEmbalse; +} + +export const ReservoirCardDetail: React.FC = (props) => { + const { datosEmbalse } = props; + return ( +
+

Datos del embalse

+
    +
  • Cuenca: {datosEmbalse.cuenca}
  • +
  • Provincia: {datosEmbalse.provincia}
  • +
  • Municipio: {datosEmbalse.municipio}
  • +
  • Río: {datosEmbalse.rio}
  • +
  • Embalses Aguas Abajo: {datosEmbalse.embalsesAguasAbajo}
  • +
  • Tipo de Presa: {datosEmbalse.tipoDePresa}
  • +
  • Año de Construcción: {datosEmbalse.anioConstruccion}
  • +
  • Superficie: {datosEmbalse.superficie}
  • +
  • Localización: {datosEmbalse.localizacion}
  • +
+
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-card-gauge.tsx b/front/src/pods/embalse/components/reservoir-card-gauge.tsx new file mode 100644 index 0000000..f3d4a35 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-card-gauge.tsx @@ -0,0 +1,26 @@ +import { ReservoirData } from "../embalse.vm"; +import { GaugeChart } from "./reservoir-gauge"; +import { GaugeLegend } from "./reservoir-gauge/gauge-chart/components/gauge-legend.component"; + +interface Props { + name: string; + reservoirData: ReservoirData; +} + +export const ReservoirCardGauge: React.FC = (props) => { + const { name, reservoirData } = props; + const { currentVolume, totalCapacity, measurementDate } = reservoirData; + // const percentage = currentVolume / totalCapacity; + // TODO: replace hardcoded % for real reservoir filled water percentage + + return ( +
+

Embalse de {name}

+ + +
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-card-info.component.tsx b/front/src/pods/embalse/components/reservoir-card-info.component.tsx new file mode 100644 index 0000000..06f01c6 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-card-info.component.tsx @@ -0,0 +1,20 @@ +import { ReservoirInfo } from "../embalse.vm"; + +interface Props { + reservoirInfo: ReservoirInfo; +} + +export const ReservoirCardInfo: React.FC = (props) => { + const { reservoirInfo } = props; + return ( +
+

Descubre el embalse

+

{reservoirInfo?.Description}

+ Mapa de embalses +
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.test.ts b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.test.ts new file mode 100644 index 0000000..713a152 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { calculateFilledAngle } from "./gauge-arcs.business"; +import { arcConfig } from "./model"; + +describe("calculateFilledAngle", () => { + it("should return start angle when percentage is 0", () => { + const result = calculateFilledAngle(0); + expect(result).toBe(arcConfig.startAngle); + }); + + it("should return end angle when percentage is 1", () => { + const result = calculateFilledAngle(1); + expect(result).toBe(arcConfig.endAngle); + }); + + it("should return middle angle when percentage is 0.5", () => { + const result = calculateFilledAngle(0.5); + const expectedMiddle = arcConfig.startAngle + (arcConfig.endAngle - arcConfig.startAngle) * 0.5; + expect(result).toBe(expectedMiddle); + }); + + it("should handle percentages between 0 and 1 correctly", () => { + const testCases = [ + { percentage: 0.25, expected: arcConfig.startAngle + (arcConfig.endAngle - arcConfig.startAngle) * 0.25 }, + { percentage: 0.75, expected: arcConfig.startAngle + (arcConfig.endAngle - arcConfig.startAngle) * 0.75 }, + { percentage: 0.1, expected: arcConfig.startAngle + (arcConfig.endAngle - arcConfig.startAngle) * 0.1 }, + ]; + + testCases.forEach(({ percentage, expected }) => { + const result = calculateFilledAngle(percentage); + expect(result).toBeCloseTo(expected, 10); + }); + }); + + it("should clamp negative percentages to 0", () => { + const result = calculateFilledAngle(-0.5); + expect(result).toBe(arcConfig.startAngle); + }); + + it("should clamp percentages greater than 1 to 1", () => { + const result = calculateFilledAngle(1.5); + expect(result).toBe(arcConfig.endAngle); + }); + + it("should handle edge case percentages", () => { + expect(calculateFilledAngle(-Infinity)).toBe(arcConfig.startAngle); + expect(calculateFilledAngle(Infinity)).toBe(arcConfig.endAngle); + expect(isNaN(calculateFilledAngle(NaN))).toBe(true); + }); + + it("should handle very small positive percentages", () => { + const result = calculateFilledAngle(0.001); + const expected = arcConfig.startAngle + (arcConfig.endAngle - arcConfig.startAngle) * 0.001; + expect(result).toBeCloseTo(expected, 10); + }); + + it("should handle very large negative percentages", () => { + const result = calculateFilledAngle(-1000); + expect(result).toBe(arcConfig.startAngle); + }); +}); \ No newline at end of file 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 new file mode 100644 index 0000000..f96eb17 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.business.ts @@ -0,0 +1,73 @@ +import * as d3 from "d3"; +import { arcConfig } from "./model"; + +type ArcGroup = d3.Selection; + +interface DrawArcParams { + arcGroup: ArcGroup; + endAngle: number; + fillColor: string; +} + +const createArcGenerator = (endAngle: number) => { + return d3 + .arc() + .innerRadius(arcConfig.innerRadius) + .outerRadius(arcConfig.outerRadius) + .startAngle(arcConfig.startAngle) + .endAngle(endAngle) + .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)); + // Total sweep of the arc (from start to end) + const totalAngle = arcConfig.endAngle - arcConfig.startAngle; + // Calculate where the filled arc should end based on percentage + return arcConfig.startAngle + normalized * totalAngle; +}; + +export const drawArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams, animate: boolean = false) => { + const arcGenerator = createArcGenerator(endAngle); + + if (animate) { + arcGroup + .append("path") + .attr("d", arcGenerator as any) + .style("fill", fillColor) + .attr("opacity", 0) + .transition() + .duration(1500) + .ease(d3.easeCubicInOut) + .attr("opacity", 1); + } else { + arcGroup + .append("path") + .attr("d", arcGenerator as any) + .style("fill", fillColor); + } +}; + + +export const drawAnimatedArc = ({ arcGroup, endAngle, fillColor }: DrawArcParams) => { + const arcGeneratorStart = createArcGenerator(arcConfig.startAngle); + + + arcGroup + .append("path") + .attr("d", arcGeneratorStart as any) + .style("fill", fillColor) + .transition() + .duration(2000) + .ease(d3.easeCubicInOut) + .attrTween("d", function() { + const interpolate = d3.interpolate(arcConfig.startAngle, endAngle); + return function(t) { + const arcGenerator = createArcGenerator(interpolate(t)); + return arcGenerator(this) || ""; + }; + }); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.component.tsx b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.component.tsx new file mode 100644 index 0000000..3bb9e62 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-arcs.component.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as d3 from "d3"; +import { useEffect, useRef } from "react"; +import { calculateFilledAngle, drawAnimatedArc, drawArc } from "./gauge-arcs.business"; +import { arcConfig, gaugeDimensions } from "./model"; + +interface GaugeArcsProps { + percentage: number; +} + +export const GaugeArcs = ({ percentage }: GaugeArcsProps) => { + const svgRef = useRef(null); + + useEffect(() => { + if (!svgRef.current) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + // Center position + const centerX = gaugeDimensions.width / 2; + const centerY = arcConfig.outerRadius; + + // Create centered group + const arcGroup = svg + .append("g") + .attr("transform", `translate(${centerX}, ${centerY})`); + + // 1. Background arc (--color-total-water, full) + drawArc({ + arcGroup, + endAngle: arcConfig.endAngle, + fillColor: "var(--color-total-water)", + }); + + // 2. Filled arc (primary color, based on percentage prop) + const filledEndAngle = calculateFilledAngle(percentage); + drawAnimatedArc({ + arcGroup, + endAngle: filledEndAngle, + fillColor: "var(--color-primary)", + }); + }, [percentage]); + + return ( + + ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-chart.component.tsx b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-chart.component.tsx new file mode 100644 index 0000000..a759ac3 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-chart.component.tsx @@ -0,0 +1,31 @@ +import { gaugeDimensions } from "./model"; +import { GaugeInformation } from "./gauge-information.component"; +import { GaugeArcs } from "./gauge-arcs.component"; + +interface Props { + percentage: number; + measurementDate: string; +} + +export const GaugeChart = ({ percentage, measurementDate }: Props) => { + return ( +
+ {/* The SVG arc */} + + + {/* Center text */} +
+ +
+
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-information.component.tsx b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-information.component.tsx new file mode 100644 index 0000000..7479665 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-information.component.tsx @@ -0,0 +1,20 @@ +interface Props { + percentage: number; + measurementDate: string; +} + +export const GaugeInformation = ({ percentage, measurementDate }: Props) => { + const displayPercentage = `${Math.round(percentage * 100)}`; + + return ( +
+ + {displayPercentage} + % + + + {measurementDate} + +
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-legend.component.tsx b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-legend.component.tsx new file mode 100644 index 0000000..6cc484b --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/gauge-legend.component.tsx @@ -0,0 +1,26 @@ +interface Props { + currentVolume: number; + totalCapacity: number; +} + +export const GaugeLegend = ({ currentVolume, totalCapacity }: Props) => { + return ( +
+ {/* Embalsada (filled water) - uses primary color */} +
+ + + Embalsada: {currentVolume}m³ + +
+ + {/* Total capacity - uses total-water color */} +
+ + + Total: {totalCapacity}m³ + +
+
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/index.tsx b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/index.tsx new file mode 100644 index 0000000..83fb7a7 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/index.tsx @@ -0,0 +1,24 @@ +import { GaugeChart } from "./gauge-chart.component"; +import { GaugeLegend } from "./gauge-legend.component"; + +interface Props { + name: string; + percentage: number; + measurementDate: string; + currentVolume: number; + totalCapacity: number; +} + +export const ReservoirGauge = (props: Props) => { + const { name, measurementDate, currentVolume, totalCapacity } = props; + return ( +
+

Embalse de {name}

+ + +
+ ); +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/model.ts b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/model.ts new file mode 100644 index 0000000..fb87310 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/components/model.ts @@ -0,0 +1,25 @@ +interface GaugeDimensions { + width: number; + height: number; +} + +interface ArcConfig { + innerRadius: number; + outerRadius: number; + startAngle: number; + endAngle: number; + cornerRadius: number; +} + +export const gaugeDimensions: GaugeDimensions = { + width: 220, + height: 184, +}; + +export const arcConfig: ArcConfig = { + innerRadius: 90, + outerRadius: 110, + startAngle: -Math.PI * 0.75, + endAngle: Math.PI * 0.75, + cornerRadius: 12, +}; diff --git a/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/index.ts b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/index.ts new file mode 100644 index 0000000..5304589 --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/gauge-chart/index.ts @@ -0,0 +1 @@ +export * from "./components/gauge-chart.component"; diff --git a/front/src/pods/embalse/components/reservoir-gauge/index.ts b/front/src/pods/embalse/components/reservoir-gauge/index.ts new file mode 100644 index 0000000..760c75d --- /dev/null +++ b/front/src/pods/embalse/components/reservoir-gauge/index.ts @@ -0,0 +1 @@ +export * from "./gauge-chart"; diff --git a/front/src/pods/embalse/embalse-mock-data.ts b/front/src/pods/embalse/embalse-mock-data.ts new file mode 100644 index 0000000..b9a3892 --- /dev/null +++ b/front/src/pods/embalse/embalse-mock-data.ts @@ -0,0 +1,22 @@ +import { ReservoirData } from "./embalse.vm"; + +export const MOCK_DATA: ReservoirData = { + currentVolume: 1500, + totalCapacity: 50000, + measurementDate: "25/12/2025", + datosEmbalse: { + cuenca: "Cuenca Ejemplo", + provincia: "Provincia Ejemplo", + municipio: "Municipio Ejemplo", + rio: "Río Ejemplo", + embalsesAguasAbajo: 3, + tipoDePresa: "Tipo Ejemplo", + anioConstruccion: 1990, + superficie: 250, + localizacion: "Localización Ejemplo", + }, + reservoirInfo: { + Description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + }, +}; diff --git a/front/src/pods/embalse/embalse.component.tsx b/front/src/pods/embalse/embalse.component.tsx new file mode 100644 index 0000000..8b52f41 --- /dev/null +++ b/front/src/pods/embalse/embalse.component.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { + ReservoirCardDetail, + ReservoirCardGauge, + ReservoirCardInfo, +} from "./components"; +import { MOCK_DATA } from "./embalse-mock-data"; + +interface Props { + embalse: string; +} + +export const Embalse: React.FC = (props) => { + const { embalse } = props; + return ( +
+
+ +
+ +
+
+ +
+
+
+ ); +}; diff --git a/front/src/pods/embalse/embalse.pod.tsx b/front/src/pods/embalse/embalse.pod.tsx new file mode 100644 index 0000000..e1c978d --- /dev/null +++ b/front/src/pods/embalse/embalse.pod.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Embalse } from "./embalse.component"; + +interface Props { + embalse: string; +} + +export const EmbalsePod: React.FC = (props) => { + const { embalse } = props; + + return ; +}; diff --git a/front/src/pods/embalse/embalse.vm.ts b/front/src/pods/embalse/embalse.vm.ts new file mode 100644 index 0000000..2f778f8 --- /dev/null +++ b/front/src/pods/embalse/embalse.vm.ts @@ -0,0 +1,23 @@ +export interface DatosEmbalse { + cuenca: string; + provincia: string; + municipio: string; + rio: string; + embalsesAguasAbajo: number; + tipoDePresa: string; + anioConstruccion: number; + superficie: number; + localizacion: string; +} + +export interface ReservoirInfo { + Description: string; +} + +export interface ReservoirData { + currentVolume: number; + totalCapacity: number; + measurementDate: string; + datosEmbalse: DatosEmbalse; + reservoirInfo: ReservoirInfo; +} diff --git a/front/src/pods/embalse/index.ts b/front/src/pods/embalse/index.ts new file mode 100644 index 0000000..0f10c84 --- /dev/null +++ b/front/src/pods/embalse/index.ts @@ -0,0 +1 @@ +export * from "./embalse.pod"; diff --git a/package-lock.json b/package-lock.json index 1159d5a..f1b3622 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "dependencies": { "@fontsource/nunito-sans": "^5.2.7", "@tailwindcss/postcss": "^4.1.17", + "d3": "^7.9.0", "next": "^15.4.1", "postcss": "^8.5.6", "react": "^19.1.0", @@ -2107,6 +2108,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2165,6 +2175,407 @@ "dev": true, "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/daisyui": { "version": "5.5.5", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz", @@ -2187,6 +2598,15 @@ "node": ">=6" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -2689,6 +3109,15 @@ "node": ">=0.10.0" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "license": "MIT", @@ -3853,6 +4282,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.46.4", "dev": true, @@ -3891,6 +4326,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT"