-
+
+
+
{!isReady && (
@@ -35,28 +36,25 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
)}
- {/* Gráfica */}
{isReady && (
-
- Number(val.toFixed(2))}
- tick={{ fill: '#475569', fontSize: 14 }}
+ Number(val.toFixed(2))}
+ tick={{ fill: '#475569', fontSize: 14 }}
/>
-
- }
- cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }}
+ }
+ cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }}
isAnimationActive={false}
/>
-
{sortedResults.map((item) => (
{item.isType2 ? (
@@ -76,8 +74,8 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
- {/* Leyenda */}
-
+ {/* Legend */}
+
{sortedResults.map((item) => (
@@ -85,6 +83,13 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
))}
+
+ {/* Evaluador Manual */}
+ {isReady && sortedResults.length > 0 && (
+
+
+
+ )}
);
});
diff --git a/frontend/src/components/editor/finalGraph/MembershipEvaluator.jsx b/frontend/src/components/editor/finalGraph/MembershipEvaluator.jsx
new file mode 100644
index 0000000..c41f75e
--- /dev/null
+++ b/frontend/src/components/editor/finalGraph/MembershipEvaluator.jsx
@@ -0,0 +1,115 @@
+import { useState } from 'react';
+import { interpolateY } from './useGraphData';
+
+const evaluateMembership = (x, sortedResults) => {
+ if (!sortedResults || sortedResults.length === 0) return [];
+ return sortedResults.map(item => {
+ if (item.isType2) {
+ const lower = interpolateY(x, item.lowerNodes) ?? 0;
+ const upper = interpolateY(x, item.upperNodes) ?? 0;
+ return { term: item.term, color: item.color, isType2: true, lower, upper };
+ }
+ const y = interpolateY(x, item.nodes) ?? 0;
+ return { term: item.term, color: item.color, isType2: false, y };
+ });
+};
+
+const filterActive = (results) =>
+ results.filter(r => r.isType2 ? r.upper > 0 : r.y > 0);
+
+const TermResult = ({ result }) => {
+ const uncertainty = result.isType2 ? Math.abs(result.upper - result.lower) : 0;
+ const isSimple = !result.isType2 || uncertainty <= 0.001;
+ const displayY = result.isType2 ? result.upper : result.y;
+
+ return (
+
+
+ {result.term}
+
+ {isSimple ? (
+
+ Pertenencia: {displayY.toFixed(3)}
+
+ ) : (
+ <>
+
+ Mínimo: {result.lower.toFixed(3)}
+
+
+ Máximo: {result.upper.toFixed(3)}
+
+
+ Incertidumbre: {uncertainty.toFixed(3)}
+
+ >
+ )}
+
+ );
+};
+
+export const MembershipEvaluator = ({ sortedResults }) => {
+ const [inputValue, setInputValue] = useState('');
+
+ const handleChange = (e) => {
+ const raw = e.target.value;
+ if (raw === '') { setInputValue(''); return; }
+ const dotIdx = raw.indexOf('.');
+ if (dotIdx !== -1 && raw.length - dotIdx - 1 > 4) {
+ setInputValue(raw.slice(0, dotIdx + 5));
+ } else {
+ setInputValue(raw);
+ }
+ };
+
+ const trimmed = inputValue.trim();
+ const xNum = trimmed === '' ? null : parseFloat(trimmed);
+ const isValidNumber = xNum !== null && !isNaN(xNum) && isFinite(xNum);
+
+ const activeResults = isValidNumber
+ ? filterActive(evaluateMembership(xNum, sortedResults))
+ : null;
+
+ return (
+
+ {/* Input group */}
+
+
+
+
+
+ {/* Results */}
+
+ {trimmed === '' && (
+
+ Introduce un valor para obtener el grado de pertenencia.
+
+ )}
+ {trimmed !== '' && !isValidNumber && (
+ Valor no válido.
+ )}
+ {isValidNumber && activeResults.length === 0 && (
+
+ Ningún término activo en X = {xNum.toFixed(4)}.
+
+ )}
+ {isValidNumber && activeResults.map(r => (
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js
index 408cdfc..965c133 100644
--- a/frontend/src/components/editor/finalGraph/useGraphData.js
+++ b/frontend/src/components/editor/finalGraph/useGraphData.js
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { CHART_COLORS } from '../../../config';
-const interpolateY = (x, nodes) => {
+export const interpolateY = (x, nodes) => {
if (!nodes || nodes.length === 0) return null;
const EPSILON = 1e-5;
const MICRO_STEP = 0.0001;