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 ? ( + + ) : ( +
+ )}
- )} +
+
); })}