Create templates/index.html
Browse files- templates/index.html +861 -0
templates/index.html
ADDED
@@ -0,0 +1,861 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Healthcare Risk Prediction Dashboard</title>
|
7 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" rel="stylesheet">
|
9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.18.2/plotly.min.js"></script>
|
10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
11 |
+
<style>
|
12 |
+
/* Custom Styles */
|
13 |
+
:root {
|
14 |
+
--brand-blue: #0073e6;
|
15 |
+
--brand-dark-blue: #00437c;
|
16 |
+
--text-primary: #1f2937; /* gray-800 */
|
17 |
+
--text-secondary: #4b5563; /* gray-600 */
|
18 |
+
--border-color: #e5e7eb; /* gray-200 */
|
19 |
+
--card-bg: #ffffff;
|
20 |
+
--body-bg: #f9fafb; /* gray-50 */
|
21 |
+
}
|
22 |
+
body { background-color: var(--body-bg); color: var(--text-primary); font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
|
23 |
+
.gradient-bg { background: linear-gradient(135deg, var(--brand-blue) 0%, var(--brand-dark-blue) 100%); }
|
24 |
+
.card { background-color: var(--card-bg); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.06); border: 1px solid var(--border-color); border-radius: 0.5rem; /* rounded-lg */ transition: all 0.3s ease-in-out; }
|
25 |
+
.card:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.07), 0 4px 6px -2px rgba(0, 0, 0, 0.05); transform: translateY(-2px) scale(1.01); }
|
26 |
+
.feature-row:nth-child(odd) { background-color: #f9fafb; /* gray-50 */ }
|
27 |
+
.feature-row:hover { background-color: #eff6ff; /* blue-50 */ }
|
28 |
+
/* Recommendation Card Styles */
|
29 |
+
.recommendation-card { border-left-width: 4px; border-left-style: solid; transition: all 0.2s ease-out; }
|
30 |
+
.recommendation-card.critical { border-left-color: #ef4444; } /* red-500 */
|
31 |
+
.recommendation-card.high { border-left-color: #f97316; } /* orange-500 */
|
32 |
+
.recommendation-card.medium { border-left-color: #eab308; } /* yellow-500 */
|
33 |
+
.recommendation-card.standard { border-left-color: #22c55e; } /* green-500 */
|
34 |
+
.recommendation-card:hover { background-color: #f8fafc; /* cool-gray-50 */ }
|
35 |
+
/* Priority Indicator Styles */
|
36 |
+
.priority-indicator { border-radius: 9999px; height: 1.5rem; width: 1.5rem; display: flex; align-items: center; justify-content: center; margin-right: 0.75rem; flex-shrink: 0; transition: transform 0.2s ease; }
|
37 |
+
.recommendation-card:hover .priority-indicator { transform: scale(1.1); }
|
38 |
+
.priority-indicator.critical { background-color: #fee2e2; } /* red-100 */
|
39 |
+
.priority-indicator.high { background-color: #ffedd5; } /* orange-100 */
|
40 |
+
.priority-indicator.medium { background-color: #fef9c3; } /* yellow-100 */
|
41 |
+
.priority-indicator.standard { background-color: #dcfce7; } /* green-100 */
|
42 |
+
.priority-indicator i { font-size: 0.8rem; }
|
43 |
+
.priority-indicator.critical i { color: #dc2626; } /* red-600 */
|
44 |
+
.priority-indicator.high i { color: #ea580c; } /* orange-600 */
|
45 |
+
.priority-indicator.medium i { color: #ca8a04; } /* yellow-600 */
|
46 |
+
.priority-indicator.standard i { color: #16a34a; } /* green-600 */
|
47 |
+
/* Risk Badge Colors */
|
48 |
+
.risk-badge.green { background-color: #10b981; color: white; } /* emerald-500 */
|
49 |
+
.risk-badge.yellow { background-color: #f59e0b; color: white; } /* amber-500 */
|
50 |
+
.risk-badge.red { background-color: #ef4444; color: white; } /* red-500 */
|
51 |
+
.risk-badge.gray { background-color: #6b7280; color: white; } /* gray-500 */
|
52 |
+
/* SHAP Bar Styles */
|
53 |
+
.shap-bar-container { position: relative; width: 100%; background-color: #e5e7eb; border-radius: 0.25rem; height: 0.75rem; margin-top: 0.25rem; margin-bottom: 0.1rem; overflow: hidden; }
|
54 |
+
.shap-bar { position: absolute; top: 0; left: 0; height: 100%; border-radius: 0.25rem; transition: width 0.4s ease-out;}
|
55 |
+
.shap-positive { background-color: #fca5a5; } /* red-300 */
|
56 |
+
.shap-negative { background-color: #86efac; } /* green-300 */
|
57 |
+
/* Loading Spinner */
|
58 |
+
.spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-left-color: var(--brand-blue); border-radius: 50%; width: 36px; height: 36px; animation: spin 1.2s linear infinite; }
|
59 |
+
.spinner-pulse { animation: spin 1.2s linear infinite, pulse 1.5s ease-in-out infinite alternate; }
|
60 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
61 |
+
@keyframes pulse { from { opacity: 0.7; } to { opacity: 1; } }
|
62 |
+
/* Tooltip Styles */
|
63 |
+
.tooltip { position: relative; display: inline-block; cursor: help; }
|
64 |
+
.tooltip .tooltip-text { visibility: hidden; width: 240px; background-color: #374151; color: #fff; text-align: center; border-radius: 6px; padding: 8px 10px; position: absolute; z-index: 10; bottom: 130%; left: 50%; margin-left: -120px; opacity: 0; transition: opacity 0.3s, visibility 0s 0.3s; font-size: 0.75rem; line-height: 1.2; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
|
65 |
+
.tooltip:hover .tooltip-text { visibility: visible; opacity: 1; transition: opacity 0.3s; }
|
66 |
+
/* Result Section Initial State & Animation */
|
67 |
+
#result-section { display: none; animation: fadeIn 0.8s ease-out forwards; }
|
68 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
69 |
+
/* Tab Styling */
|
70 |
+
.tab-btn, .factor-tab-btn { padding: 0.75rem 1.5rem; font-weight: 500; color: var(--text-secondary); border-bottom: 3px solid transparent; transition: color 0.2s ease, border-color 0.3s ease; white-space: nowrap; }
|
71 |
+
.tab-btn:hover, .factor-tab-btn:hover { color: var(--brand-blue); }
|
72 |
+
.tab-btn.active, .factor-tab-btn.active { color: var(--brand-blue); border-bottom-color: var(--brand-blue); font-weight: 600; }
|
73 |
+
/* Smaller padding for factor tabs */
|
74 |
+
.factor-tab-btn { padding: 0.5rem 1rem; }
|
75 |
+
/* Chart Containers */
|
76 |
+
.chart-container { width: 100%; /* Ensure container takes full width */ /* resize: vertical; Optional */ overflow: hidden; /* Helps contain chart */ }
|
77 |
+
/* Plotly specific adjustments if needed */
|
78 |
+
.plotly-graph-div { min-width: 100%; min-height: 100%; }
|
79 |
+
</style>
|
80 |
+
</head>
|
81 |
+
<body class="bg-gray-50 min-h-screen">
|
82 |
+
<!-- Header -->
|
83 |
+
<header class="gradient-bg text-white shadow-lg sticky top-0 z-50">
|
84 |
+
<div class="container mx-auto px-4 py-4">
|
85 |
+
<h1 class="text-2xl sm:text-3xl font-bold">Healthcare Risk Prediction with XAI</h1>
|
86 |
+
<p class="opacity-90 text-xs sm:text-sm mt-1">Explainable AI Enabled Risk Assessment Dashboard</p>
|
87 |
+
</div>
|
88 |
+
</header>
|
89 |
+
<main class="container mx-auto px-4 py-6">
|
90 |
+
<!-- Input Form Card -->
|
91 |
+
<section id="input-section" class="card bg-white rounded-lg p-5 sm:p-6 mb-6">
|
92 |
+
<h2 class="text-xl font-bold mb-4 text-gray-800 border-b border-gray-200 pb-2">Patient Information Input</h2>
|
93 |
+
<form id="patient-form">
|
94 |
+
<div id="form-fields" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4">
|
95 |
+
<!-- Form fields dynamically generated -->
|
96 |
+
<p class="text-gray-500 italic">Loading form fields...</p>
|
97 |
+
</div>
|
98 |
+
<div class="mt-6 pt-4 border-t border-gray-200 flex items-center gap-4 flex-wrap">
|
99 |
+
<button type="submit" class="gradient-bg text-white px-5 py-2 sm:px-6 sm:py-2.5 rounded-md hover:opacity-90 transition-all duration-300 ease-in-out shadow hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 transform hover:-translate-y-0.5">
|
100 |
+
<i class="fas fa-chart-line mr-2"></i>Analyze Patient Risk
|
101 |
+
</button>
|
102 |
+
<button type="button" id="reset-defaults-btn" class="text-sm text-gray-600 hover:text-blue-600 underline transition-colors duration-200">
|
103 |
+
Reset to Defaults
|
104 |
+
</button>
|
105 |
+
</div>
|
106 |
+
</form>
|
107 |
+
</section>
|
108 |
+
<!-- Loading Spinner -->
|
109 |
+
<div id="loading" class="hidden flex justify-center items-center my-10 py-8">
|
110 |
+
<div class="spinner spinner-pulse"></div>
|
111 |
+
<span class="ml-4 text-lg text-gray-600 font-medium">Analyzing patient data... Please wait.</span>
|
112 |
+
</div>
|
113 |
+
<!-- Error Message Area -->
|
114 |
+
<div id="error-message" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded relative mb-6 shadow-md" role="alert">
|
115 |
+
<strong class="font-bold mr-2"><i class="fas fa-exclamation-triangle mr-1"></i>Error:</strong>
|
116 |
+
<span class="block sm:inline" id="error-text"></span>
|
117 |
+
</div>
|
118 |
+
<!-- Results Section -->
|
119 |
+
<section id="result-section">
|
120 |
+
<!-- Risk Overview Cards -->
|
121 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
122 |
+
<!-- Non-Adherence Risk Card -->
|
123 |
+
<div class="card bg-white rounded-lg p-5 flex flex-col">
|
124 |
+
<div class="flex items-center justify-between mb-3">
|
125 |
+
<h2 class="text-lg font-bold text-gray-800">Medication Non-Adherence</h2>
|
126 |
+
<span id="na-risk-badge" class="risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider flex-shrink-0"></span>
|
127 |
+
</div>
|
128 |
+
<div id="gauge-na" class="chart-container min-h-[220px] mb-4 flex-grow"></div>
|
129 |
+
<div class="border-t border-gray-200 pt-3 mt-auto">
|
130 |
+
<h3 class="font-semibold text-gray-700 mb-1 text-sm">Interpretation:</h3>
|
131 |
+
<p id="na-explanation" class="text-gray-600 text-sm"></p>
|
132 |
+
</div>
|
133 |
+
</div>
|
134 |
+
<!-- Readmission Risk Card -->
|
135 |
+
<div class="card bg-white rounded-lg p-5 flex flex-col">
|
136 |
+
<div class="flex items-center justify-between mb-3">
|
137 |
+
<h2 class="text-lg font-bold text-gray-800">Hospital Readmission</h2>
|
138 |
+
<span id="r-risk-badge" class="risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider flex-shrink-0"></span>
|
139 |
+
</div>
|
140 |
+
<div id="gauge-r" class="chart-container min-h-[220px] mb-4 flex-grow"></div>
|
141 |
+
<div class="border-t border-gray-200 pt-3 mt-auto">
|
142 |
+
<h3 class="font-semibold text-gray-700 mb-1 text-sm">Interpretation:</h3>
|
143 |
+
<p id="r-explanation" class="text-gray-600 text-sm"></p>
|
144 |
+
</div>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
<!-- Tabs Navigation Card -->
|
148 |
+
<div class="card bg-white rounded-lg mb-6">
|
149 |
+
<div class="border-b border-gray-200">
|
150 |
+
<nav class="-mb-px flex space-x-1 sm:space-x-4 overflow-x-auto px-4" aria-label="Tabs">
|
151 |
+
<button class="tab-btn active" data-tab="recommendations">
|
152 |
+
<i class="fas fa-clipboard-list mr-1.5 opacity-80 text-sm"></i>Recommendations
|
153 |
+
</button>
|
154 |
+
<button class="tab-btn" data-tab="factors">
|
155 |
+
<i class="fas fa-chart-bar mr-1.5 opacity-80 text-sm"></i>Risk Factors
|
156 |
+
</button>
|
157 |
+
<button class="tab-btn" data-tab="whatif">
|
158 |
+
<i class="fas fa-wand-magic-sparkles mr-1.5 opacity-80 text-sm"></i>Interventions
|
159 |
+
</button>
|
160 |
+
<button class="tab-btn" data-tab="insights">
|
161 |
+
<i class="fas fa-search-plus mr-1.5 opacity-80 text-sm"></i>Insights
|
162 |
+
</button>
|
163 |
+
</nav>
|
164 |
+
</div>
|
165 |
+
<!-- Tab Content Area -->
|
166 |
+
<div class="p-5 sm:p-6">
|
167 |
+
<!-- Recommendations Tab -->
|
168 |
+
<div class="tab-content" id="recommendations-tab">
|
169 |
+
<h2 class="text-xl font-bold mb-4 text-gray-800">Personalized Recommendations</h2>
|
170 |
+
<div id="recommendations-container" class="space-y-4">
|
171 |
+
<p class="text-gray-500 italic">Loading recommendations...</p>
|
172 |
+
</div>
|
173 |
+
</div>
|
174 |
+
<!-- Risk Factors Tab -->
|
175 |
+
<div class="tab-content hidden" id="factors-tab">
|
176 |
+
<h2 class="text-xl font-bold mb-4 text-gray-800">Risk Factors Analysis</h2>
|
177 |
+
<!-- Risk Factor Sub-Tabs -->
|
178 |
+
<div class="mb-5 border-b border-gray-200">
|
179 |
+
<nav class="-mb-px flex space-x-4 sm:space-x-6" aria-label="Factor Tabs">
|
180 |
+
<button class="factor-tab-btn active" data-factor="na">
|
181 |
+
Non-Adherence Factors
|
182 |
+
</button>
|
183 |
+
<button class="factor-tab-btn" data-factor="r">
|
184 |
+
Readmission Factors
|
185 |
+
</button>
|
186 |
+
</nav>
|
187 |
+
</div>
|
188 |
+
<!-- Non-Adherence Factors Content -->
|
189 |
+
<div id="na-factors" class="factor-content space-y-6">
|
190 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
191 |
+
<h3 class="font-semibold text-gray-700 text-lg mb-1">Non-Adherence Waterfall</h3>
|
192 |
+
<p class="text-sm text-gray-600 mb-3">Shows how top factors contribute to the non-adherence risk score vs. the average.</p>
|
193 |
+
<div id="waterfall-na" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
|
194 |
+
</div>
|
195 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
196 |
+
<h4 class="font-semibold text-gray-700 mb-2 text-md">Detailed Factor Impact (SHAP):</h4>
|
197 |
+
<div id="shap-table-na" class="border-t border-gray-100 pt-3">
|
198 |
+
<p class="text-gray-500 italic">Loading factor details...</p>
|
199 |
+
</div>
|
200 |
+
</div>
|
201 |
+
</div>
|
202 |
+
<!-- Readmission Factors Content -->
|
203 |
+
<div id="r-factors" class="factor-content hidden space-y-6">
|
204 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
205 |
+
<h3 class="font-semibold text-gray-700 text-lg mb-1">Readmission Waterfall</h3>
|
206 |
+
<p class="text-sm text-gray-600 mb-3">Shows how top factors contribute to the readmission risk score vs. the average.</p>
|
207 |
+
<div id="waterfall-r" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
|
208 |
+
</div>
|
209 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
210 |
+
<h4 class="font-semibold text-gray-700 mb-2 text-md">Detailed Factor Impact (SHAP):</h4>
|
211 |
+
<div id="shap-table-r" class="border-t border-gray-100 pt-3">
|
212 |
+
<p class="text-gray-500 italic">Loading factor details...</p>
|
213 |
+
</div>
|
214 |
+
</div>
|
215 |
+
</div>
|
216 |
+
</div>
|
217 |
+
<!-- What-If / Intervention Tab -->
|
218 |
+
<div class="tab-content hidden" id="whatif-tab">
|
219 |
+
<h2 class="text-xl font-bold mb-4 text-gray-800">Intervention Impact Analysis</h2>
|
220 |
+
<div class="space-y-6">
|
221 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
222 |
+
<h3 class="font-semibold text-gray-700 text-lg mb-2">Potential Modifications</h3>
|
223 |
+
<p class="text-sm text-gray-600 mb-4">Suggests changes to modifiable factors that could potentially lower risk, based on the model. The estimated impact shows the simulated risk change.</p>
|
224 |
+
<div id="cf-table" class="border-t border-gray-100 pt-3">
|
225 |
+
<p class="text-gray-500 italic">Loading potential modifications...</p>
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
229 |
+
<h3 class="font-semibold text-gray-700 text-lg mb-2">Simulated Intervention Impact</h3>
|
230 |
+
<p class="text-sm text-gray-600 mb-4">Visualizes the estimated risk score *reduction* by optimizing the most impactful modifiable factors (SHAP-based estimate).</p>
|
231 |
+
<div id="intervention-chart" class="chart-container mt-4 min-h-[400px] md:min-h-[500px]"></div>
|
232 |
+
</div>
|
233 |
+
</div>
|
234 |
+
</div>
|
235 |
+
<!-- Advanced Insights Tab -->
|
236 |
+
<div class="tab-content hidden" id="insights-tab">
|
237 |
+
<h2 class="text-xl font-bold mb-4 text-gray-800">Advanced Analytical Insights</h2>
|
238 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
239 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
240 |
+
<h3 class="font-semibold text-gray-700 mb-2 text-md">Risk Factor Heatmap</h3>
|
241 |
+
<p class="text-sm text-gray-600 mb-3">Compares factor impacts (SHAP value) on Non-Adherence vs. Readmission. Red increases risk, Blue decreases risk.</p>
|
242 |
+
<div id="risk-heatmap" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
|
243 |
+
</div>
|
244 |
+
<div class="card border p-4 rounded-md shadow-sm">
|
245 |
+
<h3 class="font-semibold text-gray-700 mb-2 text-md">Feature Importance</h3>
|
246 |
+
<p class="text-sm text-gray-600 mb-3">Shows overall importance (absolute SHAP value) of top features for each risk type.</p>
|
247 |
+
<div id="feature-comparison" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
|
248 |
+
</div>
|
249 |
+
</div>
|
250 |
+
<!-- Cleaner Placeholder for Network Graph -->
|
251 |
+
<div class="card border p-4 rounded-md shadow-sm mt-6">
|
252 |
+
<h3 class="font-semibold text-gray-700 mb-2 text-md">Risk Factor Network (Conceptual)</h3>
|
253 |
+
<p class="text-sm text-gray-600 mb-3">Visualizing factor interconnections could reveal complex relationships. (Requires dedicated data & libraries).</p>
|
254 |
+
<div id="network-graph-placeholder"
|
255 |
+
class="text-center text-gray-400 py-10 border-2 border-dashed border-gray-300 rounded-md bg-gray-50/50">
|
256 |
+
<i class="fas fa-project-diagram fa-3x mb-3 opacity-50"></i>
|
257 |
+
<p class="font-medium">Network Graph Placeholder</p>
|
258 |
+
<p class="text-xs mt-1">(Visualization not implemented)</p>
|
259 |
+
</div>
|
260 |
+
<div id="network-graph" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
|
261 |
+
<!-- Removed the pre tag for network data preview -->
|
262 |
+
</div>
|
263 |
+
</div>
|
264 |
+
</div>
|
265 |
+
</div>
|
266 |
+
<!-- End Tabs Card -->
|
267 |
+
</section>
|
268 |
+
<!-- End Result Section -->
|
269 |
+
</main>
|
270 |
+
<script>
|
271 |
+
// Feature information and explanations from backend
|
272 |
+
const featureInfo = JSON.parse('{{ feature_info|tojson|safe }}');
|
273 |
+
const riskExplanations = JSON.parse('{{ risk_explanations|tojson|safe }}');
|
274 |
+
let currentResultsData = null; // Store latest results
|
275 |
+
|
276 |
+
// --- Robust ID Generation ---
|
277 |
+
function getFieldId(fieldName) {
|
278 |
+
return fieldName.toLowerCase()
|
279 |
+
.replace(/₹/g, 'rupees')
|
280 |
+
.replace(/[^\w\s-]/g, '')
|
281 |
+
.trim()
|
282 |
+
.replace(/\s+/g, '-');
|
283 |
+
}
|
284 |
+
|
285 |
+
// --- Form Generation ---
|
286 |
+
function generateFormFields() {
|
287 |
+
const formFieldsContainer = document.getElementById('form-fields');
|
288 |
+
if (!formFieldsContainer) return;
|
289 |
+
formFieldsContainer.innerHTML = '';
|
290 |
+
const order = JSON.parse('{{ feature_order|tojson|safe }}') || [];
|
291 |
+
|
292 |
+
order.forEach(field => {
|
293 |
+
const info = featureInfo[field];
|
294 |
+
if (!info) { console.warn(`Info missing for: ${field}`); return; }
|
295 |
+
const fieldId = getFieldId(field);
|
296 |
+
let inputHTML = '';
|
297 |
+
const commonClasses = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm shadow-sm transition-colors duration-200";
|
298 |
+
|
299 |
+
if (info.options) {
|
300 |
+
const optionsHTML = info.options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
|
301 |
+
inputHTML = `<select id="${fieldId}" name="${fieldId}" class="${commonClasses}">${optionsHTML}</select>`;
|
302 |
+
} else {
|
303 |
+
const range = info.ideal_range ? info.ideal_range.split('-') : [];
|
304 |
+
const min = range.length > 0 ? range[0] : '';
|
305 |
+
const max = range.length > 1 ? range[1] : '';
|
306 |
+
const step = info.step || (field === 'Medicine Availability (0-1)' ? '0.01' : '1');
|
307 |
+
const placeholder = info.ideal_range ? `Range: ${info.ideal_range}` : 'Enter value';
|
308 |
+
inputHTML = `<input type="number" id="${fieldId}" name="${fieldId}" step="${step}" ${min !== '' ? `min="${min}"` : ''} ${max !== '' ? `max="${max}"` : ''} class="${commonClasses}" placeholder="${placeholder}">`;
|
309 |
+
}
|
310 |
+
|
311 |
+
const fieldHTML = `
|
312 |
+
<div class="mb-1">
|
313 |
+
<label for="${fieldId}" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
|
314 |
+
<span>${info.question}</span>
|
315 |
+
<span class="tooltip text-gray-400 hover:text-blue-500 transition-colors duration-200">
|
316 |
+
<i class="far fa-question-circle"></i>
|
317 |
+
<span class="tooltip-text">${info.help_text || info.description}</span>
|
318 |
+
</span>
|
319 |
+
</label>
|
320 |
+
${inputHTML}
|
321 |
+
</div>`;
|
322 |
+
formFieldsContainer.innerHTML += fieldHTML;
|
323 |
+
});
|
324 |
+
|
325 |
+
// Add listeners for ICU days visibility
|
326 |
+
const wasIcuSelect = document.getElementById(getFieldId('Was in ICU (1=Yes)'));
|
327 |
+
const icuDaysInput = document.getElementById(getFieldId('ICU Days'));
|
328 |
+
if (wasIcuSelect && icuDaysInput) {
|
329 |
+
const icuDaysContainer = icuDaysInput.closest('div');
|
330 |
+
const updateIcuVisibility = () => {
|
331 |
+
if (!icuDaysContainer) return;
|
332 |
+
if (wasIcuSelect.value === 'No') {
|
333 |
+
icuDaysInput.value = '0';
|
334 |
+
icuDaysContainer.style.display = 'none';
|
335 |
+
} else {
|
336 |
+
icuDaysContainer.style.display = '';
|
337 |
+
// Optional: Clear ICU days if previously hidden, or keep value
|
338 |
+
// if (icuDaysInput.value === '0') icuDaysInput.value = '';
|
339 |
+
}
|
340 |
+
};
|
341 |
+
wasIcuSelect.addEventListener('change', updateIcuVisibility);
|
342 |
+
updateIcuVisibility(); // Initial check
|
343 |
+
} else {
|
344 |
+
console.warn("ICU related form fields not found for dynamic visibility.");
|
345 |
+
}
|
346 |
+
}
|
347 |
+
|
348 |
+
// --- Default Values ---
|
349 |
+
function setDefaultValues() {
|
350 |
+
hideError();
|
351 |
+
try {
|
352 |
+
document.getElementById(getFieldId('Age')).value = '65';
|
353 |
+
document.getElementById(getFieldId('Gender')).value = 'Male';
|
354 |
+
document.getElementById(getFieldId('Why in Hospital')).value = 'Heart';
|
355 |
+
document.getElementById(getFieldId('Hospital Days')).value = '7';
|
356 |
+
const wasIcuSelect = document.getElementById(getFieldId('Was in ICU (1=Yes)'));
|
357 |
+
wasIcuSelect.value = 'Yes';
|
358 |
+
document.getElementById(getFieldId('ICU Days')).value = '2';
|
359 |
+
document.getElementById(getFieldId('Number of Medicines')).value = '5';
|
360 |
+
document.getElementById(getFieldId('Cost per Medicine (₹)')).value = '200';
|
361 |
+
document.getElementById(getFieldId('Days Medicine Lasts')).value = '30';
|
362 |
+
document.getElementById(getFieldId('Total Dosage per Day (mg)')).value = '100';
|
363 |
+
document.getElementById(getFieldId('Total Pills Given')).value = '90';
|
364 |
+
document.getElementById(getFieldId('Medicine Availability (0-1)')).value = '0.8';
|
365 |
+
document.getElementById(getFieldId('Took Medicine Day 1 (1=Yes)')).value = 'Yes';
|
366 |
+
document.getElementById(getFieldId('Took Medicine Day 2 (1=Yes)')).value = 'Yes';
|
367 |
+
document.getElementById(getFieldId('Took Medicine Day 3 (1=Yes)')).value = 'No';
|
368 |
+
if (wasIcuSelect) { wasIcuSelect.dispatchEvent(new Event('change')); } // Trigger visibility update
|
369 |
+
console.log("Default values reset.");
|
370 |
+
} catch (error) {
|
371 |
+
console.error("Error setting default values:", error);
|
372 |
+
showError("Could not reset all default values. Please check the form fields.");
|
373 |
+
}
|
374 |
+
}
|
375 |
+
|
376 |
+
// --- Error Handling ---
|
377 |
+
function showError(message) {
|
378 |
+
const errorDiv = document.getElementById('error-message');
|
379 |
+
const errorText = document.getElementById('error-text');
|
380 |
+
if(errorDiv && errorText) {
|
381 |
+
errorText.textContent = message;
|
382 |
+
errorDiv.classList.remove('hidden');
|
383 |
+
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
384 |
+
}
|
385 |
+
}
|
386 |
+
function hideError() {
|
387 |
+
const errorDiv = document.getElementById('error-message');
|
388 |
+
if(errorDiv) errorDiv.classList.add('hidden');
|
389 |
+
}
|
390 |
+
|
391 |
+
// --- Form Submission Logic ---
|
392 |
+
function setupFormSubmission() {
|
393 |
+
const form = document.getElementById('patient-form');
|
394 |
+
if (!form) return;
|
395 |
+
|
396 |
+
form.addEventListener('submit', function(e) {
|
397 |
+
e.preventDefault();
|
398 |
+
hideError();
|
399 |
+
|
400 |
+
document.getElementById('loading').classList.remove('hidden');
|
401 |
+
document.getElementById('result-section').style.display = 'none';
|
402 |
+
const loadingElement = document.getElementById('loading');
|
403 |
+
if (loadingElement) loadingElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
404 |
+
|
405 |
+
const jsKeyMap = {
|
406 |
+
'age': 'Age', 'gender': 'Gender', 'why-in-hospital': 'Why in Hospital',
|
407 |
+
'hospital-days': 'Hospital Days', 'was-in-icu-1yes': 'Was in ICU (1=Yes)',
|
408 |
+
'icu-days': 'ICU Days', 'number-of-medicines': 'Number of Medicines',
|
409 |
+
'cost-per-medicine-rupees': 'Cost per Medicine (₹)', 'days-medicine-lasts': 'Days Medicine Lasts',
|
410 |
+
'total-dosage-per-day-mg': 'Total Dosage per Day (mg)', 'total-pills-given': 'Total Pills Given',
|
411 |
+
'medicine-availability-0-1': 'Medicine Availability (0-1)',
|
412 |
+
'took-medicine-day-1-1yes': 'Took Medicine Day 1 (1=Yes)',
|
413 |
+
'took-medicine-day-2-1yes': 'Took Medicine Day 2 (1=Yes)',
|
414 |
+
'took-medicine-day-3-1yes': 'Took Medicine Day 3 (1=Yes)'
|
415 |
+
};
|
416 |
+
const formData = {};
|
417 |
+
let hasValidationError = false;
|
418 |
+
|
419 |
+
for (const [jsKey, featureName] of Object.entries(jsKeyMap)) {
|
420 |
+
const element = document.getElementById(getFieldId(featureName));
|
421 |
+
if (!element) {
|
422 |
+
showError(`Internal error: Form field for ${featureName} not found.`);
|
423 |
+
hasValidationError = true; break;
|
424 |
+
}
|
425 |
+
// Check visibility for ICU Days based on its container div
|
426 |
+
const isIcuDays = featureName === 'ICU Days';
|
427 |
+
const icuContainer = element.closest('div');
|
428 |
+
const isIcuHidden = isIcuDays && icuContainer && icuContainer.style.display === 'none';
|
429 |
+
|
430 |
+
// Validate if element is visible OR if it's ICU Days and should be 0
|
431 |
+
if (!isIcuHidden) {
|
432 |
+
if (element.value === null || element.value.trim() === '') {
|
433 |
+
showError(`Missing value for: ${featureName}`);
|
434 |
+
element.classList.add('border-red-500', 'ring-red-500');
|
435 |
+
hasValidationError = true; break;
|
436 |
+
} else {
|
437 |
+
formData[jsKey] = element.value;
|
438 |
+
element.classList.remove('border-red-500', 'ring-red-500');
|
439 |
+
}
|
440 |
+
} else if (isIcuDays) {
|
441 |
+
formData[jsKey] = '0'; // Send 0 if ICU container is hidden
|
442 |
+
}
|
443 |
+
}
|
444 |
+
|
445 |
+
if (hasValidationError) {
|
446 |
+
document.getElementById('loading').classList.add('hidden');
|
447 |
+
return;
|
448 |
+
}
|
449 |
+
|
450 |
+
fetch('/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) })
|
451 |
+
.then(response => {
|
452 |
+
if (!response.ok) {
|
453 |
+
return response.json().then(errData => { throw new Error(errData.error || `Server error: ${response.status}`); })
|
454 |
+
.catch(() => { throw new Error(`Server error: ${response.status}`); });
|
455 |
+
} return response.json(); })
|
456 |
+
.then(data => {
|
457 |
+
if (data.success) {
|
458 |
+
currentResultsData = data;
|
459 |
+
displayResults(data);
|
460 |
+
const resultElement = document.getElementById('result-section');
|
461 |
+
if (resultElement) {
|
462 |
+
resultElement.style.display = 'block'; // Ensure it's displayed before scrolling
|
463 |
+
// Timeout ensures animation can start
|
464 |
+
setTimeout(() => resultElement.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
|
465 |
+
}
|
466 |
+
} else { showError('Prediction Error: ' + data.error); }})
|
467 |
+
.catch(error => { showError('Request Error: ' + error.message); })
|
468 |
+
.finally(() => { document.getElementById('loading').classList.add('hidden'); });
|
469 |
+
});
|
470 |
+
// Reset defaults button listener
|
471 |
+
const resetButton = document.getElementById('reset-defaults-btn');
|
472 |
+
if(resetButton) resetButton.addEventListener('click', setDefaultValues);
|
473 |
+
}
|
474 |
+
|
475 |
+
// --- Display Results ---
|
476 |
+
function displayResults(data) {
|
477 |
+
// Check for essential data presence
|
478 |
+
if (!data || !data.predictions || !data.visualizations || !data.explanations || !data.recommendations) {
|
479 |
+
showError("Incomplete data received from server. Cannot display results.");
|
480 |
+
return;
|
481 |
+
}
|
482 |
+
|
483 |
+
// 1. Risk Badges & Explanations
|
484 |
+
const naRisk = data.predictions.risk_level_na;
|
485 |
+
const rRisk = data.predictions.risk_level_r;
|
486 |
+
const naRiskBadge = document.getElementById('na-risk-badge');
|
487 |
+
const rRiskBadge = document.getElementById('r-risk-badge');
|
488 |
+
if(naRisk && naRiskBadge) {
|
489 |
+
naRiskBadge.textContent = `${naRisk.level} (${naRisk.percentage})`;
|
490 |
+
naRiskBadge.className = `risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider ${naRisk.color || 'gray'}`;
|
491 |
+
document.getElementById('na-explanation').textContent = riskExplanations.non_adherence.levels[naRisk.level] || 'N/A';
|
492 |
+
}
|
493 |
+
if(rRisk && rRiskBadge) {
|
494 |
+
rRiskBadge.textContent = `${rRisk.level} (${rRisk.percentage})`;
|
495 |
+
rRiskBadge.className = `risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider ${rRisk.color || 'gray'}`;
|
496 |
+
document.getElementById('r-explanation').textContent = riskExplanations.readmission.levels[rRisk.level] || 'N/A';
|
497 |
+
}
|
498 |
+
|
499 |
+
// 2. Plot Gauges
|
500 |
+
plotGauge('gauge-na', data.visualizations?.gauges?.non_adherence, 'Non-Adherence Gauge');
|
501 |
+
plotGauge('gauge-r', data.visualizations?.gauges?.readmission, 'Readmission Gauge');
|
502 |
+
|
503 |
+
// 3. Recommendations
|
504 |
+
displayRecommendations(data.recommendations);
|
505 |
+
|
506 |
+
// 4. Risk Factors (Waterfall & SHAP)
|
507 |
+
displayRiskFactors(data.explanations, data.visualizations?.additional);
|
508 |
+
|
509 |
+
// 5. Interventions (CF Table & Chart)
|
510 |
+
displayCounterfactuals(data.explanations);
|
511 |
+
plotInterventionChart(data.visualizations?.additional?.intervention_impact);
|
512 |
+
|
513 |
+
// 6. Advanced Insights
|
514 |
+
displayAdditionalVisualizations(data.visualizations?.additional);
|
515 |
+
|
516 |
+
// 7. Setup Tabs
|
517 |
+
setupTabs(true); // Force reset to default tabs
|
518 |
+
}
|
519 |
+
|
520 |
+
// --- Plotting Functions (with Error Handling) ---
|
521 |
+
function plotGauge(elementId, gaugeJson, chartName) { /* ... Keep as is ... */
|
522 |
+
const container = document.getElementById(elementId);
|
523 |
+
if (!container) { console.error(`Container not found: ${elementId}`); return; }
|
524 |
+
container.innerHTML = '';
|
525 |
+
try {
|
526 |
+
if (gaugeJson) {
|
527 |
+
const gaugeData = JSON.parse(gaugeJson);
|
528 |
+
Plotly.newPlot(elementId, gaugeData.data, gaugeData.layout, {responsive: true, displaylogo: false, modeBarButtonsToRemove: ['toImage', 'sendDataToCloud', 'select2d', 'lasso2d']});
|
529 |
+
} else { container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error: ${chartName} data not available.</p>`; }
|
530 |
+
} catch (e) { console.error(`Plotting Error ${chartName}:`, e); container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error displaying ${chartName}.</p>`; }
|
531 |
+
}
|
532 |
+
function plotGenericChart(elementId, chartJson, chartName) { /* ... Keep as is ... */
|
533 |
+
const container = document.getElementById(elementId);
|
534 |
+
if (!container) { console.error(`Container not found: ${elementId}`); return; }
|
535 |
+
container.innerHTML = '';
|
536 |
+
try {
|
537 |
+
if (chartJson) {
|
538 |
+
const plotData = JSON.parse(chartJson);
|
539 |
+
if (plotData.data && plotData.data.length > 0 && plotData.layout) {
|
540 |
+
Plotly.newPlot(elementId, plotData.data, plotData.layout, {responsive: true, displaylogo: false, modeBarButtonsToRemove: ['toImage', 'sendDataToCloud', 'select2d', 'lasso2d']});
|
541 |
+
} else if (plotData.layout?.annotations?.[0]?.text) {
|
542 |
+
container.innerHTML = `<div class="text-center text-gray-500 p-6 italic h-full flex items-center justify-center">${plotData.layout.annotations[0].text}</div>`;
|
543 |
+
} else { container.innerHTML = `<p class="text-sm text-gray-500 text-center p-4">${chartName}: No data to display.</p>`; }
|
544 |
+
} else { container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error: ${chartName} data not available.</p>`; }
|
545 |
+
} catch (e) { console.error(`Plotting Error ${chartName}:`, e); container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error displaying ${chartName}.</p>`; }
|
546 |
+
}
|
547 |
+
function plotInterventionChart(interventionJson) {
|
548 |
+
plotGenericChart('intervention-chart', interventionJson, 'Intervention Impact Chart');
|
549 |
+
}
|
550 |
+
|
551 |
+
// --- Display Recommendations ---
|
552 |
+
function displayRecommendations(recommendations) {
|
553 |
+
const container = document.getElementById('recommendations-container');
|
554 |
+
if (!container) return;
|
555 |
+
container.innerHTML = ''; // Clear previous
|
556 |
+
|
557 |
+
if (!recommendations || recommendations.length === 0 || (recommendations[0] && recommendations[0].error)) {
|
558 |
+
container.innerHTML = `<p class="text-gray-500 italic p-4">${recommendations?.[0]?.error || 'No specific recommendations generated.'}</p>`;
|
559 |
+
return;
|
560 |
+
}
|
561 |
+
|
562 |
+
recommendations.forEach(rec => {
|
563 |
+
const priorityClass = rec.priority ? rec.priority.toLowerCase() : 'standard';
|
564 |
+
const priorityColor = getPriorityColor(rec.priority);
|
565 |
+
const priorityIcon = getPriorityIcon(rec.priority);
|
566 |
+
const indicatorClasses = `priority-indicator ${priorityClass}`;
|
567 |
+
const iconClasses = `fas ${priorityIcon}`;
|
568 |
+
|
569 |
+
// !! REMOVED the invalid comment here !!
|
570 |
+
const recHTML = `
|
571 |
+
<div class="recommendation-card card p-4 ${priorityClass} bg-white shadow-sm border-l-4">
|
572 |
+
<div class="flex items-start">
|
573 |
+
<div class="${indicatorClasses}">
|
574 |
+
<i class="${iconClasses}"></i>
|
575 |
+
</div>
|
576 |
+
<div class="flex-1 min-w-0">
|
577 |
+
<div class="flex justify-between items-center flex-wrap gap-x-2 mb-1">
|
578 |
+
<h3 class="font-semibold text-gray-800 text-base">${rec.category || 'Recommendation'}</h3>
|
579 |
+
<span class="px-2 py-0.5 rounded-full text-xs font-medium whitespace-nowrap bg-${priorityColor}-100 text-${priorityColor}-800">
|
580 |
+
${rec.priority || 'Standard'} Priority
|
581 |
+
</span>
|
582 |
+
</div>
|
583 |
+
<p class="text-gray-700 mt-1 text-sm">${rec.recommendation || 'Details not available.'}</p>
|
584 |
+
<div class="mt-3 p-3 bg-blue-50 rounded border border-blue-100">
|
585 |
+
<p class="text-sm">
|
586 |
+
<strong class="font-medium text-blue-800">Suggested Action:</strong>
|
587 |
+
<span class="text-blue-700 ml-1">${rec.action || 'No specific action suggested.'}</span>
|
588 |
+
</p>
|
589 |
+
</div>
|
590 |
+
</div>
|
591 |
+
</div>
|
592 |
+
</div>`;
|
593 |
+
container.innerHTML += recHTML;
|
594 |
+
});
|
595 |
+
}
|
596 |
+
function getPriorityIcon(priority) { /* ... Keep as is ... */
|
597 |
+
switch (priority ? priority.toLowerCase() : '') {
|
598 |
+
case 'critical': return 'fa-circle-exclamation'; // Updated icon
|
599 |
+
case 'high': return 'fa-triangle-exclamation'; // Updated icon
|
600 |
+
case 'medium': return 'fa-arrow-up';
|
601 |
+
case 'standard': return 'fa-circle-check'; // Updated icon
|
602 |
+
default: return 'fa-circle-info'; // Updated icon
|
603 |
+
}
|
604 |
+
}
|
605 |
+
function getPriorityColor(priority) { /* ... Keep as is ... */
|
606 |
+
switch (priority ? priority.toLowerCase() : '') {
|
607 |
+
case 'critical': return 'red';
|
608 |
+
case 'high': return 'orange';
|
609 |
+
case 'medium': return 'yellow';
|
610 |
+
case 'standard': return 'green';
|
611 |
+
default: return 'blue';
|
612 |
+
}
|
613 |
+
}
|
614 |
+
|
615 |
+
|
616 |
+
// --- Display Risk Factors ---
|
617 |
+
function displayRiskFactors(explanations, additionalViz) {
|
618 |
+
if (!explanations) return;
|
619 |
+
// Plot Waterfalls
|
620 |
+
plotGenericChart('waterfall-na', additionalViz?.waterfall_na, 'Non-Adherence Waterfall');
|
621 |
+
plotGenericChart('waterfall-r', additionalViz?.waterfall_r, 'Readmission Waterfall');
|
622 |
+
// Display SHAP Tables
|
623 |
+
displayShapTable(explanations.shap_values_na, 'na', explanations.shap_error_na);
|
624 |
+
displayShapTable(explanations.shap_values_r, 'r', explanations.shap_error_r);
|
625 |
+
}
|
626 |
+
|
627 |
+
// --- Display SHAP Table ---
|
628 |
+
function displayShapTable(shapData, riskType, shapError) { /* ... Keep as is ... */
|
629 |
+
const tableId = `shap-table-${riskType}`;
|
630 |
+
const tableContainer = document.getElementById(tableId);
|
631 |
+
if (!tableContainer) return;
|
632 |
+
tableContainer.innerHTML = '';
|
633 |
+
|
634 |
+
if (shapError || !shapData || shapData.length === 0 || (shapData[0] && shapData[0].error)) {
|
635 |
+
const errorMsg = (shapData && shapData[0] && shapData[0].error) ? shapData[0].error : `SHAP data calculation failed or is unavailable for ${riskType === 'na' ? 'Non-Adherence' : 'Readmission'}.`;
|
636 |
+
tableContainer.innerHTML = `<p class="text-red-600 italic p-4">${errorMsg}</p>`;
|
637 |
+
return;
|
638 |
+
}
|
639 |
+
const validShapData = shapData.filter(item => !(item && item.error));
|
640 |
+
if (validShapData.length === 0) { tableContainer.innerHTML = `<p class="text-gray-500 italic p-4">No valid factor data to display.</p>`; return; }
|
641 |
+
|
642 |
+
const maxAbsShap = Math.max(...validShapData.map(item => Math.abs(item.shap_value || 0)), 0.01);
|
643 |
+
let tableHTML = `
|
644 |
+
<div class="overflow-x-auto rounded border border-gray-200">
|
645 |
+
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
646 |
+
<thead class="bg-gray-50">
|
647 |
+
<tr>
|
648 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Feature</th>
|
649 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Patient Value</th>
|
650 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider w-1/3">Impact on Risk Score (SHAP Value)</th>
|
651 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Effect</th>
|
652 |
+
</tr>
|
653 |
+
</thead>
|
654 |
+
<tbody class="bg-white divide-y divide-gray-200">`;
|
655 |
+
|
656 |
+
validShapData.forEach(item => {
|
657 |
+
const shapValue = item.shap_value || 0;
|
658 |
+
const absValue = Math.abs(shapValue);
|
659 |
+
const widthPercent = Math.min(Math.max((absValue / maxAbsShap) * 100, 1), 100);
|
660 |
+
const direction = shapValue > 0.001 ? 'Increases Risk' : (shapValue < -0.001 ? 'Decreases Risk' : 'Neutral');
|
661 |
+
const barClass = shapValue > 0.001 ? 'shap-positive' : (shapValue < -0.001 ? 'shap-negative' : 'bg-gray-300');
|
662 |
+
const directionClass = shapValue > 0.001 ? 'bg-red-100 text-red-800' : (shapValue < -0.001 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800');
|
663 |
+
const shapTextClass = shapValue > 0.001 ? 'text-red-600' : (shapValue < -0.001 ? 'text-green-600' : 'text-gray-600');
|
664 |
+
tableHTML += `
|
665 |
+
<tr class="feature-row">
|
666 |
+
<td class="px-4 py-2 whitespace-nowrap">
|
667 |
+
<span class="font-medium text-gray-900">${item.feature || 'Unknown'}</span>
|
668 |
+
<span class="tooltip ml-1 text-gray-400 hover:text-blue-500"> <i class="far fa-question-circle"></i> <span class="tooltip-text">${item.help_text || item.description || 'No details'}</span> </span>
|
669 |
+
</td>
|
670 |
+
<td class="px-4 py-2 text-gray-700 whitespace-nowrap">${item.feature_value !== null ? item.feature_value : 'N/A'}</td>
|
671 |
+
<td class="px-4 py-2">
|
672 |
+
<div class="flex items-center"> <span class="mr-2 ${shapTextClass} font-mono w-12 text-right">${shapValue.toFixed(3)}</span> <div class="shap-bar-container flex-1"> <div class="shap-bar ${barClass}" style="width: ${widthPercent}%"></div> </div> </div>
|
673 |
+
</td>
|
674 |
+
<td class="px-4 py-2 whitespace-nowrap"> <span class="inline-flex rounded-full px-2 py-0.5 text-xs font-semibold leading-5 ${directionClass}"> ${direction} </span> </td>
|
675 |
+
</tr>`;
|
676 |
+
});
|
677 |
+
tableHTML += `</tbody></table></div> <p class="text-xs text-gray-500 mt-2 italic">SHAP values estimate the contribution of each feature to the model's risk score prediction relative to the average.</p>`;
|
678 |
+
tableContainer.innerHTML = tableHTML;
|
679 |
+
}
|
680 |
+
|
681 |
+
// --- Display Counterfactuals Table ---
|
682 |
+
function displayCounterfactuals(explanations) { /* ... Keep as is ... */
|
683 |
+
const cfTableContainer = document.getElementById('cf-table');
|
684 |
+
if (!cfTableContainer || !explanations) return;
|
685 |
+
cfTableContainer.innerHTML = '';
|
686 |
+
|
687 |
+
let allCfs = [];
|
688 |
+
if (explanations.counterfactuals_na && !explanations.counterfactuals_na[0]?.error) allCfs = allCfs.concat(explanations.counterfactuals_na);
|
689 |
+
if (explanations.counterfactuals_r && !explanations.counterfactuals_r[0]?.error) allCfs = allCfs.concat(explanations.counterfactuals_r);
|
690 |
+
|
691 |
+
const modifiableCfs = {};
|
692 |
+
allCfs.forEach(cf => {
|
693 |
+
if (cf.feature && cf.suggested_change && !cf.suggested_change.toLowerCase().includes("maintain")) {
|
694 |
+
if (!modifiableCfs[cf.feature]) modifiableCfs[cf.feature] = [];
|
695 |
+
modifiableCfs[cf.feature].push(cf);
|
696 |
+
}
|
697 |
+
});
|
698 |
+
|
699 |
+
if (Object.keys(modifiableCfs).length === 0) {
|
700 |
+
cfTableContainer.innerHTML = '<p class="text-gray-500 italic p-4">No specific modifications identified with significant potential for risk reduction.</p>';
|
701 |
+
return;
|
702 |
+
}
|
703 |
+
let tableHTML = `
|
704 |
+
<div class="overflow-x-auto rounded border border-gray-200">
|
705 |
+
<table class="min-w-full divide-y divide-gray-200 text-sm"> <thead class="bg-gray-50"> <tr>
|
706 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Factor</th>
|
707 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Current</th>
|
708 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Suggestion</th>
|
709 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Affects</th>
|
710 |
+
<th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Est. Outcome</th>
|
711 |
+
</tr> </thead> <tbody class="bg-white divide-y divide-gray-200">`;
|
712 |
+
|
713 |
+
const sortedFeatures = Object.keys(modifiableCfs).sort();
|
714 |
+
sortedFeatures.forEach(feature => {
|
715 |
+
const cfsForFeature = modifiableCfs[feature];
|
716 |
+
const firstCf = cfsForFeature[0];
|
717 |
+
const affectedRisks = [...new Set(cfsForFeature.map(cf => cf.risk_type))].map(rt => rt.split('-')[0]).join(' & ');
|
718 |
+
const bestOutcomeCf = cfsForFeature.reduce((best, current) => {
|
719 |
+
const bestMag = ['Minor', 'Moderate', 'Significant'].indexOf(best.impact_magnitude);
|
720 |
+
const currentMag = ['Minor', 'Moderate', 'Significant'].indexOf(current.impact_magnitude);
|
721 |
+
return currentMag > bestMag ? current : best;
|
722 |
+
}, firstCf);
|
723 |
+
const suggestedChange = bestOutcomeCf.suggested_change || 'Optimize';
|
724 |
+
const potentialOutcome = bestOutcomeCf.potential_outcome || 'N/A';
|
725 |
+
const impactMagnitude = bestOutcomeCf.impact_magnitude || 'Minor';
|
726 |
+
let magnitudeClass = 'bg-blue-100 text-blue-800';
|
727 |
+
if (impactMagnitude === 'Significant') magnitudeClass = 'bg-yellow-100 text-yellow-800';
|
728 |
+
else if (impactMagnitude === 'Moderate') magnitudeClass = 'bg-orange-100 text-orange-800';
|
729 |
+
tableHTML += `
|
730 |
+
<tr class="feature-row">
|
731 |
+
<td class="px-4 py-2 font-medium text-gray-900">${feature}</td>
|
732 |
+
<td class="px-4 py-2 text-gray-700">${firstCf.current_value}</td>
|
733 |
+
<td class="px-4 py-2 text-blue-600 font-medium">${suggestedChange}</td>
|
734 |
+
<td class="px-4 py-2 text-gray-700">${affectedRisks}</td>
|
735 |
+
<td class="px-4 py-2 text-gray-700"> ${potentialOutcome !== 'N/A' ? potentialOutcome : ''} <span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${magnitudeClass}"> ${impactMagnitude} Potential </span> </td>
|
736 |
+
</tr>`;
|
737 |
+
});
|
738 |
+
tableHTML += `</tbody></table></div>`;
|
739 |
+
cfTableContainer.innerHTML = tableHTML;
|
740 |
+
}
|
741 |
+
|
742 |
+
|
743 |
+
// --- Display Additional Visualizations ---
|
744 |
+
function displayAdditionalVisualizations(additionalViz) {
|
745 |
+
if (!additionalViz) return;
|
746 |
+
|
747 |
+
// 1. Risk Factor Heatmap
|
748 |
+
plotGenericChart(
|
749 |
+
'risk-heatmap',
|
750 |
+
additionalViz.risk_heatmap,
|
751 |
+
'Risk Factor Heatmap'
|
752 |
+
);
|
753 |
+
|
754 |
+
// 2. Feature Importance Comparison
|
755 |
+
plotGenericChart(
|
756 |
+
'feature-comparison',
|
757 |
+
additionalViz.feature_comparison,
|
758 |
+
'Feature Importance Comparison'
|
759 |
+
);
|
760 |
+
|
761 |
+
// 3. Intervention Impact Chart
|
762 |
+
plotGenericChart(
|
763 |
+
'intervention-chart',
|
764 |
+
additionalViz.intervention_impact,
|
765 |
+
'Intervention Impact Chart'
|
766 |
+
);
|
767 |
+
|
768 |
+
// 4. Network Graph
|
769 |
+
const networkContainer = document.getElementById('network-graph');
|
770 |
+
const networkPlaceholder = document.getElementById('network-graph-placeholder');
|
771 |
+
|
772 |
+
if (additionalViz.network_graph) {
|
773 |
+
// Hide the old placeholder if present
|
774 |
+
if (networkPlaceholder) networkPlaceholder.classList.add('hidden');
|
775 |
+
|
776 |
+
// Render the Plotly network graph
|
777 |
+
if (networkContainer) {
|
778 |
+
networkContainer.innerHTML = '';
|
779 |
+
try {
|
780 |
+
const netData = JSON.parse(additionalViz.network_graph);
|
781 |
+
Plotly.newPlot(
|
782 |
+
'network-graph',
|
783 |
+
netData.data,
|
784 |
+
netData.layout,
|
785 |
+
{
|
786 |
+
responsive: true,
|
787 |
+
displaylogo: false,
|
788 |
+
modeBarButtonsToRemove: ['toImage','sendDataToCloud','select2d','lasso2d']
|
789 |
+
}
|
790 |
+
);
|
791 |
+
} catch (e) {
|
792 |
+
console.error('Error rendering network graph:', e);
|
793 |
+
networkContainer.innerHTML = `
|
794 |
+
<p class="text-sm text-red-500 text-center p-4">
|
795 |
+
Could not display network graph.
|
796 |
+
</p>`;
|
797 |
+
}
|
798 |
+
}
|
799 |
+
} else {
|
800 |
+
// No network data → show placeholder
|
801 |
+
if (networkContainer) networkContainer.innerHTML = '';
|
802 |
+
if (networkPlaceholder) networkPlaceholder.classList.remove('hidden');
|
803 |
+
}
|
804 |
+
}
|
805 |
+
|
806 |
+
|
807 |
+
// --- Tab Switching Logic ---
|
808 |
+
function setupTabs(forceDefault = false) { /* ... Keep as is ... */
|
809 |
+
const mainTabs = document.querySelectorAll('.tab-btn');
|
810 |
+
const mainTabContents = document.querySelectorAll('.tab-content');
|
811 |
+
const factorTabs = document.querySelectorAll('.factor-tab-btn');
|
812 |
+
const factorTabContents = document.querySelectorAll('.factor-content');
|
813 |
+
|
814 |
+
const activateTab = (tabs, contents, tabToActivate) => {
|
815 |
+
if (!tabToActivate) return;
|
816 |
+
tabs.forEach(t => t.classList.remove('active'));
|
817 |
+
contents.forEach(c => c.classList.add('hidden'));
|
818 |
+
tabToActivate.classList.add('active');
|
819 |
+
const targetContentId = tabToActivate.getAttribute('data-tab') || tabToActivate.getAttribute('data-factor');
|
820 |
+
const targetContent = document.getElementById(`${targetContentId}-tab`) || document.getElementById(`${targetContentId}-factors`);
|
821 |
+
if (targetContent) {
|
822 |
+
targetContent.classList.remove('hidden');
|
823 |
+
setTimeout(() => {
|
824 |
+
const charts = targetContent.querySelectorAll('.js-plotly-plot');
|
825 |
+
charts.forEach(chart => { try { if (chart.layout && chart.data) Plotly.Plots.resize(chart); } catch(e) { console.warn("Resize err:", e); } });
|
826 |
+
}, 150); // Slightly longer delay for complex tabs maybe
|
827 |
+
} else { console.error(`Target content not found for tab: ${targetContentId}`); }
|
828 |
+
};
|
829 |
+
|
830 |
+
mainTabs.forEach(tab => tab.addEventListener('click', () => activateTab(mainTabs, mainTabContents, tab)));
|
831 |
+
factorTabs.forEach(tab => tab.addEventListener('click', () => activateTab(factorTabs, factorTabContents, tab)));
|
832 |
+
|
833 |
+
if (forceDefault || !document.querySelector('.tab-btn.active')) {
|
834 |
+
const defaultMainTab = document.querySelector('.tab-btn[data-tab="recommendations"]');
|
835 |
+
activateTab(mainTabs, mainTabContents, defaultMainTab);
|
836 |
+
const defaultFactorTab = document.querySelector('.factor-tab-btn[data-factor="na"]');
|
837 |
+
activateTab(factorTabs, factorTabContents, defaultFactorTab);
|
838 |
+
// Ensure other factor content is hidden
|
839 |
+
const rFactorContent = document.getElementById('r-factors');
|
840 |
+
if (rFactorContent) rFactorContent.classList.add('hidden');
|
841 |
+
}
|
842 |
+
}
|
843 |
+
|
844 |
+
// --- Initialization ---
|
845 |
+
document.addEventListener('DOMContentLoaded', () => {
|
846 |
+
console.log("DOM Ready. Initializing...");
|
847 |
+
try {
|
848 |
+
generateFormFields();
|
849 |
+
setDefaultValues();
|
850 |
+
setupFormSubmission();
|
851 |
+
setupTabs();
|
852 |
+
console.log("Initialization complete.");
|
853 |
+
} catch (initError) {
|
854 |
+
console.error("Initialization Error:", initError);
|
855 |
+
showError("Failed to initialize the dashboard interface. Please refresh the page.");
|
856 |
+
}
|
857 |
+
});
|
858 |
+
|
859 |
+
</script>
|
860 |
+
</body>
|
861 |
+
</html>
|