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_configtable (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
- Always include: axis labels with units, legend, data point tooltips
- Threshold lines: Horizontal dashed lines for clinical normal ranges
- Color accessibility: Patterns/textures alongside colors; data tables as fallback
- Max 6 slices on pie/donut; group remainder as "Other"
- Adjacent legend -- never overlap labels on the chart
- Responsive: Charts resize with viewport; touch-friendly tooltips on mobile
- 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 |
|---|---|---|
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 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_schedulestable - Print layouts:
@media printhides nav/alerts/interactive elements; followreport-print-pdfskill
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 |