skills/healthcare-ui-design/references/dashboards-analytics-ui.md

21 KiB

Dashboards & Analytics UI

Cross-platform healthcare dashboard and analytics patterns for Web (Bootstrap 5/Tabler + PHP) and Android (Jetpack Compose + Material 3). Covers role-based dashboards, KPI metric cards, bed occupancy, data visualization, risk scores, real-time alerts, and report generation.


1. Dashboard Architecture

1.1 Role-Based Dashboards

Each role sees a tailored default layout. Never build one-size-fits-all.

Role Primary Widgets Priority
Clinician Today's patients, pending tasks, unread messages, recent lab results Speed -- scan in under 5s
Nurse Station Ward overview, bed status grid, vital alerts, medication due, handoff notes Task flow -- zero missed actions
Admin Facility KPIs, revenue trends, bed occupancy %, staff utilization, satisfaction Analytics -- drill-down capability
Patient Upcoming appointments, medications, health goals, messages, lab results Clarity -- plain language, large targets

1.2 Customizable Widget System

  • Drag-and-drop placement: Users reorder widgets within a grid layout
  • Widget catalog: Modal picker to add/remove widgets per preference
  • Layout save/restore: Persist per-user in user_dashboard_config table (JSON column)
  • Refresh intervals: Per-widget poll interval (30s vitals, 5min KPIs, configurable)
  • Default layouts: Curated per role; user customization overlays the default

1.3 Dashboard Wireframes

Web (Tabler): Sidebar + 3-col KPI row + chart/alert split + bed grid
┌──────┬──────────────────────────────────────┐
│ Nav  │ [KPI] [KPI] [KPI] / [KPI] [KPI] [KPI]│
│ Side │ [Chart — Line/Bar] [Alert Feed Panel]  │
│ bar  │ [Bed Occupancy Grid — full width]      │
└──────┴──────────────────────────────────────┘

Android (Compose): TopAppBar + 2-col KPI grid + chart + alert feed
┌──────────────────────────┐
│ TopAppBar: Dashboard      │
├──────────────────────────┤
│ [KPI] [KPI] / [KPI] [KPI]│
│ [Line Chart — Trends]     │
│ [Alert Feed — LazyColumn] │
├──────────────────────────┤
│ BottomNav: Home|Patients  │
└──────────────────────────┘

2. KPI Metric Cards

2.1 Card Anatomy

┌──────────────────────────┐
│ [Icon]  Metric Label     │
│ 1,247                    │
│ +12% arrow-up vs last mo │
│ progressbar 78%          │
└──────────────────────────┘

Components: icon (semantic color), label, large value, trend indicator, optional progress bar or sparkline.

2.2 Healthcare KPIs

KPI Unit Good Trend Threshold Example
Patient Volume count/day Context-dependent > 50/day = high
Average Wait Time minutes Down is good < 15 green, < 30 amber, > 30 red
Bed Occupancy Rate % 75-85% optimal < 70 under, > 90 critical
Average Length of Stay days Down is good Varies by department
Readmission Rate % Down is good < 5% green, < 10% amber, > 10% red
Patient Satisfaction score/5 Up is good > 4.2 green, > 3.5 amber, < 3.5 red
Revenue per Dept currency Up is good vs. budget target
Staff-to-Patient Ratio ratio Context-dependent Meets regulatory minimums
Prescription Fill Rate % Up is good > 95% green
Lab Turnaround Time hours Down is good < 2h green, < 4h amber, > 4h red

2.3 Color Coding and Trend Arrows

  • Green (--clinical-success): Target met or exceeded
  • Amber (--clinical-warning): Approaching threshold
  • Red (--clinical-critical): Below target / critical
  • Trend arrows: up = improving, right = stable, down = declining
  • Context matters: For wait time and readmission rate, down is good (green). For satisfaction, down is bad (red). Always pair arrow direction with color.

2.4 Web: Tabler KPI Card with Sparkline

<div class="col-sm-6 col-lg-3">
  <div class="card">
    <div class="card-body">
      <div class="d-flex align-items-center mb-2">
        <span class="badge bg-success-lt p-2 me-2">
          <i class="ti ti-heartbeat icon"></i>
        </span>
        <span class="text-secondary">Bed Occupancy</span>
      </div>
      <div class="d-flex align-items-baseline">
        <h1 class="mb-0 me-2">82%</h1>
        <span class="text-success d-inline-flex align-items-center lh-1">
          <i class="ti ti-trending-up me-1"></i> +3%
        </span>
      </div>
      <canvas id="sparkline-occupancy" height="30" class="mt-2"></canvas>
      <div class="progress progress-sm mt-2">
        <div class="progress-bar bg-success" style="width: 82%"></div>
      </div>
    </div>
  </div>
