Health Metrics

All health calculations in Health Observability are implemented in worker/utils.js and applied server-side before data is stored. This page documents the medical context, formulae, and implementation details.

Blood Pressure Overview

Blood pressure is expressed as systolic / diastolic in millimetres of mercury (mmHg):

  • Systolic — peak pressure when the heart contracts
  • Diastolic — resting pressure between beats

A measurement session captures three sequential readings to account for natural variability (white-coat effect, measurement artefact, positioning). The system automatically selects the best reading and stores derived metrics.

Best Reading Selection

flowchart TD R["3 readings: R1, R2, R3"] Compare["Compare systolic values"] LowestSys["Reading with lowest systolic wins"] Tie{"Systolic equal?"} LowestDia["Tiebreak: lowest diastolic wins"] Best["Selected as 'best' reading\nis_best = 1"] R --> Compare Compare --> LowestSys LowestSys --> Tie Tie -->|Yes| LowestDia Tie -->|No| Best LowestDia --> Best

Implementation (worker/utils.js):

export function selectBestReading(readings) {
  return readings.reduce((best, current) => {
    if (!best) return current;
    if (current.systolic < best.systolic) return current;
    if (current.systolic === best.systolic && current.diastolic < best.diastolic) return current;
    return best;
  }, null);
}

Only the best reading’s derived metrics (pulse pressure and MAP) are stored in the database. All three readings’ systolic, diastolic, and pulse values are stored for audit purposes.


Derived Metrics

Pulse Pressure (PP)

Pulse pressure is the difference between systolic and diastolic pressure. It reflects arterial stiffness; values consistently above 60 mmHg may indicate cardiovascular risk.

$$PP = \text{Systolic} - \text{Diastolic}$$

Normal range: ~40 mmHg

Mean Arterial Pressure (MAP)

MAP represents the average pressure in the arteries during one cardiac cycle. It is the perfusion pressure that drives blood to organs.

$$MAP = \text{Diastolic} + \frac{PP}{3}$$

This is equivalent to:

$$MAP \approx \frac{\text{Systolic} + 2 \times \text{Diastolic}}{3}$$

Normal range: 70–100 mmHg

Implementation:

export function calculateDerivedMetrics(reading) {
  const pulsePressure = reading.systolic - reading.diastolic;
  const MAP = Math.round((reading.diastolic + pulsePressure / 3) * 10) / 10;
  return { pulsePressure, MAP };
}

MAP is rounded to one decimal place.


AHA/ACC 2017 Blood Pressure Categories

The system implements the 2017 American Heart Association / American College of Cardiology hypertension guidelines. These superseded the JNC 7 guidelines and lowered the threshold for hypertension diagnosis.

flowchart TD Start(["Systolic / Diastolic"]) Start --> N{"Sys < 120\nAND Dia < 80"} N -->|Yes| Normal["🟢 Normal\n< 120 / < 80"] N -->|No| E{"Sys < 130\nAND Dia < 80"} E -->|Yes| Elevated["🟡 Elevated\n120–129 / < 80"] E -->|No| H1{"Sys < 140\nOR Dia < 90"} H1 -->|Yes| Stage1["🟠 High — Stage 1\n130–139 / 80–89"] H1 -->|No| H2{"Sys < 180\nAND Dia < 120"} H2 -->|Yes| Stage2["🔴 High — Stage 2\n≥ 140 / ≥ 90"] H2 -->|No| Crisis["🚨 Hypertensive Crisis\n≥ 180 / ≥ 120"]

Category Reference Table

CategorySystolic (mmHg)Diastolic (mmHg)Badge ColourColour Code
Normal< 120< 80Green#27ae60
Elevated120–129< 80Amber#f39c12
High — Stage 1130–13980–89Dark Orange#e67e22
High — Stage 2≥ 140≥ 90Red#c0392b
Hypertensive Crisis≥ 180≥ 120Dark Red#922b21

Note on Stage 1 logic: The OR condition in Stage 1 means a reading of 138/85 qualifies even though diastolic is below 90, and 125/85 qualifies even though systolic is below 130. This matches the AHA/ACC specification.

Implementation:

export function bpCategory(systolic, diastolic) {
  if (systolic < 120 && diastolic < 80)  return { label: 'Normal',              color: '#27ae60' };
  if (systolic < 130 && diastolic < 80)  return { label: 'Elevated',             color: '#f39c12' };
  if (systolic < 140 || diastolic < 90)  return { label: 'High — Stage 1',       color: '#e67e22' };
  if (systolic < 180 && diastolic < 120) return { label: 'High — Stage 2',       color: '#c0392b' };
  return                                        { label: 'Hypertensive crisis',   color: '#922b21' };
}

Input Validation

Server-side validation is applied to all readings before any database operation.

flowchart TD Input["readings array"] IsArray{"Array of exactly 3?"} IsArray -->|No| E1["Error: Exactly three readings required"] IsArray -->|Yes| Loop["For each reading i…"] Loop --> IsObj{"Is object?"} IsObj -->|No| E2["Reading i must be an object"] IsObj -->|Yes| SysCheck{"50 ≤ systolic ≤ 300?"} SysCheck -->|No| E3["Reading i: systolic out of range"] SysCheck -->|Yes| DiaCheck{"30 ≤ diastolic ≤ 200?"} DiaCheck -->|No| E4["Reading i: diastolic out of range"] DiaCheck -->|Yes| PulCheck{"20 ≤ pulse ≤ 300?"} PulCheck -->|No| E5["Reading i: pulse out of range"] PulCheck -->|Yes| Order{"diastolic < systolic?"} Order -->|No| E6["Reading i: diastolic must be < systolic"] Order -->|Yes| Next["Next reading / Pass"]

Validation Ranges

FieldMinMaxUnit
Systolic50300mmHg
Diastolic30200mmHg
Pulse20300bpm
Weight20300kg
Height50280cm

CPAP / Sleep Metrics

CPAP data is sourced from the ResMed MyAir service. The key clinical metrics are:

MetricFieldUnitClinical Significance
AHIahievents/hourApnea-Hypopnea Index; < 5 is normal, 5–15 mild OSA, > 30 severe
Usageusage_minutesminutesTherapeutic compliance; ≥ 4h/night is typical compliance threshold
Mask Leakmask_leak_pct% of nightHigh leak reduces CPAP efficacy
MyAir Scoremyair_score0–100ResMed composite score; ≥ 70 considered good
Pressurepressure_min / pressure_maxcmH₂OAPAP pressure range used during the night

CPAP Score Colour Coding (Frontend)

The history table colours the MyAir score for quick visual assessment:

ScoreColourClass
≥ 70Green #27ae60Good
50–69Orange #e67e22Fair
< 50Red #c0392bPoor

Sleep–BP Correlation

The correlation chart juxtaposes each CPAP night’s sleep score against the next morning’s blood pressure reading. The hypothesis is that poor sleep quality (low score, high AHI) correlates with elevated morning blood pressure.

The join is performed by the external CPAP worker’s /api/cpap-correlation endpoint, which matches cpap_data.log_date to the first blood pressure session recorded on log_date + 1 day.

xychart-beta title "Example: Sleep Score vs Next-Morning Systolic" x-axis ["Mon", "Tue", "Wed", "Thu", "Fri"] y-axis "Value" 50 --> 140 bar [72, 85, 61, 90, 78] line [128, 122, 134, 118, 125]

(Illustrative data — values shown are examples only)