add: añadir la gráfica final con la zona de incertidumbre marcada por el usuario en el paso 2
refactor: mejorar el manejo de errores para hacerlo agradable al usuario, cambiando alerts por mensajes en la interfaz. fixed: mejorar orden de los sliders.
This commit is contained in:
@@ -11,7 +11,8 @@ export default function Step2FuzzyModeling({
|
||||
handleFinalSubmit,
|
||||
onBack,
|
||||
subscales,
|
||||
onOpenSubscale
|
||||
onOpenSubscale,
|
||||
submitError
|
||||
}) {
|
||||
const scaleKeys = Object.keys(baseScale);
|
||||
|
||||
@@ -51,6 +52,20 @@ export default function Step2FuzzyModeling({
|
||||
colors={CHART_COLORS}
|
||||
/>
|
||||
|
||||
{submitError && (
|
||||
<div className="bg-red-50 mb-6 border-red-500 p-4 rounded-xl shadow-sm animate-fade-in mx-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-red-500 text-xl mr-3">⚠️</span>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-red-800">Error de validación al generar la gráfica</h3>
|
||||
<div className="mt-1 text-sm text-red-700 whitespace-pre-line font-medium">
|
||||
{submitError}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Controls
|
||||
selectedTerm={selectedTerm}
|
||||
currentMf={mfDefinitions[selectedTerm]}
|
||||
|
||||
@@ -1,78 +1,84 @@
|
||||
import { useMemo } from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { CHART_COLORS } from '../../config';
|
||||
|
||||
const Step3FinalGraph = ({ data }) => {
|
||||
const Step3FinalGraph = ({ data, criterionName }) => {
|
||||
const sortedResults = useMemo(() => {
|
||||
if (!data || !data.results) return [];
|
||||
const rawItems = data?.levels || data?.results || [];
|
||||
|
||||
const withPermanentColors = data.results.map((item, index) => ({
|
||||
...item,
|
||||
color: CHART_COLORS[index % CHART_COLORS.length]
|
||||
}));
|
||||
const processed = rawItems.map((item, index) => {
|
||||
const isType2 = !!item.lower && !!item.upper;
|
||||
const color = CHART_COLORS[index % CHART_COLORS.length] || '#333';
|
||||
|
||||
return withPermanentColors.sort((a, b) => {
|
||||
const coreA = Array.isArray(a.core) ? Number(a.core[0]) : 0;
|
||||
const coreB = Array.isArray(b.core) ? Number(b.core[0]) : 0;
|
||||
return coreA - coreB;
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
return { ...item, term: termName, isType2, lineData, color, coreVal };
|
||||
});
|
||||
|
||||
return processed.sort((a, b) => a.coreVal - b.coreVal);
|
||||
}, [data]);
|
||||
|
||||
if (!data || !data.results) {
|
||||
if (!data || (!data.levels && !data.results)) {
|
||||
return <p className="text-center mt-10 text-slate-500">Cargando gráfico final...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-[550px] mt-2 bg-white p-6 rounded-2xl shadow-sm border border-slate-200 flex flex-col">
|
||||
<h3 className="text-2xl font-bold mb-4 text-center text-slate-800">Espectro Difuso Final</h3>
|
||||
|
||||
{/* Título */}
|
||||
<h3 className="text-2xl font-bold mb-4 text-center text-slate-800">
|
||||
{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}
|
||||
</h3>
|
||||
|
||||
{/* Gráfica */}
|
||||
<div className="flex-1 w-full min-h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart margin={{ top: 10, right: 30, left: 10, bottom: 10 }}>
|
||||
<ComposedChart margin={{ top: 10, right: 30, left: 10, bottom: 10 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" opacity={0.5} vertical={false} />
|
||||
<XAxis
|
||||
dataKey="x"
|
||||
type="number"
|
||||
domain={[0, 1]}
|
||||
tickCount={11}
|
||||
tick={{ fill: '#475569', fontWeight: 600, fontSize: 14 }}
|
||||
allowDuplicatedCategory={false}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 1]}
|
||||
tickCount={6}
|
||||
tick={{ fill: '#475569', fontSize: 14 }}
|
||||
dataKey="x" type="number" domain={[0, 1]} tickCount={11}
|
||||
tick={{ fill: '#475569', fontWeight: 600, fontSize: 14 }} allowDuplicatedCategory={false}
|
||||
/>
|
||||
<YAxis domain={[0, 1]} tickCount={6} tick={{ fill: '#475569', fontSize: 14 }} />
|
||||
<Tooltip
|
||||
formatter={(value, name) => [Number(value).toFixed(3), name]}
|
||||
formatter={(value, name) => 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)' }}
|
||||
/>
|
||||
|
||||
{sortedResults.map((item) => {
|
||||
const lineData = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(node => ({
|
||||
x: Number(node[0]),
|
||||
y: Number(node[1])
|
||||
}));
|
||||
|
||||
if (item.isType2) {
|
||||
return (
|
||||
<Line
|
||||
key={item.term}
|
||||
data={lineData}
|
||||
type="linear"
|
||||
dataKey="y"
|
||||
name={item.term.toUpperCase()}
|
||||
stroke={item.color}
|
||||
strokeWidth={4}
|
||||
dot={{ r: 5, strokeWidth: 2, fill: '#fff' }}
|
||||
activeDot={{ r: 8 }}
|
||||
isAnimationActive={true}
|
||||
animationDuration={1500}
|
||||
/>
|
||||
<React.Fragment key={item.term}>
|
||||
<Area data={item.lineData} type="linear" dataKey="range" name={`${item.term.toUpperCase()} (Incertidumbre)`} fill={item.color} fillOpacity={0.25} stroke="none" isAnimationActive={true} animationDuration={1500} />
|
||||
<Line data={item.lineData} type="linear" dataKey="upperY" name={`${item.term.toUpperCase()} (Máx)`} stroke={item.color} strokeWidth={2} strokeDasharray="5 5" dot={false} activeDot={{ r: 6 }} isAnimationActive={true} animationDuration={1500} />
|
||||
<Line data={item.lineData} type="linear" dataKey="lowerY" name={`${item.term.toUpperCase()} (Mín)`} stroke={item.color} strokeWidth={3} dot={{ r: 4, strokeWidth: 2, fill: '#fff' }} activeDot={{ r: 8 }} isAnimationActive={true} animationDuration={1500} />
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return <Line key={item.term} data={item.lineData} type="linear" dataKey="y" name={item.term.toUpperCase()} stroke={item.color} strokeWidth={4} dot={{ r: 5, strokeWidth: 2, fill: '#fff' }} activeDot={{ r: 8 }} isAnimationActive={true} animationDuration={1500} />;
|
||||
}
|
||||
})}
|
||||
</LineChart>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -80,22 +86,11 @@ const Step3FinalGraph = ({ data }) => {
|
||||
<div className="flex flex-wrap justify-center gap-x-8 gap-y-3 mt-6 pb-2">
|
||||
{sortedResults.map((item) => (
|
||||
<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="text-sm font-medium uppercase tracking-wide"
|
||||
style={{ color: item.color }}
|
||||
>
|
||||
{item.term}
|
||||
</span>
|
||||
<span className="w-3.5 h-3.5 rounded-full shadow-sm" style={{ backgroundColor: item.color }} />
|
||||
<span className="text-sm font-medium uppercase tracking-wide" style={{ color: item.color }}>{item.term}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function Controls({
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Lado izquierdo (Pendiente ascendente) */}
|
||||
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
||||
@@ -37,7 +36,6 @@ export default function Controls({
|
||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.coreStart} onChange={(e) => updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
|
||||
</div>
|
||||
|
||||
{/* Botón subescala izquierda */}
|
||||
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
||||
<button
|
||||
onClick={() => onOpenSubscale(selectedTerm, 'left', leftSubscale)}
|
||||
@@ -48,22 +46,20 @@ export default function Controls({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lado derecho (Pendiente descendente) */}
|
||||
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||
<div>
|
||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
||||
<span>Fin del Núcleo (Punto superior)</span><span style={{ color: selectedColor }}>{currentMf.coreEnd.toFixed(3)}</span>
|
||||
</label>
|
||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.coreEnd} onChange={(e) => updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
||||
<span>Fin del Soporte (Punto inferior)</span><span style={{ color: selectedColor }}>{currentMf.supportEnd.toFixed(3)}</span>
|
||||
</label>
|
||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.supportEnd} onChange={(e) => updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
||||
<span>Fin del Núcleo (Punto superior)</span><span style={{ color: selectedColor }}>{currentMf.coreEnd.toFixed(3)}</span>
|
||||
</label>
|
||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.coreEnd} onChange={(e) => updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
|
||||
</div>
|
||||
|
||||
{/* Botón subescala derecha */}
|
||||
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
||||
<button
|
||||
onClick={() => onOpenSubscale(selectedTerm, 'right', rightSubscale)}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function DocEditor() {
|
||||
|
||||
// ESTADO: FASE 3
|
||||
const [finalResult, setFinalResult] = useState(null);
|
||||
const [submitError, setSubmitError] = useState(null);
|
||||
|
||||
// MANEJADORES: FASE 1
|
||||
const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); };
|
||||
@@ -119,6 +120,7 @@ export default function DocEditor() {
|
||||
|
||||
// Petición para el endpoint "build"
|
||||
const handleFinalSubmit = async () => {
|
||||
setSubmitError(null);
|
||||
const scaleKeys = Object.keys(baseScale);
|
||||
|
||||
const payload = {
|
||||
@@ -157,14 +159,35 @@ export default function DocEditor() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await buildFuzzyGraph(payload);
|
||||
console.log("RESPUESTA DEL BACKEND:", result);
|
||||
|
||||
setFinalResult(result);
|
||||
setStep(3);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("Error del servidor: \n" + JSON.stringify(error, null, 2));
|
||||
console.error("Error capturado:", error);
|
||||
let friendlyMessage = "Ocurrió un error de validación.";
|
||||
|
||||
if (Array.isArray(error)) {
|
||||
friendlyMessage = error.map(err => {
|
||||
let msg = err.msg.replace("Value error, ", "");
|
||||
|
||||
// Traducción de mensajes del backend
|
||||
msg = msg.replace("a < b", "el 'Inicio del Soporte' no puede ser mayor que el 'Inicio del Núcleo'");
|
||||
msg = msg.replace("b <= c", "el 'Inicio del Núcleo' no puede ser mayor que el 'Fin del Núcleo'");
|
||||
msg = msg.replace("c < d", "el 'Fin del Núcleo' no puede ser mayor que el 'Fin del Soporte'");
|
||||
msg = msg.replace("El soporte debe cumplir", "Revisa los valores:");
|
||||
msg = msg.replace("El núcleo debe cumplir", "Revisa los valores:");
|
||||
|
||||
if (err.loc && err.loc.includes("levels")) {
|
||||
const levelIndex = err.loc[err.loc.indexOf("levels") + 1];
|
||||
const termName = scaleKeys[levelIndex] || `Nivel ${Number(levelIndex) + 1}`;
|
||||
return `• En la etiqueta "${termName}": ${msg}`;
|
||||
}
|
||||
return `• ${msg}`;
|
||||
}).join("\n");
|
||||
} else if (typeof error === 'string') {
|
||||
friendlyMessage = error;
|
||||
}
|
||||
|
||||
setSubmitError(friendlyMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -191,12 +214,13 @@ export default function DocEditor() {
|
||||
onBack={() => setStep(1)}
|
||||
subscales={subscales}
|
||||
onOpenSubscale={handleOpenSubscale}
|
||||
submitError={submitError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 3 && finalResult && (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<Step3FinalGraph data={finalResult} />
|
||||
<Step3FinalGraph data={finalResult} criterionName={criterionName} />
|
||||
|
||||
<button
|
||||
onClick={() => console.log("Lógica para guardar")}
|
||||
|
||||
@@ -12,12 +12,10 @@ export const calculateValueFunction = async (payload) => {
|
||||
|
||||
export const buildFuzzyGraph = async (payload) => {
|
||||
try {
|
||||
const response = await api.post('/criteria/doc-mf/build', payload);
|
||||
const response = await api.post('/criteria/doc-it2mf/build', payload);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error building fuzzy graph:', error);
|
||||
throw error.response?.data?.detail || error.message;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user