</div>
// Chart.js sparkline — minimal config, no axes
new Chart(document.getElementById('sparkline-occupancy'), {
  type: 'line',
  data: {
    labels: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'],
    datasets: [{ data: [76,78,80,79,82,85,82], borderColor: '#059669',
                 borderWidth: 2, fill: false, pointRadius: 0, tension: 0.4 }]
  },
  options: {
    plugins: { legend: { display: false } },
    scales: { x: { display: false }, y: { display: false } },
    responsive: true, maintainAspectRatio: false
  }
});

2.5 Android: KPI Card Composable

@Composable
fun KpiCard(
    icon: ImageVector, label: String, value: String,
    trend: String, isPositiveTrend: Boolean,
    progress: Float? = null, sparklineData: List<Float> = emptyList(),
    modifier: Modifier = Modifier
) {
    val trendColor = if (isPositiveTrend) ClinicalSuccess else ClinicalCritical
    Card(modifier = modifier.fillMaxWidth(),
         colors = CardDefaults.cardColors(containerColor = ClinicalSurface)) {
        Column(Modifier.padding(16.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Icon(icon, label, tint = ClinicalPrimary, modifier = Modifier.size(24.dp))
                Spacer(Modifier.width(8.dp))
                Text(label, style = MaterialTheme.typography.bodyMedium, color = ClinicalTextSecondary)
            }
            Spacer(Modifier.height(8.dp))
            Row(verticalAlignment = Alignment.Bottom) {
                Text(value, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
                Spacer(Modifier.width(8.dp))
                Text(trend, color = trendColor, style = MaterialTheme.typography.bodySmall)
            }
            if (sparklineData.isNotEmpty()) {
                Spacer(Modifier.height(8.dp))
                SparklineChart(sparklineData, trendColor, Modifier.fillMaxWidth().height(30.dp))
            }
            progress?.let {
                Spacer(Modifier.height(8.dp))
                LinearProgressIndicator(progress = { it },
                    modifier = Modifier.fillMaxWidth().height(4.dp).clip(RoundedCornerShape(2.dp)),
                    color = trendColor, trackColor = ClinicalBorder)
            }
        }
    }
}

3. Bed Occupancy Management

3.1 Visual Ward Layout

Ward 3A — 20 Beds (16 Occupied, 2 Available, 1 Cleaning, 1 Maintenance)
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│3A-01 │ │3A-02 │ │3A-03 │ │3A-04 │ │3A-05 │
│ Blue │ │ Blue │ │Green │ │ Blue │ │ Red  │
│J.Doe │ │M.Lee │ │      │ │R.Kim │ │Clean │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
Status Color Hex Meaning
Available Green #059669 Ready for admission
Occupied Blue #2563EB Patient assigned
Needs Cleaning Red #DC2626 Post-discharge, not ready
Maintenance Gray #64748B Out of service

3.2 Occupancy Metrics and Projection

Display above ward grid: Total Beds, Occupied %, Available Count, Expected Discharges Today, Pending Admissions. Include a 7-day projected occupancy line chart with a horizontal threshold at 90% (critical capacity).

3.3 Web: CSS Grid Ward Layout

<div class="ward-grid" style="display:grid; grid-template-columns:repeat(5,1fr); gap:8px;">
  <div class="bed-cell bg-primary-lt text-center p-2 rounded"
       data-bs-toggle="tooltip" title="John Doe — Admitted 2026-02-20 — Cardiology">
    <div class="fw-bold">3A-01</div><small>J. Doe</small>
  </div>
  <div class="bed-cell bg-success-lt text-center p-2 rounded">
    <div class="fw-bold">3A-03</div><small>Available</small>
  </div>
  <!-- Repeat per bed -->
</div>

3.4 Android: Bed Grid Composable

@Composable
fun WardBedGrid(beds: List<BedInfo>, onBedClick: (BedInfo) -> Unit) {
    LazyVerticalGrid(columns = GridCells.Fixed(4), contentPadding = PaddingValues(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)) {
        items(beds, key = { it.bedId }) { bed -> BedCell(bed, onClick = { onBedClick(bed) }) }
    }
}

@Composable
fun BedCell(bed: BedInfo, onClick: () -> Unit) {
    val bgColor = when (bed.status) {
        BedStatus.AVAILABLE -> ClinicalSuccess.copy(alpha = 0.15f)
        BedStatus.OCCUPIED -> ClinicalPrimary.copy(alpha = 0.15f)
        BedStatus.CLEANING -> ClinicalCritical.copy(alpha = 0.15f)
        BedStatus.MAINTENANCE -> ClinicalTextMuted.copy(alpha = 0.15f)
    }
    Card(onClick = onClick, colors = CardDefaults.cardColors(containerColor = bgColor)) {
        Column(Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
            Text(bed.bedNumber, fontWeight = FontWeight.Bold)
            Text(bed.patientName ?: bed.status.label,
                 style = MaterialTheme.typography.bodySmall, color = ClinicalTextSecondary)
        }
    }
}

4. Data Visualization Patterns

4.1 Chart Type Selection Guide

Chart Type Use For Healthcare Example
Line Trends over time Vital sign trends, patient volume, revenue
Bar (Vertical) Time-series comparison Monthly admissions, quarterly revenue
Bar (Horizontal) Category comparison Department comparison, top diagnoses
Pie/Donut Part-of-whole (max 6 + Other) Insurance mix, disease distribution
Heatmap Density/correlation Appointment slots by hour/day
Gauge Single KPI progress Occupancy %, satisfaction score
Funnel Sequential stage drop-off Admission to follow-up journey

4.2 Clinical Chart Rules

  1. Always include: axis labels with units, legend, data point tooltips
  2. Threshold lines: Horizontal dashed lines for clinical normal ranges
  3. Color accessibility: Patterns/textures alongside colors; data tables as fallback
  4. Max 6 slices on pie/donut; group remainder as "Other"
  5. Adjacent legend -- never overlap labels on the chart
  6. Responsive: Charts resize with viewport; touch-friendly tooltips on mobile
  7. Libraries: Web uses Chart.js or ApexCharts with Tabler; Android uses Vico or Compose Canvas

4.3 Web: Chart.js Line with Clinical Thresholds

new Chart(document.getElementById('vitalTrend'), {
  type: 'line',
  data: {
    labels: timeLabels,
    datasets: [{ label: 'Heart Rate (bpm)', data: heartRateData,
                 borderColor: '#2563EB', tension: 0.3, fill: false }]
  },
  options: {
    plugins: { annotation: { annotations: {
      highThreshold: { type: 'line', yMin: 100, yMax: 100,
        borderColor: '#DC2626', borderDash: [6,4], label: { content: 'High', display: true } },
      lowThreshold: { type: 'line', yMin: 60, yMax: 60,
        borderColor: '#D97706', borderDash: [6,4], label: { content: 'Low', display: true } }
    }}},
    scales: {
      y: { title: { display: true, text: 'BPM' } },
      x: { title: { display: true, text: 'Time' } }
    }
  }
});

4.4 Android: Compose Canvas Sparkline

@Composable
fun SparklineChart(data: List<Float>, color: Color, modifier: Modifier = Modifier) {
    Canvas(modifier = modifier) {
        if (data.size < 2) return@Canvas
        val maxVal = data.max(); val minVal = data.min()
        val range = (maxVal - minVal).coerceAtLeast(1f)
        val stepX = size.width / (data.size - 1)
        val path = Path().apply {
            data.forEachIndexed { i, v ->
                val x = i * stepX
                val y = size.height - ((v - minVal) / range) * size.height
                if (i == 0) moveTo(x, y) else lineTo(x, y)
            }
        }
        drawPath(path, color, style = Stroke(width = 2.dp.toPx(), cap = StrokeCap.Round))
    }
}

5. Risk Score and Population Health

5.1 Patient Risk Stratification

  • Histogram/bell curve: Risk score distribution across patient population
  • High-risk list: Table sorted by score descending, with score breakdown tooltip
  • Stacked bar: Contribution of risk factors per patient (age, comorbidities, utilization)

5.2 Population Health Metrics

Metric Visualization Details
Chronic disease prevalence Horizontal bar Grouped by condition (diabetes, HTN, COPD)
Preventive care completion Progress bars Screenings, vaccinations, wellness checks
Age/gender distribution Population pyramid Stacked horizontal bars, M left, F right
RUB distribution Donut chart Resource Utilization Band segments
ACG/HCC risk detail Expandable panel Score components with weight contribution

5.3 Web Implementation

Use DataTables with expandable rows for risk factor breakdown. Chart.js donut overlays inside expanded rows show weight contribution visually.

5.4 Android: Expandable Risk List

@Composable
fun RiskPatientList(patients: List<RiskPatient>) {
    LazyColumn {
        items(patients, key = { it.patientId }) { patient ->
            var expanded by remember { mutableStateOf(false) }
            Card(onClick = { expanded = !expanded },
                 modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) {
                Column(Modifier.padding(12.dp)) {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        RiskScoreBadge(score = patient.riskScore)
                        Spacer(Modifier.width(12.dp))
                        Column(Modifier.weight(1f)) {
                            Text(patient.name, fontWeight = FontWeight.SemiBold)
                            Text("MRN: ${patient.mrn}",
                                 style = MaterialTheme.typography.bodySmall, color = ClinicalTextMuted)
                        }
                        Icon(if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                             contentDescription = "Toggle details")
                    }
                    AnimatedVisibility(visible = expanded) {
                        Column(Modifier.padding(top = 8.dp)) {
                            patient.riskFactors.forEach { factor ->
                                RiskFactorRow(name = factor.name, weight = factor.weight)
                            }
                        }
                    }
                }
            }
        }
    }
}

