feat: manual membership evaluator implemented reusing existing logic
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useEffect, memo } from 'react';
|
|||||||
import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
import { useGraphData } from './finalGraph/useGraphData';
|
import { useGraphData } from './finalGraph/useGraphData';
|
||||||
import { GraphTooltip } from './finalGraph/GraphTooltip';
|
import { GraphTooltip } from './finalGraph/GraphTooltip';
|
||||||
|
import { MembershipEvaluator } from './finalGraph/MembershipEvaluator';
|
||||||
|
|
||||||
const Step3FinalGraph = memo(({ data, criterionName }) => {
|
const Step3FinalGraph = memo(({ data, criterionName }) => {
|
||||||
const { sortedResults, denseData } = useGraphData(data);
|
const { sortedResults, denseData } = useGraphData(data);
|
||||||
@@ -19,14 +20,14 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-[550px] bg-white p-6 rounded-2xl shadow-sm border border-slate-200 flex flex-col final-graph-container relative">
|
<div className="w-full bg-white p-6 rounded-2xl shadow-sm border border-slate-200 flex flex-col final-graph-container relative">
|
||||||
<style>{`.final-graph-container svg * { clip-path: none !important; }`}</style>
|
<style>{`.final-graph-container svg * { clip-path: none !important; }`}</style>
|
||||||
|
|
||||||
<h3 className="text-xl font-bold mb-4 text-center text-slate-800 uppercase">
|
<h3 className="text-xl font-bold mb-4 text-center text-slate-800 uppercase shrink-0">
|
||||||
{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}
|
{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex-1 w-full min-h-[400px] relative">
|
<div className="w-full h-[400px] relative shrink-0">
|
||||||
|
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
@@ -35,7 +36,6 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Gráfica */}
|
|
||||||
<div className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${isReady ? 'opacity-100' : 'opacity-0'}`}>
|
<div className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${isReady ? 'opacity-100' : 'opacity-0'}`}>
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
@@ -50,13 +50,11 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
|
|||||||
domain={[0, 1]} tickCount={6} tickFormatter={(val) => Number(val.toFixed(2))}
|
domain={[0, 1]} tickCount={6} tickFormatter={(val) => Number(val.toFixed(2))}
|
||||||
tick={{ fill: '#475569', fontSize: 14 }}
|
tick={{ fill: '#475569', fontSize: 14 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={<GraphTooltip sortedResults={sortedResults} />}
|
content={<GraphTooltip sortedResults={sortedResults} />}
|
||||||
cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }}
|
cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{sortedResults.map((item) => (
|
{sortedResults.map((item) => (
|
||||||
<React.Fragment key={item.term}>
|
<React.Fragment key={item.term}>
|
||||||
{item.isType2 ? (
|
{item.isType2 ? (
|
||||||
@@ -76,8 +74,8 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Leyenda */}
|
{/* Legend */}
|
||||||
<div className="flex flex-wrap justify-center gap-x-8 gap-y-3 mt-6 pb-2 relative z-10">
|
<div className="flex flex-wrap justify-center gap-x-8 gap-y-3 mt-5 relative z-10 shrink-0">
|
||||||
{sortedResults.map((item) => (
|
{sortedResults.map((item) => (
|
||||||
<div key={`legend-${item.term}`} className="flex items-center gap-2">
|
<div key={`legend-${item.term}`} className="flex items-center gap-2">
|
||||||
<span className="w-3.5 h-3.5 rounded-full shadow-sm" style={{ backgroundColor: item.color }} />
|
<span className="w-3.5 h-3.5 rounded-full shadow-sm" style={{ backgroundColor: item.color }} />
|
||||||
@@ -85,6 +83,13 @@ const Step3FinalGraph = memo(({ data, criterionName }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Evaluador Manual */}
|
||||||
|
{isReady && sortedResults.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-slate-100 relative z-10">
|
||||||
|
<MembershipEvaluator sortedResults={sortedResults} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col text-xs font-medium bg-slate-50 px-3 py-2 rounded-xl border border-slate-100 shrink-0">
|
||||||
|
<span className="uppercase font-black mb-1 tracking-wide leading-none" style={{ color: result.color }}>
|
||||||
|
{result.term}
|
||||||
|
</span>
|
||||||
|
{isSimple ? (
|
||||||
|
<span className="text-slate-700 flex justify-between gap-2">
|
||||||
|
Pertenencia: <b>{displayY.toFixed(3)}</b>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-slate-600 flex justify-between gap-2">
|
||||||
|
Inferior: <b>{result.lower.toFixed(3)}</b>
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-600 flex justify-between gap-2 mt-0.5">
|
||||||
|
Superior: <b>{result.upper.toFixed(3)}</b>
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-500 font-bold mt-1 pt-1 border-t border-slate-200 flex justify-between gap-2">
|
||||||
|
Incertidumbre: <span>{uncertainty.toFixed(3)}</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
{/* Input group */}
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<label
|
||||||
|
htmlFor="eval-x-input"
|
||||||
|
className="text-xs font-bold uppercase tracking-wider text-slate-400 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Evaluar X:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="eval-x-input"
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="ej. 0.35"
|
||||||
|
className="w-28 px-2.5 py-1.5 border border-slate-300 rounded-lg text-sm font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400 transition-shadow placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
<div className="flex flex-row flex-wrap gap-3 items-center">
|
||||||
|
{trimmed === '' && (
|
||||||
|
<span className="text-xs text-slate-400 italic">
|
||||||
|
Introduce un valor para obtener el grado de pertenencia.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{trimmed !== '' && !isValidNumber && (
|
||||||
|
<span className="text-xs text-red-400 italic">Valor no válido.</span>
|
||||||
|
)}
|
||||||
|
{isValidNumber && activeResults.length === 0 && (
|
||||||
|
<span className="text-xs text-slate-400 italic">
|
||||||
|
Ningún término activo en X = {xNum.toFixed(4)}.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isValidNumber && activeResults.map(r => (
|
||||||
|
<TermResult key={r.term} result={r} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { CHART_COLORS } from '../../../config';
|
import { CHART_COLORS } from '../../../config';
|
||||||
|
|
||||||
const interpolateY = (x, nodes) => {
|
export const interpolateY = (x, nodes) => {
|
||||||
if (!nodes || nodes.length === 0) return null;
|
if (!nodes || nodes.length === 0) return null;
|
||||||
const EPSILON = 1e-5;
|
const EPSILON = 1e-5;
|
||||||
const MICRO_STEP = 0.0001;
|
const MICRO_STEP = 0.0001;
|
||||||
|
|||||||
Reference in New Issue
Block a user