Ashkan Taghipour (The University of Western Australia) commited on
Commit
c62c4d1
·
1 Parent(s): 2fb0b9f

UI update: Minimalistic diagnosis bars and sample info

Browse files
Files changed (1) hide show
  1. app.py +139 -64
app.py CHANGED
@@ -49,6 +49,22 @@ SAMPLE_DESCRIPTIONS = {
49
  "Sample 3": "Ventricular Tachycardia - A fast heart rhythm originating from the ventricles, potentially life-threatening.",
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def load_inference_engine():
54
  """Load the inference engine on startup."""
@@ -83,13 +99,14 @@ def get_sample_ecgs():
83
  return samples
84
 
85
 
86
- def analyze_ecg(ecg_signal: np.ndarray, filename: str = "ECG Analysis"):
87
  """
88
  Analyze an ECG signal and return all visualizations.
89
 
90
  Args:
91
  ecg_signal: ECG signal array
92
  filename: Name to display
 
93
 
94
  Returns:
95
  Tuple of (ecg_plot, diagnosis_plot, risk_plot, summary_text)
@@ -137,23 +154,44 @@ def analyze_ecg(ecg_signal: np.ndarray, filename: str = "ECG Analysis"):
137
  else:
138
  severity_class = "severity-high"
139
 
 
 
 
 
 
 
 
 
 
 
140
  diagnosis_html += f"""
141
- <div class="diagnosis-card">
142
- <div class="diagnosis-header">
143
- <span class="diagnosis-rank">#{i}</span>
144
- <span class="diagnosis-name">{class_names[idx]}</span>
145
- <span class="diagnosis-percent {severity_class}">{prob_pct:.1f}%</span>
146
- </div>
147
- <div class="progress-container">
148
- <div class="progress-bar {severity_class}" style="width: {prob_pct}%;"></div>
149
  </div>
 
150
  </div>
151
  """
152
 
 
 
 
 
 
 
 
 
 
153
  summary = f"""
154
  <div style="padding: 10px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;">
155
 
156
- <h2 style="margin: 0 0 16px 0; color: #333;">Analysis Results: {filename}</h2>
 
157
 
158
  <div style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.9em; margin-bottom: 20px;">
159
  Inference Time: {inference_time:.1f} ms
@@ -222,7 +260,9 @@ def analyze_sample_by_name(sample_name: str):
222
  if sample["name"] == sample_name:
223
  try:
224
  ecg_signal = np.load(sample["path"])
225
- return analyze_ecg(ecg_signal, sample["name"])
 
 
226
  except Exception as e:
227
  logger.error(f"Error loading sample: {e}")
228
  return None, None, None, f"<p style='color: #dc3545;'>Error loading sample: {str(e)}</p>"
@@ -371,81 +411,121 @@ def create_demo_interface():
371
  box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
372
  }
373
 
374
- /* Modern Diagnosis Card Styles */
375
  .diagnosis-dashboard {
376
- padding: 20px;
 
377
  }
378
 
379
- .diagnosis-card {
380
- background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
381
- border-radius: 16px;
382
- padding: 20px;
383
- margin: 12px 0;
384
- box-shadow: 0 4px 20px rgba(0,0,0,0.08);
385
- border: 1px solid rgba(0,0,0,0.05);
386
- transition: transform 0.2s ease, box-shadow 0.2s ease;
387
- }
388
-
389
- .diagnosis-card:hover {
390
- transform: translateY(-2px);
391
- box-shadow: 0 8px 30px rgba(0,0,0,0.12);
392
- }
393
-
394
- .diagnosis-header {
395
  display: flex;
396
- justify-content: space-between;
397
  align-items: center;
398
- margin-bottom: 12px;
 
 
 
 
 
 
 
 
399
  }
400
 
401
  .diagnosis-rank {
402
- font-size: 0.85em;
403
  font-weight: 600;
404
- color: #666;
405
- background: #f0f0f0;
406
- padding: 4px 10px;
407
- border-radius: 20px;
408
  }
409
 
410
  .diagnosis-name {
411
- font-size: 1.1em;
412
- font-weight: 600;
413
  color: #333;
414
- flex: 1;
415
- margin-left: 12px;
 
 
 
416
  }
417
 
418
- .diagnosis-percent {
419
- font-size: 1.3em;
420
- font-weight: 700;
 
 
 
 
421
  }
422
 