6. Real-Time Alerts and Monitoring

6.1 Alert Feed Design

Chronological list panel with priority sorting: Critical first, then Warning, then Info.

┌──────────────────────────────────────┐
│ Alerts (3 unread)            Filter  │
├──────────────────────────────────────┤
│ [!] CRITICAL 14:32                   │
│ Bed 3A-07: SpO2 dropped to 88%      │
│ [Acknowledge] [Assign] [Escalate]    │
├──────────────────────────────────────┤
│ [!] WARNING 14:15                    │
│ Lab: Potassium 5.8 mEq/L — Room 204 │
│ [Acknowledge] [Dismiss]              │
├──────────────────────────────────────┤
│ [i] INFO 13:50                       │
│ Discharge orders signed — Bed 2B-12  │
│ [Dismiss]                            │
└──────────────────────────────────────┘

6.2 Alert Rules

  • Critical: Blocks workflow until acknowledged. Audio notification (configurable, visual fallback for muted environments). Never auto-dismiss.
  • Warning: Prominent banner, dismissible after 3s minimum display.
  • Info: Auto-dismiss after 10s, always in feed history.
  • Badge: Dashboard icon shows unread count. Red dot for critical unread.

6.3 Web: Server-Sent Events Feed

const alertSource = new EventSource('/api/alerts/stream');
alertSource.addEventListener('clinical-alert', function(e) {
  const alert = JSON.parse(e.data);
  prependAlertToFeed(alert);
  if (alert.priority === 'critical') { playAlertSound(); showBlockingModal(alert); }
  updateAlertBadge();
});

