IDKHowToCodeFr/tinyml-backend
1
1"use client";2 3import { motion } from "framer-motion";4import { useEffect, useState } from "react";5import { Loader2 } from "lucide-react";6 7export default function ShapChart({ patientData }: { patientData: any }) {8 const [shapData, setShapData] = useState<{feature_names: string[], shap_values: number[]} | null>(null);9 const [loading, setLoading] = useState(false);10 11 useEffect(() => {12 if (!patientData) return;13 14 const fetchExplain = async () => {15 setLoading(true);16 try {17 const res = await fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/explain", {18 method: "POST",19 headers: { "Content-Type": "application/json" },20 body: JSON.stringify(patientData)21 });22 const data = await res.json();23 if (data.shap_values) {24 setShapData(data);25 }26 } catch (err) {27 console.error("SHAP fetch error", err);28 } finally {29 setLoading(false);30 }31 };32 33 const timeout = setTimeout(fetchExplain, 2500);34 return () => clearTimeout(timeout);35 }, [patientData]);36 37 if (!shapData) {38 return (39 <div className="h-full w-full flex flex-col items-center justify-center gap-3">40 {loading ? (41 <>42 <Loader2 className="animate-spin text-ink" size={20} />43 <span className="text-slate text-[12px] font-bold uppercase tracking-[0.1em]">COMPUTING_SHAP_VECTORS...</span>44 </>45 ) : (46 <span className="text-slate text-[12px] font-bold uppercase tracking-[0.1em]">AWAITING_TELEMETRY...</span>47 )}48 </div>49 );50 }51 52 // Sort by absolute magnitude of SHAP value53 const items = shapData.feature_names.map((name, i) => ({54 name: name.toUpperCase(),55 value: shapData.shap_values[i],56 abs: Math.abs(shapData.shap_values[i])57 })).sort((a, b) => b.abs - a.abs); // show ALL features58 59 const maxAbs = Math.max(...items.map(i => i.abs), 0.1);60 61 return (62 <div className="flex flex-col gap-4 w-full h-full justify-center">63 {items.map((item, idx) => {64 const isPositive = item.value > 0;65 const widthPercent = (item.abs / maxAbs) * 50;66 67 return (68 <div key={item.name} className="flex flex-col gap-2">69 <div className="flex justify-between text-[11px] font-bold tracking-[0.1em]">70 <span className="text-ink">{item.name}</span>71 <span className={isPositive ? "text-hazard" : "text-ink"}>72 {item.value > 0 ? "+" : ""}{item.value.toFixed(3)}73 </span>74 </div>75 76 {/* Brutalist Chart Bar */}77 <div className="relative h-4 w-full bg-white border-2 border-ink flex items-center">78 {/* Midline (zero axis) */}79 <div className="absolute left-1/2 top-[-2px] bottom-[-2px] w-0.5 bg-ink z-10" />80 81 {/* Animated Value Bar */}82 <motion.div 83 initial={{ width: 0, opacity: 0 }}84 animate={{ 85 width: `${widthPercent}%`,86 opacity: 1,87 }}88 transition={{ type: "spring", stiffness: 60, damping: 15 }}89 className={`absolute h-full ${isPositive ? 'bg-hazard border-x-2 border-hazard' : 'bg-ink border-x-2 border-ink'}`}90 style={{91 left: isPositive ? '50%' : 'auto',92 right: !isPositive ? '50%' : 'auto'93 }}94 />95 </div>96 </div>97 );98 })}99 </div>100 );101}102 