📑 Table of Contents
🏠 1. Overview & Getting Started
CardioAnnotate is a browser-based ECG annotation toolkit that allows you to:
- Load and visualize ECG signals from WFDB files, CSV, or NPZ bundles
- Manually annotate R, P, T, U, Q, and S peaks across multiple channels
- Classify R-peaks by beat type using 19 WFDB annotation standards
- Automatic peak detection using Find Peaks or Pan-Tompkins algorithms
- Import WFDB annotation files (
.atrand wave-specific annotation files) - Apply signal processing (resampling, filtering, inversion)
- Compute Signal Quality Indices (SQI) for the visible segment
- Compute HRV metrics (time-domain and frequency-domain) for the visible segment
- Detect and correct RR interval outliers using Gaussian Process regression
- Export annotations in JSON, CSV, WFDB, or NPZ format with enhanced options
- Export and import full sessions in JSON, MAT, or NPZ format to restore the complete analysis state
- View real-time ECG statistics and intervals
- Toggle between dark and light themes
💡 100% Privacy-Preserving
All processing happens in your browser using Pyodide (Python in WebAssembly). No data is uploaded to any server.
Peak Types
📂 2. Loading ECG Data
Option A: Load WFDB Files
- Click "Choose Files" in the Load section
- Select both
.hea(header) and.dator.mat(signal) files with the same basename- Example:
100.hea+100.dat
- Example:
- The signal loads automatically after the file selection is complete
💡 Where to Find WFDB Files
Download ECG databases from PhysioNet (MIT-BIH, LUDB, INCART, etc.)
Loading Status
A small spinner appears next to the signal file input while a signal is loading. It disappears automatically when loading finishes, whether the file loads successfully or an error is reported.
Option B: Load CSV Files
- Click "Load" button in the "Import from CSV" row
- Select a CSV file containing ECG data
CSV Requirements:
- First row must be a header with column names
- Must include one of:
fsorsampling_ratecolumn, ORtime_sortimecolumn (sampling rate will be calculated)
- Signal columns should contain numeric values
- Example columns:
fs, time_s, MLII, V1, V2
Option C: Load NPZ Bundle
- Click "Load" button in the "Import bundle (.npz)" row
- Select an NPZ file previously exported from CardioAnnotate
💡 NPZ Bundles
NPZ bundles are a convenient format that packages the processed signal data, sampling frequency, channel names, and all peak annotations into a single compressed file. Use a full session export when you need to restore the complete workspace, including settings, view window, corrections, and computed results.
Option D: Load Full Session
- Click the session import control in the Session section
- Select a CardioAnnotate session file in
.json,.mat, or.npzformat - The file format is detected automatically, and the workspace is restored from the saved session state
Full Session Restore
Session import restores the signal viewer, annotations, RR corrections, processing settings, detector settings, selected channel, zoom window, and saved analysis results. SQI, HRV, and stationarity panels are recomputed after import so that the displayed values reproduce the saved session.
📁 3. File Formats (Input & Output)
3.1 Input File Formats
WFDB Format (.hea + .dat/.mat)
The PhysioNet standard format for physiological signals, consisting of two files:
Header File (.hea) Structure:
record_name n_signals sampling_frequency n_samples filename format gain baseline units resolution 0 0 channel_name filename format gain baseline units resolution 0 0 channel_name ... # Optional comment lines starting with #
Example .hea file:
100 2 360 650000 100.dat 212 200 1024 mV 11 0 0 MLII 100.dat 212 200 1024 mV 11 0 0 V5 # Age: 69 Sex: M
Field descriptions:
| Field | Description | Example |
|---|---|---|
| record_name | Base name of the record | 100 |
| n_signals | Number of channels | 2 |
| sampling_frequency | Samples per second (Hz) | 360 |
| n_samples | Total samples per channel | 650000 |
| filename | Signal data file name | 100.dat |
| format | Storage format (212=12-bit, 16=16-bit, 80=8-bit offset) | 212 |
| gain | ADC units per physical unit | 200 |
| baseline | ADC zero value | 1024 |
| units | Physical units | mV |
| resolution | ADC resolution in bits | 11 |
| channel_name | Signal/lead name | MLII |
Signal File (.dat) Structure:
- Binary file containing interleaved signal samples
- Format 212: 12-bit samples, 2 samples packed in 3 bytes (little-endian)
- Format 16: 16-bit signed integers, 2 bytes per sample (little-endian)
- Format 80: 8-bit unsigned with offset 128
- Physical value = (ADC_value − baseline) / gain
Format 212 Byte Layout
Two 12-bit samples (A, B) stored in 3 bytes:
Byte0 = A[7:0], Byte1 = B[3:0]|A[11:8], Byte2 = B[11:4]
CSV Format (.csv)
Comma-separated values with a header row.
Structure:
fs,time_s,MLII,V1,V5 360,0.000000,0.123,-0.045,0.089 360,0.002778,0.125,-0.043,0.091 360,0.005556,0.128,-0.041,0.094 360,0.008333,0.132,-0.038,0.098 ...
Required columns (one of the following):
| Column Name(s) | Description |
|---|---|
fs, sampling_rate, or sampling_frequency | Sampling rate in Hz (same value repeated each row) |
time_s or time | Time in seconds (fs calculated from Δtime) |
Signal columns:
- Any numeric column not in the exclude list becomes a channel
- Excluded column names:
fs,sampling_rate,sampling_frequency,time_s,time,index,sample - Values should be in physical units (e.g., mV)
NPZ Bundle Format (.npz)
Compressed NumPy archive containing signal data and metadata.
Structure:
{
"signals": numpy.ndarray, // Shape: (n_channels, n_samples), dtype: float64
"meta": JSON string // Metadata as serialized JSON
}
Meta JSON structure:
{
"fs": 360, // Sampling frequency in Hz
"channelNames": ["MLII", "V1", "V5"], // Array of channel names
"units": ["mV", "mV", "mV"], // Units per channel (optional)
"recordBase": "100", // Original record name (optional)
"annotations": { // Peak annotations
"R": {
"0": [ // Channel index as string key
{"index": 77, "type": "N"}, // R-peaks: objects with index and type
{"index": 370, "type": "N"},
{"index": 662, "type": "V"}
],
"1": [{"index": 79, "type": "N"}]
},
"P": {
"0": [30, 320, 610], // Other peaks: arrays of sample indices
"1": [32, 322]
},
"Q": {"0": [70, 363, 655]},
"S": {"0": [85, 378, 670]},
"T": {"0": [180, 475, 765]},
"U": {"0": []}
}
}
Legacy bundles that use peaks for the annotation map may still be loaded when supported by the importer.
Annotation JSON Format (.json)
JSON file containing peak annotations only (no signal data).
Structure:
{
"fs": 360,
"channelNames": ["MLII", "V1"],
"annotations": {
"R": {
"0": [
{"index": 77, "type": "N"},
{"index": 370, "type": "N"},
{"index": 662, "type": "V"},
{"index": 955, "type": "A"}
],
"1": [
{"index": 79, "type": "N"},
{"index": 372, "type": "N"}
]
},
"P": {
"0": [30, 320, 610, 905],
"1": [32, 322, 612]
},
"Q": {
"0": [70, 363, 655, 948],
"1": []
},
"S": {
"0": [85, 378, 670, 963],
"1": []
},
"T": {
"0": [180, 475, 765, 1060],
"1": [182, 477]
},
"U": {
"0": [],
"1": []
}
}
}
Key points:
- The top-level annotation map is named
annotations, following theecg-annotator-json-v2schema - R-peaks: Array of objects with
{"index": sample_number, "type": "beat_code"} - P, Q, S, T, U peaks: Arrays of sample indices (integers)
- Channel indices are string keys ("0", "1", "2", ...)
- JSON files that use the legacy top-level
peakskey are also supported for import - Legacy format (R-peaks as simple integer arrays) is also supported for import
Annotation CSV Format (.csv)
Tabular format for peak annotations.
Structure:
channel_index,label,index,time_s,beat_type 0,R,77,0.214,N 0,R,370,1.028,N 0,R,662,1.839,V 0,R,955,2.653,A 0,P,30,0.083, 0,P,320,0.889, 0,Q,70,0.194, 0,S,85,0.236, 0,T,180,0.500, 1,R,79,0.219,N 1,P,32,0.089,
Column descriptions:
| Column | Required | Description |
|---|---|---|
channel_index | Yes | Zero-indexed channel number |
label | Yes | Peak type: R, P, Q, S, T, or U |
index | Yes* | Sample index (integer) |
time_s | Yes* | Time in seconds (alternative to index) |
beat_type | No | WFDB beat code for R-peaks (N, V, A, L, R, etc.) |
*Either index or time_s is required.
WFDB Annotation Import Files (.atr / wave annotations)
WFDB annotation files can be imported after a signal is loaded. The file picker accepts all file extensions so that non-standard WFDB annotation extensions remain selectable.
Supported import forms:
| File Type | Import Behavior |
|---|---|
.atr | Symbol-based import. Beat annotation codes such as N, L, V, A, and related WFDB beat codes are imported as R-peaks with the beat type preserved. Wave symbols are imported as p → P, t → T, u → U, q → Q, and s → S. |
.qrs | All annotations in the file are imported as R-peaks. |
.pwave | All annotations in the file are imported as P-peaks. |
.twave | All annotations in the file are imported as T-peaks. |
.uwave | All annotations in the file are imported as U-waves. |
.qwave | All annotations in the file are imported as Q-waves. |
.swave | All annotations in the file are imported as S-waves. |
Recognized extension variants are mapped by their extension name. Imported WFDB annotations are merged into all loaded channels.
3.2 Output File Formats
Annotation JSON Export
Same structure as input JSON, with added export timestamp and the annotations top-level key.
Structure:
{
"exportedAt": "2026-01-15T10:30:00.000Z",
"fs": 360,
"channelNames": ["MLII", "V1"],
"annotations": {
"R": {
"0": [
{"index": 77, "type": "N"},
{"index": 370, "type": "V"}
]
},
"P": {"0": [30, 320]},
"Q": {"0": [70, 363]},
"S": {"0": [85, 378]},
"T": {"0": [180, 475]},
"U": {"0": []}
}
}
Annotation CSV Export
Tabular export with full metadata per peak.
Structure:
peak_type,channel_index,channel_name,sample_index,time_s,beat_type R,0,MLII,77,0.214,N R,0,MLII,370,1.028,V R,0,MLII,662,1.839,A P,0,MLII,30,0.083, P,0,MLII,320,0.889, Q,0,MLII,70,0.194, S,0,MLII,85,0.236, T,0,MLII,180,0.500, R,1,V1,79,0.219,N
Column descriptions:
| Column | Description |
|---|---|
peak_type | R, P, Q, S, T, or U |
channel_index | Zero-indexed channel number |
channel_name | Human-readable channel label |
sample_index | Peak location in samples |
time_s | Peak time = sample_index / fs |
beat_type | WFDB code (R-peaks only, empty for others) |
ECG Signal CSV Export
Exports the current (possibly processed) signal data.
Structure:
fs,time_s,MLII,V1,V5 360,0.000000,0.123,-0.045,0.089 360,0.002778,0.125,-0.043,0.091 360,0.005556,0.128,-0.041,0.094 360,0.008333,0.132,-0.038,0.098 ...
Notes:
fscolumn contains the sampling frequency (repeated each row)time_sis computed as sample_index / fs- Signal columns contain values after any processing (filtering, resampling)
NPZ Bundle Export
Signal-and-annotation bundle with processed signals and all annotations.
Structure:
numpy.savez_compressed("export.npz",
signals=numpy.array(...), // Shape: (n_channels, n_samples), dtype: float64
meta=json.dumps({
"fs": 360,
"channelNames": ["MLII", "V1"],
"units": ["mV", "mV"],
"recordBase": "100",
"annotations": { ... } // Same structure as annotation JSON
})
)
3.3 Full Session Format
A full session file stores the complete state of an analysis in a single, self-contained container. Use this format when an analysis must be paused and resumed, archived, shared with collaborators, or reopened exactly as it was left.
Recorded Session State
| State Category | What Is Stored |
|---|---|
| Signal data | The current processed waveform for every channel and the original unmodified signal, together with sampling frequency, channel names, physical units, and record identifier. Retaining both processed and original signals keeps filtering, resampling, inversion, and reset available after reload. |
| Annotations and corrections | All fiducial annotations, R-peak beat-type labels, RR intervals flagged by quality control, and any RR series corrected by Gaussian Process regression. |
| Processing and analysis settings | Filtering and resampling parameters, per-peak-type detector settings for Find Peaks and Pan-Tompkins, RR quality-control rules, selected GP kernel and hyperparameters, SQI parameters, and interface-control state. |
| View and selection | The active channel and current zoom window. Because SQI and HRV are evaluated on the visible segment, saving the window fixes the exact sample range used for segment-level analysis. |
| Computed results | A provenance snapshot of reported measures, including pSQI, kSQI, basSQI, lineSQI; time- and frequency-domain HRV metrics; the ADF stationarity test; and summary statistics. |
Session Container Formats
Sessions can be exported as .json, .mat, or .npz. All three contain the same analysis state. In the .mat and .npz containers, signal arrays are stored as native numeric arrays for direct use in MATLAB or Python, while annotations, settings, view state, corrections, and results are embedded as a JSON record in the same container.
Session JSON structure (conceptual):
{
"sessionVersion": "full-state-v1",
"exportedAt": "2026-01-15T10:30:00.000Z",
"signals": {
"processed": [[...]],
"original": [[...]],
"fs": 360,
"channelNames": ["MLII", "V1"],
"units": ["mV", "mV"],
"recordBase": "100"
},
"annotations": { ... },
"corrections": {
"flaggedRR": [...],
"gpCorrectedRR": [...]
},
"settings": { ... },
"view": {
"activeChannel": 0,
"xRange": [0.0, 10.5]
},
"results": {
"sqi": { ... },
"hrv": { ... },
"adf": { ... },
"summary": { ... }
}
}
Export Type Coverage
| State | Annotations (JSON/CSV) |
Signal Bundle (NPZ) |
Session (full state) |
|---|---|---|---|
| Signal data (processed + original) | ✓ | ✓ | |
| Peaks and beat-type labels | ✓ | ✓ | ✓ |
| Flagged / GP-corrected RR | ✓ | ||
| Processing and detector settings | ✓ | ||
| View / segment selection | ✓ | ||
| Computed results (SQI, HRV, ADF) | ✓ |
💡 When to Use a Full Session
Use annotation exports for peak labels only, NPZ bundles for signal-plus-annotation exchange, and full sessions when the workspace must reopen with the same processing pipeline, correction state, view window, and analysis panels.
WFDB Export
PhysioNet-compatible format with header, signal, and annotation files.
Files created:
Header file (record.hea):
export 2 360 650000 export.dat 16 200(0) 0 16 0 0 0 MLII export.dat 16 200(0) 0 16 0 0 0 V1
Signal file (record.dat):
- Format 16: 16-bit signed integers, little-endian
- Gain: 200 ADC units per mV (fixed)
- Baseline: 0
- Channels interleaved sample-by-sample
Annotation file(s) - Single file mode (.atr):
Binary WFDB annotation format containing: - Sample number (delta-encoded) - Annotation type code - Subtype (beat classification for R-peaks) - Channel number
Peak type to WFDB code mapping:
| Peak Type | WFDB Code | Description |
|---|---|---|
| R-peaks | N, V, A, L, R, etc. | Uses assigned beat type |
| P-peaks | p | P-wave peak |
| T-peaks | t | T-wave peak |
| U-waves | u | U-wave |
| Q-waves | ( | Waveform onset |
| S-waves | ) | Waveform end |
Annotation files - Separate files mode:
| File Extension | Contents |
|---|---|
record.atr | R-peaks with beat type annotations |
record.pwave | P-peaks |
record.qwave | Q-waves |
record.swave | S-waves |
record.twave | T-peaks |
record.uwave | U-waves |
✏️ 4. Peak Annotation
Basic Annotation Workflow
- Select a Channel from the dropdown
- Use the mouse scroll wheel or plot controls to zoom into a region of interest
- Select an annotation Mode:
- Navigate — Pan and zoom only
- Add R (Normal) — Click to add R-peaks (default type: Normal)
- Add P — Click to add P-peaks
- Add T — Click to add T-peaks
- Add U — Click to add U-waves
- Add Q — Click to add Q-waves
- Add S — Click to add S-waves
- Remove nearest — Click near a peak to delete it
- Click on the signal where you want to place peaks
💡 Zoom Persists
Your zoom state is preserved when switching channels or annotation modes. The plot remembers where you were working.
Snap to Peak
After placing peaks approximately, you can refine them automatically:
- Enter a window size (e.g., 4-10 samples) in the "Snap peaks" field
- Click "🎯 Adjust"
- All R, P, T, and U peaks will snap to the local maximum within the window
⚠️ Q and S Waves Are Not Snapped
Q and S waves are typically minima or inflection points, so they are excluded from the snap function. These must be placed precisely by hand.
Undo Annotations
↶ Remove Last
Click "↶ Remove Last" to undo the most recent peak annotation. This button removes peaks in the reverse order they were added, acting as a simple undo function.
Clear All Peaks
Click "Clear peaks" to remove all R, P, Q, S, T, and U annotations. A confirmation dialog will appear.
Invert ECG
Click "Invert ECG" to multiply all signal values by -1. Useful for signals with inverted polarity.
4.1 R-Peak Beat Classification
CardioAnnotate supports comprehensive beat-by-beat R-peak classification following WFDB annotation standards. Each R-peak can be classified into one of 19 different beat types for arrhythmia analysis.
How to Classify R-Peaks
Method 1: Quick Classify Buttons
After adding an R-peak, use the quick classify buttons for common types:
- N — Normal beat
- V — Premature ventricular contraction (PVC)
- A — Atrial premature contraction (APC)
- L — Left bundle branch block (LBBB)
- R — Right bundle branch block (RBBB)
Simply add an R-peak, then click the appropriate quick classify button.
Method 2: Full Classification Panel
Click "More..." to access the complete classification panel with all 19 beat types. The panel shows:
- Current beat type and sample position
- Full list of WFDB annotation codes
- Beat type descriptions
Method 3: Right-Click Classification
Right-click near any R-peak on the plot to open the classification panel for that specific peak. This is the fastest way to classify previously-added R-peaks.
Supported Beat Types
| Code | Name | Category | Description |
|---|---|---|---|
N | Normal | Normal | Normal beat (default) |
L | LBBB | Bundle Branch | Left bundle branch block beat |
R | RBBB | Bundle Branch | Right bundle branch block beat |
B | BBB | Bundle Branch | Bundle branch block (unspecified) |
A | APC | Supraventricular | Atrial premature contraction |
a | Aberr APC | Supraventricular | Aberrated atrial premature beat |
J | NPC | Supraventricular | Nodal (junctional) premature beat |
S | SVPB | Supraventricular | Supraventricular premature beat |
V | PVC | Ventricular | Premature ventricular contraction |
r | R-on-T | Ventricular | R-on-T premature ventricular contraction |
F | Fusion | Ventricular | Fusion of ventricular and normal beat |
E | V Escape | Ventricular | Ventricular escape beat |
e | A Escape | Escape | Atrial escape beat |
j | N Escape | Escape | Nodal (junctional) escape beat |
n | SV Escape | Escape | Supraventricular escape beat |
/ | Paced | Paced | Paced beat |
f | Paced Fus | Paced | Fusion of paced and normal beat |
Q | Unknown | Unknown | Unclassifiable beat |
? | Learn | Unknown | Beat not classified during learning |
Visual Feedback
R-peaks are color-coded by beat type on the plot:
- Red — Normal beats
- Amber — Bundle branch blocks
- Emerald — Supraventricular beats
- Dark Red — Ventricular beats
- Violet — Escape beats
- Indigo — Paced beats
- Gray — Unknown/unclassified
The R-peaks badge in the Stats panel shows a breakdown of counts by beat type (e.g., "N:245, V:12, A:3").
💡 Last R-Peak Info
After adding an R-peak, the interface displays "Last: R@[sample] [type]" to help you track your most recent annotation.
4.2 Keyboard Shortcuts
Speed up your workflow with keyboard shortcuts for R-peak classification:
| Key | Action |
|---|---|
| N | Classify last R-peak as Normal |
| V | Classify last R-peak as PVC (Ventricular) |
| A | Classify last R-peak as APC (Atrial) |
| L | Classify last R-peak as LBBB |
| Shift+R | Classify last R-peak as RBBB |
| J | Classify last R-peak as Nodal/Junctional |
| B | Classify last R-peak as Bundle Branch Block |
| F | Classify last R-peak as Fusion |
| E | Classify last R-peak as Ventricular Escape |
| Q | Classify last R-peak as Unknown |
| S | Classify last R-peak as Supraventricular |
💡 Efficient Workflow
The fastest workflow is:
- Set mode to "Add R (Normal)"
- Click on each R-peak
- Immediately press a keyboard shortcut to classify (if not normal)
- Continue to next peak
⚠️ Keyboard Shortcuts Only Work on Last R-Peak
Keyboard shortcuts classify the most recently added R-peak. To classify older R-peaks, use the right-click method or the "More..." panel.
4.3 Automatic Peak Detection
CardioAnnotate provides two automatic peak detection algorithms that can be used for any peak type:
Find Peaks (scipy.signal.find_peaks)
A general-purpose peak detection algorithm suitable for all peak types. Configurable parameters:
- Polarity: Detect maxima, minima, or absolute value maxima
- Min distance (ms): Minimum distance between consecutive peaks
- Threshold: Minimum height for peaks (in signal units)
- Prominence: Minimum vertical prominence required
- Prom window (ms): Window size for prominence calculation
Pan-Tompkins Algorithm
A classic QRS detection algorithm optimized for R-peak detection. Parameters:
- Band-pass (Hz): Frequency range for filtering (default 5–15 Hz)
- MWI (ms): Moving window integration width
- Min RR (ms): Minimum allowed RR interval
- Thresh k: Threshold factor (detection threshold = mean + k·std)
- Refine ± (ms): Window for refining detected peaks to local maximum
Detection Scope
- Full signal: Process the entire recording
- Visible segment: Process only the current zoom window
Options
- Replace existing (in scope): If checked, removes existing peaks of the target type before detection
💡 Save Settings per Peak Type
Click "💾 Save settings to this peak type" to remember your detection parameters for each peak type. The tool will remember different settings for R, P, T, etc.
💡 Detection Tips
- For R-peaks, Pan-Tompkins usually works best
- For P and T waves, use Find Peaks with appropriate polarity and distance settings
- For Q and S waves, use Find Peaks with "Minima" polarity
- Run detection on a visible segment first to tune parameters before processing full signal
💾 5. Export & Sharing
Export Annotations (JSON)
Click "Export annotations (JSON)" to save all peak annotations in a structured JSON format:
- Includes sampling frequency and channel names
- Preserves R-peak beat type classifications
- Organized by peak type and channel
- Can be reloaded later
Export Annotations (CSV)
Click "Export annotations (CSV)" for a tabular format with columns:
peak_type— R, P, Q, S, T, or Uchannel_index— Zero-indexed channel numberchannel_name— Channel labelsample_index— Peak location in samplestime_s— Peak time in secondsbeat_type— WFDB annotation code (for R-peaks only)
Export ECG Signal (CSV)
Click "Export ECG (CSV)" to save the current processed signal with columns:
fs— Sampling frequency (repeated in every row)time_s— Time in seconds- One column per channel with signal values
Export NPZ Bundle
Click "Export bundle (.npz)" to save a signal-and-annotation bundle containing:
- All current processed signal data
- Sampling frequency and channel metadata
- All peak annotations with beat types
This format is useful for exchanging processed signals and annotations in a compact Python-compatible archive. Use full session export when you need exact workspace restoration.
Export Full Session
Click the full session export control to save the complete analysis state in .json, .mat, or .npz format. A session contains:
- Original and current processed signals for every channel
- Sampling frequency, channel names, units, and record identifier
- All annotations, R-peak beat classifications, flagged RR intervals, and GP-corrected RR values
- Filtering, resampling, detector, SQI, RR quality-control, GP kernel, and GP hyperparameter settings
- The active channel and current zoom window used for visible-segment analysis
- A saved snapshot of SQI, HRV, ADF stationarity, and summary statistics
Import Full Session
Click the full session import control and select a .json, .mat, or .npz session file. The importer detects the format automatically, restores the workspace state, and recomputes the SQI, HRV, and stationarity panels so the displayed values reproduce the saved snapshot.
Full Session vs. Other Exports
Annotation JSON/CSV files store peak labels only. NPZ signal bundles store processed signals and annotations. Full sessions store the complete processing pipeline, interface state, correction state, visible segment, and computed analysis results.
Load Previous Annotations
You can reload previously exported annotations or import compatible WFDB annotation files:
- For JSON or CSV annotations, click "📂 Load JSON/CSV" and select the annotation file
- For WFDB annotations, use the "Import WFDB annotations" row, select the annotation file, then click "📂 Load .atr / wave"
- Annotations will be merged with current peaks (duplicates may occur)
- R-peak beat type information is preserved when loading JSON or
.atrannotation files
JSON imports accept the current annotations top-level key and the legacy peaks key. WFDB imports accept .atr, .qrs, .pwave, .twave, .uwave, .qwave, .swave, and recognized extension variants. The WFDB file picker accepts all extensions so that non-standard annotation files remain selectable.
Export to WFDB Format
Save the current signal and annotations as WFDB files with enhanced export options:
- Enter a record name (default is "export")
- Choose an annotation format:
- All peaks in one file (.atr) — Single annotation file containing all peak types with WFDB codes
- Separate files — Creates individual annotation files:
.atr— R-peaks with beat type annotations.pwave— P-peaks.twave— T-peaks.uwave— U-waves.qwave— Q-waves.swave— S-waves
- Click "Export WFDB (.hea/.dat + annotations)"
- Files will be downloaded:
.hea— Header with metadata.dat— Signal data (16-bit signed, gain 200)- Annotation file(s) based on your selection
WFDB Annotation Mapping
When using the "All peaks in one file" mode, peak types are mapped to WFDB annotation codes:
- R-peaks → Use their assigned beat type code (N, V, A, L, etc.)
- P-peaks →
p(P-wave peak) - T-peaks →
t(T-wave peak) - U-waves →
u(U-wave) - Q-waves →
((waveform onset) - S-waves →
)(waveform end)
💡 Choosing Between Export Modes
Single file mode is best for standard WFDB compatibility and when you need all annotations in one place. Separate files mode is useful when different peak types need independent processing or when working with tools that expect specific annotation file extensions.
💡 Export Summary
After exporting, a status message shows which files were created and the count of each peak type (e.g., "✓ Exported: record.hea, record.dat, record.atr (245 R, 38 P, 41 T)").
🔧 6. Signal Processing
Resampling
Change the sampling frequency of your signal:
- Enter the target sampling frequency in Hz
- Click "Resample"
- The signal will be resampled using
scipy.signal.resample_poly - If you have annotations, you'll be warned that they need to be reprojected
⚠️ Important: Reproject Annotations After Resampling
After resampling, your peak annotations will be at incorrect positions. Click "Reproject annotations to new fs" to automatically scale them to the new sampling rate.
Filtering
Apply Butterworth filters (order 4, zero-phase using filtfilt):
Low-Pass Filter
Remove high-frequency noise. Enter cutoff frequency in Hz (e.g., 40 Hz).
High-Pass Filter
Remove baseline wander and low-frequency drift. Enter cutoff frequency in Hz (e.g., 0.5 Hz).
Notch Filter
Remove powerline interference at 50 Hz or 60 Hz. Select from dropdown (Q=30).
Band-Pass Filter
Convenience shortcut that applies high-pass and low-pass in one step. Enter low and high cutoff frequencies.
💡 Typical ECG Filter Settings
Common band-pass: 0.5 Hz (high-pass) to 40 Hz (low-pass). This removes baseline drift while preserving ECG morphology.
Reset to Original
Click "↺ Reset all signals" to discard all signal processing (resampling, filtering, inversion) and return to the originally loaded signal.
⚠️ Filter Order Matters
If you plan to both resample and filter, resample first, then filter. Filters are designed for specific sampling rates.
📊 7. Statistics & Analysis
7.1 Basic ECG Metrics
The Statistics panel automatically computes common ECG metrics based on your peak annotations:
Available Metrics
- Duration: Total signal length in seconds
- Avg RR (ms): Average R-R interval duration (time between consecutive R-peaks)
- HR (bpm): Heart rate calculated from average RR interval (60000 / RR_ms)
- Avg PR (ms): Average P-R interval (from P-peak to next R-peak)
- Avg QT (ms): Average Q-T interval (from Q-wave to T-peak, or R-peak to T-peak if no Q annotations)
- Avg QRS (ms): Average QRS duration (from Q-wave to S-wave if both are annotated, otherwise estimated from R-peak to S-wave)
💡 Statistical Note
All interval statistics require at least 2 peaks of the relevant types. Metrics will show "—" if insufficient data is available.
Badge Counters
The colored badges show total peak counts across all channels:
- Signal: Current processing state (original, resampled, filtered, modified)
- R-peaks: Total R-peaks with breakdown by beat type (e.g., "N:245, V:12, A:3")
- P-peaks: Total P-peaks
- Q-waves: Total Q-waves
- S-waves: Total S-waves
- T-peaks: Total T-peaks
- U-waves: Total U-waves
R-Peak Breakdown
The R-peaks badge includes a detailed breakdown showing the count of each beat type, making it easy to see the distribution of normal beats versus arrhythmias at a glance.
7.2 Signal Quality Indices (SQI)
Signal quality metrics help assess the reliability of the ECG signal for analysis. These are computed on the currently visible window of the active channel.
| Metric | Formula | Description | Interpretation |
|---|---|---|---|
| pSQI | Power(5–15 Hz) / Power(5–40 Hz) | QRS-band relative power | Higher values (0.3–0.7) indicate stronger QRS complexes relative to other content |
| kSQI | Pearson kurtosis | Signal "peakiness" | Gaussian ≈ 3; higher values indicate prominent QRS spikes; low values suggest noise |
| basSQI | 1 − Power(0–1 Hz) / Power(0–40 Hz) | Baseline wander index | Higher is better (close to 1); low values indicate significant baseline drift |
| lineSQI | Power(mains ± width) / Power(0–Nyquist) | Mains interference ratio | Lower is better; high values indicate powerline contamination at 50/60 Hz |
💡 Mains Frequency Settings
Adjust the Mains dropdown (50 Hz for Europe/Asia or 60 Hz for Americas) and ± bandwidth (0.5 to 2 Hz) to match your region's power grid for accurate lineSQI calculation.
7.3 Heart Rate Variability (HRV)
HRV metrics are computed from R-peaks within the current zoom window. The segment range is displayed (e.g., "Segment: 0.0–10.5s").
Time-Domain Metrics
| Metric | Description | Clinical Significance |
|---|---|---|
| Mean NN / RR (ms) | Average of all NN (RR) intervals | Inversely related to heart rate |
| SDNN (ms) | Standard deviation of NN intervals | Overall HRV; reflects all cyclic components |
| RMSSD (ms) | Root mean square of successive differences | Short-term variability; parasympathetic activity |
| NN50 | Count of successive NN intervals differing by >50 ms | Parasympathetic indicator |
| pNN50 (%) | NN50 as percentage of total NN intervals | Normalized parasympathetic indicator |
Frequency-Domain Metrics
Computed using Welch's method (Hann window, 50% overlap):
| Metric | Frequency Band | Physiological Interpretation |
|---|---|---|
| ULF power (ms²) | 0–0.003 Hz | Ultra low frequency; circadian rhythms, thermoregulation |
| VLF power (ms²) | 0.003–0.04 Hz | Very low frequency; thermoregulation, hormonal fluctuations |
| LF power (ms²) | 0.04–0.15 Hz | Low frequency; mix of sympathetic and parasympathetic |
| HF power (ms²) | 0.15–0.4 Hz | High frequency; parasympathetic (vagal) activity, respiratory sinus arrhythmia |
| LF/HF ratio | — | Sympathovagal balance (interpretation debated) |
Nonlinear Metrics
Derived from the Poincaré plot of successive NN intervals (RRn versus RRn+1) and from the sample entropy of the NN series. A Poincaré plot can be opened in a separate window using the Poincaré plot button; the SD1/SD2 dispersion ellipse, centered at the mean NN, is overlaid on the scatter.
| Metric | Description | Interpretation |
|---|---|---|
| Poincaré SD1 (ms) | Dispersion perpendicular to the line of identity (SD1 = SDSD/√2, where SDSD is the standard deviation of successive NN differences) | Short-term variability; closely tracks RMSSD and reflects parasympathetic (vagal) activity |
| Poincaré SD2 (ms) | Dispersion along the line of identity (SD2 = √(2·SDNN² − ½·SDSD²)) | Long-term variability; reflects overall HRV and satisfies SD1² + SD2² = 2·SDNN² |
| SD2/SD1 | Ratio of long-term to short-term variability | Balance between long- and short-term variability; higher values indicate greater long-term dispersion relative to beat-to-beat variability |
| SampEn | Sample entropy of the NN series (embedding dimension m = 2, tolerance r = 0.2 × SDNN) | Regularity and complexity of the RR series; lower values indicate more regular, predictable rhythms, higher values indicate more complex or irregular dynamics |
Stationarity (ADF Test)
The Augmented Dickey-Fuller test assesses whether the RR interval series is stationary:
- Stationary (p < 0.05) — RR intervals show stable statistical properties; HRV metrics are reliable
- Non-stationary — RR intervals show trends or drifts; interpret HRV metrics with caution
⚠️ HRV Requirements
Time-domain HRV requires ≥2 R-peaks (to compute at least one RR interval). Frequency-domain HRV requires ≥4 RR intervals for reliable spectral estimation.
Nonlinear metrics require ≥3 NN intervals for the Poincaré descriptors; SampEn requires at least m+2 intervals and is reported as "—" when no matching templates exist at the chosen tolerance, so it is most reliable on longer segments with many intervals.
7.4 RR Error Detection & GP Correction
CardioAnnotate includes advanced RR interval quality control using Gaussian Process (GP) regression for detecting and correcting outliers. This is essential for reliable HRV analysis.
Detection Modes
| Mode | Description | Parameters |
|---|---|---|
| Absolute bounds | Flag RR intervals outside physiological limits | Abs min RR: Minimum (default 250 ms) Abs max RR: Maximum (default 2000 ms) |
| Rate-of-change bounds | Flag intervals with excessive beat-to-beat variation | Max rel change α: Maximum relative change (default 0.20 = 20%) |
| Both | Combine absolute and rate-of-change criteria | Both sets of parameters apply |
Compare to last accepted
When enabled (default), rate-of-change detection compares each interval to the last accepted value rather than the immediate predecessor. This prevents cascading rejections from a single outlier.
Gaussian Process Kernels
Choose a GP kernel for RR interval interpolation:
- Rational Quadratic (RQ) — Default; flexible, handles multiple length scales. Best general-purpose choice.
- Matérn 3/2 — Once-differentiable, allows rougher variations
- Matérn 5/2 — Twice-differentiable, smoother than 3/2
- Periodic — For regular heartbeat patterns with periodic structure
- Brownian — For trending RR series (non-stationary)
Default GP Hyperparameters
Kernel defaults use optimized mean values from the Harmonizing Heartbeats RR-correction model. Each kernel applies its variance term, σ², as a scale factor.
| Kernel | Default Hyperparameters |
|---|---|
| Rational Quadratic (RQ) | ℓ=0.475, α=0.412, σ²=3.06×10⁻⁴ |
| Matérn 3/2 | ℓ=2.65, σ²=54.93 |
| Matérn 5/2 | ℓ=0.241, σ²=29.40 |
| Periodic | period=0.784, ℓ=0.985, σ²=2.90 |
| Brownian | σ²=0.0010 |
GP Hyperparameters Panel
Click "GP hyperparameters ▾" below the GP controls to open the collapsible hyperparameter panel. The visible sliders change with the selected kernel, each slider shows its current value, and GP correction uses the current slider values each time it runs.
Workflow
- Configure detection parameters (min/max RR, max change α)
- Select a GP kernel and adjust the GP hyperparameters sliders if needed
- Click "🧪 Detect RR outliers" to flag implausible intervals
- Review the flagged count in the display
- Click "✨ GP-correct flagged RR" to estimate corrected values using GP regression
- Optionally click "🗑 Remove flagged RR" to delete the R-peaks associated with flagged intervals
⚠️ GP Correction Requirements
GP correction requires at least 3 accepted (non-flagged) RR intervals to train the model, and at least 1 flagged interval to correct. If correction fails, try adjusting thresholds or using a different kernel.
💡 When to Use GP Correction
Run RR outlier detection and GP correction before computing HRV metrics. This removes or corrects ectopic beats, missed beats, and artifacts that would otherwise distort HRV estimates.
🌙 8. Dark Mode
CardioAnnotate supports both light and dark themes for comfortable viewing in any environment.
Toggle Theme
Click the "🌙 Dark" / "☀️ Light" button in the header to switch between themes. Your preference is saved in the browser's local storage and will be remembered for future sessions.
💡 Reduced Eye Strain
Dark mode is recommended for extended annotation sessions, especially in low-light environments. The plot colors, UI elements, and contrast levels are optimized for visibility in both themes.
💡 9. Tips & Best Practices
1. Work at the Right Zoom Level
Zoom in to 2-3 heartbeats for optimal annotation precision. You can see the overall morphology while placing peaks accurately.
2. Annotate R-peaks First, Then Classify
The most efficient workflow is to add all R-peaks first with the default "Normal" type, then use keyboard shortcuts or quick classify buttons to mark arrhythmias as you go. For complex cases, right-click on R-peaks to access the full classification panel.
3. Use Snap-to-Peak for Refinement
Place approximate peaks quickly, then use the snap function with a small window (3-5 samples) to refine them automatically. This works best for R, P, T, and U peaks.
4. Save Your Work Frequently
Export annotations to JSON or a signal bundle to preserve peak labels. Use full session export when you need to preserve the complete workspace, including the original and processed signals, settings, corrections, selected window, and computed results. Since everything runs in-browser, refreshing the page will lose all unsaved work.
5. Process Then Annotate
Apply necessary filtering and resampling before annotating. It's easier to see features in clean, properly-sampled signals.
6. Multi-Lead Consistency
For multi-lead ECGs, annotate the clearest lead first (often MLII), then use those annotations as a temporal reference for other leads.
7. Q and S Wave Annotation
Q and S waves require precise manual placement since they're often minima or inflection points. They are automatically excluded from the "snap to peak" function. For accurate QRS measurements, annotate both Q and S waves.
8. Use "Remove Last" for Quick Corrections
If you make a mistake while annotating, use the "↶ Remove Last" button to undo the most recent annotation. This is faster than switching to "Remove nearest" mode.
9. Choose the Right WFDB Export Mode
For general-purpose exports and maximum compatibility, use the single file mode. Use separate files mode when you need to process different peak types independently or when working with specialized WFDB tools.
10. Use GP Correction Before HRV Analysis
Before computing HRV metrics, run RR outlier detection and GP correction to remove or correct ectopic beats and artifacts. This ensures more reliable HRV estimates.
11. Check Signal Quality First
Review the SQI metrics before detailed annotation. Poor signal quality (low pSQI, high lineSQI) may require filtering or indicate a segment unsuitable for analysis.
12. Use Full Sessions for Reproducibility
When sharing an analysis with collaborators or preparing work for review, export a full session rather than only annotations. The session records the visible segment, detector parameters, GP correction state, and computed SQI, HRV, and ADF results needed to reproduce the workspace.
🔧 10. Troubleshooting
Common Issues
Issue: "Initializing..." never completes
Solution: Check your internet connection. Pyodide and WFDB need to download (~20MB) on first load. Try refreshing the page or clearing your browser cache.
Issue: "Failed to load" WFDB files
Solution: Ensure you have both .hea and .dat (or .mat) files with the same basename. Check that the files aren't corrupted. Try the files with standard WFDB tools first.
Issue: Annotations disappear after resampling
Solution: This is expected behavior. Use the "Reproject annotations to new fs" button to scale peak positions to the new sampling rate.
Issue: CSV import fails
Solution: Verify your CSV has:
- A header row with
fsorsampling_ratecolumn - Channel columns with recognizable names
- Numeric values in all data rows
- No missing values
Issue: Filter causes error
Solution: Make sure cutoff frequencies are below the Nyquist frequency (sampling_rate / 2). Very low or very high cutoffs can cause numerical issues.
Issue: Peaks not visible on plot
Solution: Zoom out to see the full signal range. Annotations are only visible when the corresponding samples are in the current view window.
Issue: Beat classification not working
Solution: Beat classification only works for R-peaks. Make sure you're in "Add R (Normal)" mode or have recently added an R-peak. Keyboard shortcuts only classify the most recently added R-peak.
Issue: WFDB annotation file is not visible in the file picker
Solution: The WFDB annotation importer accepts all file extensions. Choose the file directly, or switch the operating system file picker to show all files, then select the desired .atr, .qrs, .pwave, .twave, .uwave, .qwave, or .swave annotation file.
Issue: WFDB export creates unexpected files
Solution: Check your selected export mode (single file vs. separate files). The "single file" mode creates one .atr file, while "separate files" mode creates multiple annotation files (.atr, .pwave, .twave, etc.) depending on which peak types you've annotated.
Issue: GP correction fails or returns poor results
Solution: GP correction requires at least 3 accepted (non-flagged) RR intervals. Try adjusting the detection thresholds to flag fewer intervals, or try a different kernel (RQ is usually most robust).
Issue: HRV metrics show "—"
Solution: HRV requires R-peaks in the visible segment. Time-domain metrics need ≥2 R-peaks; frequency-domain metrics need ≥4 RR intervals. Zoom to include more heartbeats.
Issue: NPZ import fails
Solution: Ensure the NPZ file was exported from CardioAnnotate and contains both "signals" and "meta" arrays. Files from other sources may not be compatible.
Issue: Full session import does not restore the expected workspace
Solution: Confirm that the file is a CardioAnnotate full session rather than an annotation-only JSON/CSV file or a signal bundle. Full sessions include signals, annotations, corrections, settings, view state, and results. If the visible segment differs after import, check that the saved session contained the expected zoom window and active channel.
Performance Tips
- Large Files: For signals >1 million samples, operations may take several seconds. Be patient and watch the console log for progress.
- Browser Choice: Chrome/Edge typically perform best. Firefox and Safari also work but may be slower for very large signals.
- Memory: Very large multi-channel recordings may exceed browser memory limits (typically ~2GB). Consider downsampling or processing channels separately.
📝 11. Citation & Credits
If you use CardioAnnotate in your research or clinical work, please cite:
CardioAnnotate: A Toolkit for ECG Peak Labeling and Processing
A browser-based ECG annotation toolkit with integrated signal processing, R-peak beat classification, HRV analysis, and WFDB format support.
Harmonizing heartbeats: RR-interval correction with Gaussian Process for reliable HRV
Mojtahed, Hamed, et al. (2026). Biomedical Signal Processing and Control, 117, 109513. Elsevier.
If you use the RR error detection and Gaussian Process correction features, please also cite this work.
Technical Details
- Implementation: JavaScript + Pyodide (Python 3.11 in WebAssembly)
- Dependencies: WFDB-Python, NumPy, SciPy, Plotly.js
- Signal Processing: Butterworth filters (order 4), zero-phase filtering (filtfilt), polyphase resampling
- Visualization: Plotly.js for interactive plotting with color-coded beat types
- Privacy: 100% client-side processing, no server uploads
- Beat Classification: 19 WFDB annotation codes following PhysioNet standards
- HRV Analysis: Time-domain (SDNN, RMSSD, pNN50), frequency-domain (ULF, VLF, LF, HF) with Welch's PSD, and nonlinear (Poincaré SD1/SD2/SD2:SD1, sample entropy) metrics
- RR Correction: Gaussian Process regression with multiple kernel options (RQ, Matérn, Periodic, Brownian) and adjustable optimized hyperparameters
- Session Management: Full-state export and import in JSON, MAT, and NPZ containers for reproducible analysis restoration
Acknowledgments
This toolkit builds upon the excellent work of:
- WFDB Software Package - Physiological signal processing standards and annotation codes
- PhysioNet - ECG databases and research resources
- Pyodide Project - Python in the browser
- SciPy - Scientific computing in Python