diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx
index fd6edf6..49d3e47 100644
--- a/frontend/src/components/editor/Step3FinalGraph.jsx
+++ b/frontend/src/components/editor/Step3FinalGraph.jsx
@@ -1,256 +1,92 @@
-import React, { useMemo } from 'react';
+import React, { useState, useEffect, memo } from 'react';
import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
-import { CHART_COLORS } from '../../config';
+import { useGraphData } from './finalGraph/useGraphData';
+import { GraphTooltip } from './finalGraph/GraphTooltip';
-// 1. Función auxiliar
-const interpolateY = (x, nodes) => {
- if (!nodes || nodes.length === 0) return null;
- const EPSILON = 1e-5;
- const MICRO_STEP = 0.0001;
+const Step3FinalGraph = memo(({ data, criterionName }) => {
+ const { sortedResults, denseData } = useGraphData(data);
+ const [isReady, setIsReady] = useState(false);
- const firstX = nodes[0][0];
- const lastX = nodes[nodes.length - 1][0];
-
- if (x < firstX - MICRO_STEP - EPSILON) return null;
- if (x > lastX + MICRO_STEP + EPSILON) return null;
-
- if (x < firstX - EPSILON) return 0;
- if (x > lastX + EPSILON) return 0;
-
- for (let i = nodes.length - 1; i >= 0; i--) {
- if (Math.abs(nodes[i][0] - x) < EPSILON) {
- return nodes[i][1];
- }
- }
-
- for (let i = 0; i < nodes.length - 1; i++) {
- const x1 = nodes[i][0];
- const x2 = nodes[i + 1][0];
-
- if (Math.abs(x2 - x1) < EPSILON) continue;
-
- if (x >= x1 && x <= x2) {
- const y1 = nodes[i][1];
- const y2 = nodes[i + 1][1];
- return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1);
- }
- }
- return null;
-};
-
-const Step3FinalGraph = ({ data, criterionName }) => {
-
- // Extracción de Nodos Base
- const sortedResults = useMemo(() => {
- const rawItems = data?.levels || data?.results || [];
-
- const processed = rawItems.map((item, index) => {
- const isType2 = !!item.lower && !!item.upper;
- const color = CHART_COLORS[index % CHART_COLORS.length] || '#333';
- let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`;
-
- if (isType2) {
- const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
- const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
- const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0;
- return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal };
- } else {
- const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
- const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0;
- return { ...item, term: termName, isType2, nodes, color, coreVal };
- }
- });
-
- return processed.sort((a, b) => a.coreVal - b.coreVal);
- }, [data]);
-
- // Generación inteligente de datos
- const denseData = useMemo(() => {
- const xSet = new Set();
- const steps = 1000;
-
- for (let i = 0; i <= steps; i++) {
- xSet.add(Number((i / steps).toFixed(4)));
- }
-
- sortedResults.forEach(item => {
- const addNodes = (nodes) => {
- nodes.forEach(n => {
- const x = n[0];
- xSet.add(Number((x - 0.0001).toFixed(4)));
- xSet.add(Number(x.toFixed(4)));
- xSet.add(Number((x + 0.0001).toFixed(4)));
- });
- };
- if (item.isType2) {
- addNodes(item.lowerNodes);
- addNodes(item.upperNodes);
- } else {
- addNodes(item.nodes);
- }
- });
-
- const xValues = Array.from(xSet).sort((a, b) => a - b);
-
- const dataPoints = [];
- xValues.forEach(x => {
- const point = { x };
-
- sortedResults.forEach(item => {
- if (item.isType2) {
- const lowerRaw = interpolateY(x, item.lowerNodes);
- const upperRaw = interpolateY(x, item.upperNodes);
-
- point[`${item.term}_lower`] = lowerRaw;
- point[`${item.term}_upper`] = upperRaw;
-
- if (lowerRaw === null && upperRaw === null) {
- point[`${item.term}_range`] = null;
- } else {
- point[`${item.term}_range`] = [lowerRaw !== null ? lowerRaw : 0, upperRaw !== null ? upperRaw : 0];
- }
- } else {
- point[item.term] = interpolateY(x, item.nodes);
- }
- });
- dataPoints.push(point);
- });
- return dataPoints;
- }, [sortedResults]);
-
- // Tooltip
- const renderCustomTooltip = ({ active, payload, label }) => {
- if (active && payload && payload.length) {
- const dataPoint = payload[0].payload;
-
- const activeTerms = sortedResults.filter(item => {
- if (item.isType2) {
- return dataPoint[`${item.term}_upper`] !== null && dataPoint[`${item.term}_upper`] > 0;
- } else {
- return dataPoint[item.term] !== null && dataPoint[item.term] > 0;
- }
- });
-
- if (activeTerms.length === 0) return null;
-
- return (
-
-
- Punto X:
- {Number(label).toFixed(3)}
-
-
- {activeTerms.map(item => {
- if (item.isType2) {
- const lower = dataPoint[`${item.term}_lower`] !== null ? dataPoint[`${item.term}_lower`] : 0;
- const upper = dataPoint[`${item.term}_upper`] !== null ? dataPoint[`${item.term}_upper`] : 0;
- const range = Math.abs(upper - lower);
-
- if (range <= 0.001) {
- return (
-
- {item.term}
-
- Pertenencia: {Number(upper).toFixed(3)}
-
-
- );
- }
-
- return (
-
- {item.term}
- Mínimo: {Number(lower).toFixed(3)}
- Máximo: {Number(upper).toFixed(3)}
-
- Incertidumbre: {Number(range).toFixed(3)}
-
-
- );
- } else {
- const val = dataPoint[item.term];
- return (
-
- {item.term}
-
- Pertenencia: {Number(val).toFixed(3)}
-
-
- );
- }
- })}
-
-
- );
- }
- return null;
- };
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsReady(true);
+ }, 400);
+ return () => clearTimeout(timer);
+ }, []);
if (!data || (!data.levels && !data.results)) {
- return Cargando gráfico final...
;
+ return Cargando datos...
;
}
return (
-
-
-
-
-
+
+
+
+
{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}
-
-
-
-
-
- Number(val.toFixed(2))}
- tick={{ fill: '#475569', fontSize: 14 }}
- />
-
-
+
+
+ {!isReady && (
+
+
+
Generando gráfica...
+
+ )}
- {sortedResults.map((item) => {
- if (item.isType2) {
- return (
+ {/* Gráfica */}
+
+ {isReady && (
+
+
+
+
+ Number(val.toFixed(2))}
+ tick={{ fill: '#475569', fontSize: 14 }}
+ />
+
+ }
+ cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }}
+ isAnimationActive={false}
+ />
+
+ {sortedResults.map((item) => (
-
-
-
+ {item.isType2 ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ )}
- );
- } else {
- return ;
- }
- })}
-
-
+ ))}
+
+
+ )}
+
-
+ {/* Leyenda */}
+
{sortedResults.map((item) => (
-
-
- {item.term}
-
+
+
+ {item.term}
+
))}
);
-};
+});
export default Step3FinalGraph;
\ No newline at end of file
diff --git a/frontend/src/components/editor/finalGraph/GraphTooltip.jsx b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx
new file mode 100644
index 0000000..bc95ac9
--- /dev/null
+++ b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx
@@ -0,0 +1,53 @@
+const TermInfo = ({ title, color, children }) => (
+
+ {title}
+ {children}
+
+);
+
+export const GraphTooltip = ({ active, payload, label, sortedResults }) => {
+ if (!active || !payload || !payload.length) return null;
+ const dataPoint = payload[0].payload;
+
+ const activeTerms = sortedResults.filter(item =>
+ item.isType2 ? (dataPoint[`${item.term}_upper`] ?? 0) > 0 : (dataPoint[item.term] ?? 0) > 0
+ );
+
+ if (activeTerms.length === 0) return null;
+
+ return (
+
+
+ Punto X: {Number(label).toFixed(3)}
+
+
+ {activeTerms.map(item => {
+ if (item.isType2) {
+ const lower = dataPoint[`${item.term}_lower`] ?? 0;
+ const upper = dataPoint[`${item.term}_upper`] ?? 0;
+ const range = Math.abs(upper - lower);
+
+ return range <= 0.001 ? (
+
+ Pertenencia: {Number(upper).toFixed(3)}
+
+ ) : (
+
+ Mínimo: {Number(lower).toFixed(3)}
+ Máximo: {Number(upper).toFixed(3)}
+
+ Incertidumbre: {Number(range).toFixed(3)}
+
+
+ );
+ }
+ return (
+
+ Pertenencia: {Number(dataPoint[item.term]).toFixed(3)}
+
+ );
+ })}
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js
new file mode 100644
index 0000000..408cdfc
--- /dev/null
+++ b/frontend/src/components/editor/finalGraph/useGraphData.js
@@ -0,0 +1,89 @@
+import { useMemo } from 'react';
+import { CHART_COLORS } from '../../../config';
+
+const interpolateY = (x, nodes) => {
+ if (!nodes || nodes.length === 0) return null;
+ const EPSILON = 1e-5;
+ const MICRO_STEP = 0.0001;
+ const firstX = nodes[0][0];
+ const lastX = nodes[nodes.length - 1][0];
+
+ if (x < firstX - MICRO_STEP - EPSILON) return null;
+ if (x > lastX + MICRO_STEP + EPSILON) return null;
+ if (x < firstX - EPSILON) return 0;
+ if (x > lastX + EPSILON) return 0;
+
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ if (Math.abs(nodes[i][0] - x) < EPSILON) return nodes[i][1];
+ }
+
+ for (let i = 0; i < nodes.length - 1; i++) {
+ const x1 = nodes[i][0];
+ const x2 = nodes[i + 1][0];
+ if (Math.abs(x2 - x1) < EPSILON) continue;
+ if (x >= x1 && x <= x2) {
+ const y1 = nodes[i][1];
+ const y2 = nodes[i + 1][1];
+ return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1);
+ }
+ }
+ return null;
+};
+
+export const useGraphData = (data) => {
+ const sortedResults = useMemo(() => {
+ const rawItems = data?.levels || data?.results || [];
+ const processed = rawItems.map((item, index) => {
+ const isType2 = !!item.lower && !!item.upper;
+ const color = CHART_COLORS[index % CHART_COLORS.length] || '#333';
+ let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`;
+
+ if (isType2) {
+ const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
+ const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
+ const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0;
+ return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal };
+ } else {
+ const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]);
+ const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0;
+ return { ...item, term: termName, isType2, nodes, color, coreVal };
+ }
+ });
+ return processed.sort((a, b) => a.coreVal - b.coreVal);
+ }, [data]);
+
+ const denseData = useMemo(() => {
+ const xSet = new Set();
+ const steps = 1000;
+ for (let i = 0; i <= steps; i++) xSet.add(Number((i / steps).toFixed(4)));
+
+ sortedResults.forEach(item => {
+ const addNodes = (nodes) => nodes.forEach(n => {
+ const x = n[0];
+ xSet.add(Number((x - 0.0001).toFixed(4)));
+ xSet.add(Number(x.toFixed(4)));
+ xSet.add(Number((x + 0.0001).toFixed(4)));
+ });
+ item.isType2 ? (addNodes(item.lowerNodes), addNodes(item.upperNodes)) : addNodes(item.nodes);
+ });
+
+ const xValues = Array.from(xSet).sort((a, b) => a - b);
+ return xValues.map(x => {
+ const point = { x };
+ sortedResults.forEach(item => {
+ if (item.isType2) {
+ const lowerRaw = interpolateY(x, item.lowerNodes);
+ const upperRaw = interpolateY(x, item.upperNodes);
+ point[`${item.term}_lower`] = lowerRaw;
+ point[`${item.term}_upper`] = upperRaw;
+ point[`${item.term}_range`] = (lowerRaw === null && upperRaw === null) ? null : [lowerRaw ?? 0, upperRaw ?? 0];
+ } else {
+ point[item.term] = interpolateY(x, item.nodes);
+ }
+ });
+ return point;
+ });
+ }, [sortedResults]);
+
+ return { sortedResults, denseData };
+};
\ No newline at end of file
diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx
index 10529fa..e7067d9 100644
--- a/frontend/src/pages/History.jsx
+++ b/frontend/src/pages/History.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { getUserHistory, deleteHistoryItem } from '../services/docService';
import Step3FinalGraph from '../components/editor/Step3FinalGraph';
@@ -116,12 +116,21 @@ export default function History() {
- {/* Contenido Desplegable (La gráfica) */}
- {isExpanded && (
-
-
+ {/* Contenido Desplegable (La gráfica)*/}
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
- )}
+
+
);
})}