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}
+

+
+ );
+};
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"