Use Tabler notification panel styling. Critical alerts: red-bordered cards with pulsing left border.

6.4 Android: Alert Feed Composable

@Composable
fun AlertFeed(alerts: List<ClinicalAlert>, onAction: (ClinicalAlert, AlertAction) -> Unit) {
    LazyColumn(Modifier.fillMaxSize()) {
        items(alerts, key = { it.alertId }) { alert ->
            val borderColor = when (alert.priority) {
                AlertPriority.CRITICAL -> ClinicalCritical
                AlertPriority.WARNING -> ClinicalWarning
                AlertPriority.INFO -> ClinicalInfo
            }
            Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)
                .border(3.dp, borderColor, RoundedCornerShape(8.dp))) {
                Column(Modifier.padding(12.dp)) {
                    Row {
                        PriorityBadge(alert.priority)
                        Spacer(Modifier.weight(1f))
                        Text(alert.timestamp, style = MaterialTheme.typography.bodySmall,
                             color = ClinicalTextMuted)
                    }
                    Spacer(Modifier.height(4.dp))
                    Text(alert.message, style = MaterialTheme.typography.bodyMedium)
                    Spacer(Modifier.height(8.dp))
                    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                        alert.availableActions.forEach { action ->
                            OutlinedButton(onClick = { onAction(alert, action) }) {
                                Text(action.label, style = MaterialTheme.typography.labelSmall)
                            }
                        }
                    }
                }
            }
        }
    }
}

7. Report Generation

7.1 Quick Report Builder

Report configuration panel: date range picker (Flatpickr web / DatePicker Android), department multi-select, metric checklist, format selector (PDF, CSV, Excel, Print).

7.2 Export Formats

Format Web Approach Android Approach
PDF mPDF backend (see report-print-pdf skill) PdfDocument API (see android-pdf-export skill)
CSV PHP fputcsv with UTF-8 BOM Kotlin FileWriter to cache, share via Intent
Excel PhpSpreadsheet library Apache POI or CSV fallback
Print Print CSS @media print (see report-print-pdf) PrintManager framework

7.3 Scheduled Reports and Print

  • Configure: frequency (daily/weekly/monthly), recipients, template, filters
  • Backend cron generates and emails PDF attachments; store in report_schedules table
  • Print layouts: @media print hides nav/alerts/interactive elements; follow report-print-pdf skill

Cross-Reference

Topic Skill / Reference
Design tokens and colors design-tokens.md
Patient records and lookup patient-records-ui.md
Vital signs and medications clinical-workflows-ui.md
PDF report generation (Web) report-print-pdf skill
PDF export (Android) android-pdf-export skill
Web component patterns web-implementation.md
Android Compose patterns android-implementation.md
HIPAA and accessibility compliance-accessibility.md