Python package for reading, writing, and processing physiologic signals and annotations in WFDB format.
Tools for plotting physiological signals and annotations with customizable styling, multiple display options, and publication-quality output. This module provides matplotlib-based visualization specifically designed for biomedical signal data and WFDB records.
High-level functions for plotting WFDB records and annotations with automatic formatting and styling.
def plot_wfdb(record: Record = None, annotation: Annotation = None,
plot_sym: bool = False, time_units: str = 'seconds',
title: str = None, sig_style: List[str] = [''], ann_style: List[str] = ['r*'],
ecg_grids: List[int] = [], figsize: Tuple[float, float] = None,
return_fig: bool = False, sharex: str = "auto") -> Union[None, matplotlib.figure.Figure]:
"""
Plot WFDB record and annotation objects with automatic formatting.
Parameters:
- record: Record, WFDB record object to plot
- annotation: Annotation, annotation object to overlay
- plot_sym: bool, whether to plot annotation symbols (default: False)
- time_units: str, x-axis units ('samples', 'seconds', 'minutes', 'hours')
- title: str, plot title
- sig_style: list of str, signal line styles for each channel
- ann_style: list of str, annotation marker styles
- ecg_grids: list of int, channels to apply ECG grid styling
- figsize: tuple, figure size (width, height) in inches
- return_fig: bool, return figure object
- sharex: str, x-axis sharing mode ("auto", "all", "none")
Returns:
None or Figure object based on return_fig parameter
"""
def plot_items(signal: Union[np.ndarray, List[np.ndarray]] = None,
ann_samp: List[Union[int, np.ndarray]] = None,
ann_sym: List[List[str]] = None, fs: float = None,
time_units: str = "samples", sig_name: List[str] = None,
sig_units: List[str] = None, xlabel: str = None, ylabel: str = None,
title: str = None, sig_style: List[str] = [""],
ann_style: List[str] = ["r*"], ecg_grids: List[int] = [],
figsize: Tuple[float, float] = None, sharex: bool = False,
sharey: bool = False, return_fig: bool = False,
return_fig_axes: bool = False, sampling_freq: float = None,
ann_freq: List[float] = None) -> Union[None, matplotlib.figure.Figure, Tuple[matplotlib.figure.Figure, List[matplotlib.axes.Axes]]]:
"""
Plot signals and annotations with detailed customization options.
Parameters:
- signal: ndarray or list of ndarray, signals to plot
- ann_samp: list of array-like, annotation sample locations for each subplot
- ann_sym: list of list of str, annotation symbols for each subplot
- fs: float, sampling frequency for time conversion
- time_units: str, x-axis time units ('samples', 'seconds', 'minutes', 'hours')
- sig_name: list of str, signal names for subplot titles
- sig_units: list of str, signal units for y-axis labels
- xlabel: str, x-axis label (overrides time_units)
- ylabel: str, y-axis label for single subplot
- title: str, main plot title
- sig_style: list of str, line styles for each signal
- ann_style: list of str, marker styles for annotations
- ecg_grids: list of int, subplot indices for ECG grid styling
- figsize: tuple, figure size (width, height) in inches
- sharex: bool, share x-axis across subplots
- sharey: bool, share y-axis across subplots
- return_fig: bool, return figure object
- return_fig_axes: bool, return both figure and axes objects
- sampling_freq: float, deprecated, use fs parameter
- ann_freq: list of float, sampling frequencies for annotations
Returns:
None, Figure object, or (Figure, Axes) tuple based on return parameters
"""
def plot_all_records(directory: str = "") -> None:
"""
Plot all WFDB records in a directory.
Parameters:
- directory: str, directory path containing WFDB records (default: current)
"""import wfdb
import matplotlib.pyplot as plt
# Read record and annotations
record = wfdb.rdrecord('100', pn_dir='mitdb')
annotation = wfdb.rdann('100', 'atr', pn_dir='mitdb')
# Simple plot with default settings
wfdb.plot.plot_wfdb(record=record, annotation=annotation)
plt.show()
# Plot with time units in seconds
wfdb.plot.plot_wfdb(record=record, annotation=annotation,
time_units='seconds', title='MIT-BIH Record 100')
plt.show()
# Plot subset of data (first 10 seconds)
record_subset = wfdb.rdrecord('100', pn_dir='mitdb',
sampfrom=0, sampto=3600) # 10 sec at 360 Hz
wfdb.plot.plot_wfdb(record=record_subset,
time_units='seconds', title='First 10 seconds')
plt.show()import wfdb
import matplotlib.pyplot as plt
import numpy as np
# Read multi-channel record
record = wfdb.rdrecord('100', pn_dir='mitdb')
annotation = wfdb.rdann('100', 'atr', pn_dir='mitdb')
# Custom styling
fig, axes = wfdb.plot.plot_wfdb(
record=record,
annotation=annotation,
time_units='minutes',
title='ECG with Custom Styling',
sig_style='b-', # Blue solid line
ann_style=['ro', 'g^'], # Red circles, green triangles
ecg_grids=[0, 1], # Apply ECG grid to both channels
figsize=(12, 8),
return_fig_axes=True
)
# Further customize the plot
for ax in axes:
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 5) # First 5 minutes
plt.tight_layout()
plt.show()import wfdb
import numpy as np
import matplotlib.pyplot as plt
# Read signals only
signals, fields = wfdb.rdsamp('100', pn_dir='mitdb', channels=[0, 1])
fs = fields['fs']
# Create sample annotations (e.g., detected peaks)
qrs_inds = wfdb.processing.xqrs_detect(signals[:, 0], fs=fs)
# Plot using plot_items for more control
wfdb.plot.plot_items(
signal=signals.T, # Transpose for subplot per signal
ann_samp=[qrs_inds, []], # Annotations only on first channel
ann_sym=[['R'] * len(qrs_inds), []], # Symbol for each annotation
fs=fs,
time_units='seconds',
sig_name=['MLI', 'MLII'],
sig_units=['mV', 'mV'],
title='ECG Signals with QRS Detection',
sig_style=['b-', 'r-'],
ann_style=['ko'],
figsize=(15, 6)
)
plt.show()import wfdb
import matplotlib.pyplot as plt
# Plot multiple records for comparison
records = ['100', '101', '102']
fig, axes = plt.subplots(len(records), 1, figsize=(12, 10), sharex=True)
for i, record_name in enumerate(records):
record = wfdb.rdrecord(record_name, pn_dir='mitdb',
sampfrom=0, sampto=1800) # 5 seconds
# Plot each on separate subplot
plt.sca(axes[i])
wfdb.plot.plot_wfdb(record=record, time_units='seconds',
title=f'Record {record_name}')
plt.tight_layout()
plt.show()import wfdb
import matplotlib.pyplot as plt
# Read ECG record
record = wfdb.rdrecord('100', pn_dir='mitdb', channels=[0])
# Plot with ECG-specific grid styling
wfdb.plot.plot_wfdb(
record=record,
time_units='seconds',
title='ECG with Standard Grid',
ecg_grids=[0], # Apply ECG grid to channel 0
figsize=(15, 6)
)
plt.show()
# Alternative: manual ECG grid setup
fig, ax = plt.subplots(figsize=(15, 6))
time_vec = np.arange(record.sig_len) / record.fs
ax.plot(time_vec, record.p_signal[:, 0], 'b-', linewidth=0.8)
# Standard ECG grid (0.04s major, 0.008s minor)
ax.grid(True, which='major', alpha=0.5, linestyle='-', linewidth=0.8)
ax.grid(True, which='minor', alpha=0.3, linestyle='-', linewidth=0.4)
ax.set_xlabel('Time (seconds)')
ax.set_ylabel('Amplitude (mV)')
ax.set_title('ECG with Manual Grid')
plt.show()import wfdb
import matplotlib.pyplot as plt
# Read long record (will be automatically chunked)
record = wfdb.rdrecord('100', pn_dir='mitdb') # Full 30-minute record
# Automatic chunking for large datasets
wfdb.plot.plot_wfdb(
record=record,
time_units='minutes',
title='Full 30-minute ECG Record',
auto_chunk=True,
chunk_size=50000, # Adjust chunk size as needed
figsize=(20, 8)
)
plt.show()
# Plot specific time windows
for start_min in range(0, 30, 5): # 5-minute windows
start_samp = int(start_min * 60 * record.fs)
end_samp = int((start_min + 5) * 60 * record.fs)
record_chunk = wfdb.rdrecord('100', pn_dir='mitdb',
sampfrom=start_samp, sampto=end_samp)
wfdb.plot.plot_wfdb(record=record_chunk, time_units='seconds',
title=f'Minutes {start_min}-{start_min+5}')
plt.show()import wfdb
import matplotlib.pyplot as plt
# Read record with multiple annotation types
record = wfdb.rdrecord('100', pn_dir='mitdb', channels=[0])
annotations = wfdb.rdann('100', 'atr', pn_dir='mitdb')
# Plot with symbol display
wfdb.plot.plot_wfdb(
record=record,
annotation=annotations,
plot_sym=True, # Show annotation symbols
time_units='seconds',
ann_style=['r*'], # Red asterisks
title='ECG with Beat Annotations'
)
plt.show()
# Custom annotation display
normal_beats = annotations.sample[annotations.symbol == 'N']
pvc_beats = annotations.sample[annotations.symbol == 'V']
wfdb.plot.plot_items(
signal=[record.p_signal[:, 0]],
ann_samp=[normal_beats, pvc_beats],
ann_sym=[['N'] * len(normal_beats), ['V'] * len(pvc_beats)],
fs=record.fs,
time_units='seconds',
sig_name=['ECG'],
ann_style=['go', 'r^'], # Green circles for N, red triangles for V
title='Normal vs PVC Beats',
figsize=(15, 6)
)
plt.show()import wfdb
import matplotlib.pyplot as plt
# Create plot
record = wfdb.rdrecord('100', pn_dir='mitdb')
fig = wfdb.plot.plot_wfdb(record=record, time_units='seconds',
title='ECG Record 100', return_fig=True)
# Save in different formats
fig.savefig('ecg_record_100.png', dpi=300, bbox_inches='tight')
fig.savefig('ecg_record_100.pdf', bbox_inches='tight')
fig.savefig('ecg_record_100.svg', bbox_inches='tight')
plt.close(fig) # Clean up memoryInstall with Tessl CLI
npx tessl i tessl/pypi-wfdb