From d3e44a624977a4666188eef32660a87bd2460aca Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 9 Apr 2026 13:00:46 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20optimizar=20la=20generaci=C3=B3n=20?= =?UTF-8?q?de=20datos=20en=20el=20gr=C3=A1fico=20final,=20mostrando=20el?= =?UTF-8?q?=20grado=20de=20pertenencia=20en=20cualquier=20punto=20de=20la?= =?UTF-8?q?=20gr=C3=A1fica.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 226 +++++++++++++++--- 1 file changed, 192 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 99884dd..fd6edf6 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -2,87 +2,245 @@ import React, { useMemo } from 'react'; import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { CHART_COLORS } from '../../config'; +// 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 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 lineData = []; - let coreVal = 0; let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; if (isType2) { - const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])]; - const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])]; - - lineData = lowerNodes.map((lNode, i) => { - const uNode = upperNodes[i]; - const lowerY = Number(lNode[1]); - const upperY = Number(uNode ? uNode[1] : lNode[1]); - return { x: Number(lNode[0]), lowerY, upperY, range: [lowerY, upperY] }; - }); - coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + 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 || [])]; - lineData = nodes.map(node => ({ x: Number(node[0]), y: Number(node[1]) })); - coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + 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 { ...item, term: termName, isType2, lineData, 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; + }; + if (!data || (!data.levels && !data.results)) { return

Cargando gráfico final...

; } return ( -
+
+ + - {/* Título */}

{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}

- {/* Gráfica */}
- + - - Array.isArray(value) ? [`[${Number(value[0]).toFixed(3)}, ${Number(value[1]).toFixed(3)}]`, name] : [Number(value).toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} /> + + {sortedResults.map((item) => { if (item.isType2) { return ( - - - + + + ); } else { - return ; + return ; } })}
- {/* Leyenda */}
{sortedResults.map((item) => (