# 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 ```html
Bed Occupancy

82%

+3%
``` ```javascript // 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 ```kotlin @Composable fun KpiCard( icon: ImageVector, label: String, value: String, trend: String, isPositiveTrend: Boolean, progress: Float? = null, sparklineData: List = 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 ```html
3A-01
J. Doe
3A-03
Available
``` ### 3.4 Android: Bed Grid Composable ```kotlin @Composable fun WardBedGrid(beds: List, 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 ```javascript 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 ```kotlin @Composable fun SparklineChart(data: List, 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 ```kotlin @Composable fun RiskPatientList(patients: List) { 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 ```javascript 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 ```kotlin @Composable fun AlertFeed(alerts: List, 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`](design-tokens.md) | | Patient records and lookup | [`patient-records-ui.md`](patient-records-ui.md) | | Vital signs and medications | [`clinical-workflows-ui.md`](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`](web-implementation.md) | | Android Compose patterns | [`android-implementation.md`](android-implementation.md) | | HIPAA and accessibility | [`compliance-accessibility.md`](compliance-accessibility.md) |