423
- .progress-container {
424
- height: 10px;
425
- background: #e9ecef;
426
- border-radius: 10px;
 
 
427
  overflow: hidden;
428
  }
429
 
430
- .progress-bar {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  height: 100%;
432
- border-radius: 10px;
433
- transition: width 0.8s ease;
434
  }
435
 
436
- .severity-low {
437
- background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
438
- color: #28a745;
439
  }
440
 
441
- .severity-medium {
442
- background: linear-gradient(90deg, #ffc107 0%, #fd7e14 100%);
443
- color: #fd7e14;
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
445
 
446
- .severity-high {
447
- background: linear-gradient(90deg, #dc3545 0%, #e83e8c 100%);
448
- color: #dc3545;
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
 
451
  /* Footer Styles */
@@ -539,11 +619,6 @@ def create_demo_interface():
539
  info="Click on a sample to select it"
540
  )
541
 
542
- # Sample descriptions
543
- gr.Markdown("**Sample Descriptions:**")
544
- for sample in samples:
545
- gr.Markdown(f"- **{sample['name']}**: {sample['description']}")
546
-
547
  analyze_sample_btn = gr.Button(
548
  "🔍 Analyze Selected ECG",
549
  variant="primary",
 
49
  "Sample 3": "Ventricular Tachycardia - A fast heart rhythm originating from the ventricles, potentially life-threatening.",
50
  }
51
 
52
+ # Reverse mapping: display name to real condition info for analysis results
53
+ DISPLAY_TO_CONDITION = {
54
+ "Sample 1": {
55
+ "name": "Atrial Flutter",
56
+ "description": "A rapid but regular atrial rhythm, typically around 250-350 bpm in the atria."
57
+ },
58
+ "Sample 2": {
59
+ "name": "Normal Sinus Rhythm",
60
+ "description": "A healthy heart rhythm with regular beats originating from the sinus node."
61
+ },
62
+ "Sample 3": {
63
+ "name": "Ventricular Tachycardia",
64
+ "description": "A fast heart rhythm originating from the ventricles, potentially life-threatening."
65
+ },
66
+ }
67
+
68
 
69
  def load_inference_engine():
70
  """Load the inference engine on startup."""
 
99
  return samples
100
 
101
 
102
+ def analyze_ecg(ecg_signal: np.ndarray, filename: str = "ECG Analysis", condition_info: dict = None):
103
  """
104
  Analyze an ECG signal and return all visualizations.
105
 
106
  Args:
107
  ecg_signal: ECG signal array
108
  filename: Name to display
109
+ condition_info: Optional dict with 'name' and 'description' for the condition
110
 
111
  Returns:
112
  Tuple of (ecg_plot, diagnosis_plot, risk_plot, summary_text)
 
154
  else:
155
  severity_class = "severity-high"
156
 
157
+ # Create segmented bar with 10 segments
158
+ total_segments = 10
159
+ filled_segments = int(prob_pct / 10)
160
+ segments_html = ""
161
+ for s in range(total_segments):
162
+ if s < filled_segments:
163
+ segments_html += '<div class="bar-segment"></div>'
164
+ else:
165
+ segments_html += '<div class="bar-segment empty"></div>'
166
+
167
  diagnosis_html += f"""
168
+ <div class="diagnosis-row {severity_class}">
169
+ <span class="diagnosis-rank">#{i}</span>
170
+ <span class="diagnosis-name" title="{class_names[idx]}">{class_names[idx]}</span>
171
+ <div class="diagnosis-bar-container">
172
+ <div class="diagnosis-bar-segments">{segments_html}</div>
173
+ <div class="diagnosis-bar-track">
174
+ <div class="diagnosis-bar-fill" style="width: {prob_pct}%;"></div>
175
+ </div>
176
  </div>
177
+ <span class="diagnosis-percent">{prob_pct:.1f}%</span>
178
  </div>
179
  """
180
 
181
+ # Determine display title and description
182
+ if condition_info:
183
+ display_title = condition_info.get("name", filename)
184
+ condition_desc = condition_info.get("description", "")
185
+ condition_html = f'<p style="color: #666; font-size: 0.95em; margin: 8px 0 16px 0; font-style: italic;">{condition_desc}</p>' if condition_desc else ""
186
+ else:
187
+ display_title = filename
188
+ condition_html = ""
189
+
190
  summary = f"""
191
  <div style="padding: 10px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;">
192
 
193
+ <h2 style="margin: 0 0 8px 0; color: #333;">Analysis Results: {display_title}</h2>
194
+ {condition_html}
195
 
196
  <div style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.9em; margin-bottom: 20px;">
197
  Inference Time: {inference_time:.1f} ms
 
260
  if sample["name"] == sample_name:
261
  try:
262
  ecg_signal = np.load(sample["path"])
263
+ # Get the real condition info for display
264
+ condition_info = DISPLAY_TO_CONDITION.get(sample_name)
265
+ return analyze_ecg(ecg_signal, sample["name"], condition_info)
266
  except Exception as e:
267
  logger.error(f"Error loading sample: {e}")
268
  return None, None, None, f"<p style='color: #dc3545;'>Error loading sample: {str(e)}</p>"
 
411
  box-shadow: 0 2px 8px rgba(76, 175, 80, 0.15);
412
  }
413
 
414
+ /* Minimalistic Diagnosis Styles */
415
  .diagnosis-dashboard {
416
+ padding: 12px 0;
417
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
418
  }
419
 
420
+ .diagnosis-row {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  display: flex;
 
422
  align-items: center;
423
+ padding: 8px 12px;
424
+ margin: 4px 0;
425
+ background: #fafafa;
426
+ border-radius: 6px;
427
+ transition: background 0.15s ease;
428
+ }
429
+
430
+ .diagnosis-row:hover {
431
+ background: #f0f0f0;
432
  }
433
 
434
  .diagnosis-rank {
435
+ font-size: 0.8em;
436
  font-weight: 600;
437
+ color: #888;
438
+ width: 28px;
439
+ flex-shrink: 0;
 
440
  }
441
 
442
  .diagnosis-name {
443
+ font-size: 0.9em;
444
+ font-weight: 500;
445
  color: #333;
446
+ width: 160px;
447
+ flex-shrink: 0;
448
+ white-space: nowrap;
449
+ overflow: hidden;
450
+ text-overflow: ellipsis;
451
  }
452
 
453
+ .diagnosis-bar-container {
454
+ flex: 1;
455
+ display: flex;
456
+ align-items: center;
457
+ margin: 0 12px;
458
+ height: 16px;
459
+ position: relative;
460
  }
461
 
462
+ .diagnosis-bar-track {
463
+ width: 100%;
464
+ height: 3px;
465
+ background: #e0e0e0;
466
+ border-radius: 2px;
467
+ position: relative;
468
  overflow: hidden;
469
  }
470
 
471
+ .diagnosis-bar-fill {
472
+ height: 100%;
473
+ border-radius: 2px;
474
+ transition: width 0.5s ease;
475
+ }
476
+
477
+ .diagnosis-bar-segments {
478
+ position: absolute;
479
+ top: 0;
480
+ left: 0;
481
+ height: 100%;
482
+ display: flex;
483
+ gap: 2px;
484
+ }
485
+
486
+ .bar-segment {
487
+ width: 3px;
488
  height: 100%;
489
+ border-radius: 1px;
490
+ opacity: 0.9;
491
  }
492
 
493
+ .bar-segment.empty {
494
+ background: #e0e0e0;
495
+ opacity: 0.5;
496
  }
497
 
498
+ .diagnosis-percent {
499
+ font-size: 0.85em;
500
+ font-weight: 600;
501
+ width: 55px;
502
+ text-align: right;
503
+ flex-shrink: 0;
504
+ }
505
+
506
+ /* Color classes for severity */
507
+ .severity-low .bar-segment:not(.empty),
508
+ .severity-low .diagnosis-bar-fill {
509
+ background: #20c997;
510
+ }
511
+ .severity-low .diagnosis-percent {
512
+ color: #20c997;
513
  }
514
 
515
+ .severity-medium .bar-segment:not(.empty),
516
+ .severity-medium .diagnosis-bar-fill {
517
+ background: #f0ad4e;
518
+ }
519
+ .severity-medium .diagnosis-percent {
520
+ color: #e09000;
521
+ }
522
+
523
+ .severity-high .bar-segment:not(.empty),
524
+ .severity-high .diagnosis-bar-fill {
525
+ background: #e74c3c;
526
+ }
527
+ .severity-high .diagnosis-percent {
528
+ color: #e74c3c;
529
  }
530
 
531
  /* Footer Styles */
 
619
  info="Click on a sample to select it"
620
  )
621
 
 
 
 
 
 
622
  analyze_sample_btn = gr.Button(
623
  "🔍 Analyze Selected ECG",
624
  variant="primary",