Cardiology ECG strip
MIT-BIH record 100 via PhysioNet LightWAVE API
Example from the compendium of canonical charts
Python Code
"""Cardiology ECG strip — MIT-BIH record 100 via PhysioNet LightWAVE API."""
import numpy as np
import plotly.graph_objects as go
LIGHTWAVE_URL = (
"https://physionet.org/lightwave/server"
"?action=fetch&db=mitdb/1.0.0&record=100&signal=0&t0=s0&tf=s360"
)
FS = 360 # sampling rate Hz
MV_OFFSET = 1024 # zero baseline
MV_SCALE = 200 # ADU per mV
DURATION = 2.5 # seconds to display
MAJOR_T = 0.20 # 0.2 s major grid
MINOR_T = 0.04 # 0.04 s minor grid
MAJOR_MV = 0.5 # 0.5 mV major grid
MINOR_MV = 0.1 # 0.1 mV minor grid
def ecg_colors():
return dict(
bg="white",
major="#ffc8c8",
minor="#ffe0e0",
signal="#1a1a1a",
)
def synthetic_ecg(n_samples: int, fs: int = 360) -> np.ndarray:
"""Generate a realistic synthetic Lead-II ECG signal."""
t = np.arange(n_samples) / fs
hr_bpm = 72
rr = 60 / hr_bpm # R-R interval in seconds
signal = np.zeros(n_samples)
def gaussian(t_arr, center, sigma, amp):
return amp * np.exp(-((t_arr - center) ** 2) / (2 * sigma ** 2))
# Place beats
beat_times = np.arange(0.2, t[-1], rr)
for bt in beat_times:
# P wave: 80ms wide, 0.15 mV
signal += gaussian(t, bt - 0.16, 0.025, 0.15)
# Q wave: small negative
signal += gaussian(t, bt - 0.02, 0.008, -0.15)
# R wave: tall, narrow
signal += gaussian(t, bt, 0.009, 1.2)
# S wave: small negative
signal += gaussian(t, bt + 0.025, 0.010, -0.25)
# ST segment + T wave: 120ms wide, 0.2 mV
signal += gaussian(t, bt + 0.20, 0.045, 0.25)
# Add baseline wander and noise
rng = np.random.default_rng(40)
signal += 0.04 * np.sin(2 * np.pi * 0.3 * t) # respiratory sway
signal += rng.normal(0, 0.015, n_samples) # measurement noise
return signal
def generate():
print("fetching MIT-BIH record 100 via PhysioNet LightWAVE …")
mV = None
try:
data = fetch_json(LIGHTWAVE_URL)
# LightWAVE returns {"fetch": {"signal": [{"samp": [...], ...}]}}
sig_list = data.get("fetch", data).get("signal", data.get("samples", []))
sig0 = sig_list[0]
raw_delta = sig0.get("samp", sig0.get("data", []))
raw = np.cumsum(raw_delta)
mV = (raw - MV_OFFSET) / MV_SCALE
t = np.arange(len(mV)) / FS
print(f" fetched {len(mV)} samples at {FS} Hz")
except Exception as e:
print(f" fetch failed ({e}), using synthetic ECG")
n_show = int(DURATION * FS)
if mV is not None:
t_show = t[:n_show]
mv_show = mV[:n_show]
else:
mv_show = synthetic_ecg(n_show, FS)
t_show = np.arange(n_show) / FS
c = ecg_colors()
fig = go.Figure()
# ECG grid shapes
shapes = []
t_max = DURATION
mv_min = float(mv_show.min()) - 0.2
mv_max = float(mv_show.max()) + 0.2
# Minor grid lines (time)
t_minor = np.arange(0, t_max + MINOR_T, MINOR_T)
for tx in t_minor:
shapes.append(dict(
type="line", x0=tx, x1=tx, y0=mv_min, y1=mv_max,
line=dict(color=c["minor"], width=0.5), layer="below",
))
# Major grid lines (time)
t_major = np.arange(0, t_max + MAJOR_T, MAJOR_T)
for tx in t_major:
shapes.append(dict(
type="line", x0=tx, x1=tx, y0=mv_min, y1=mv_max,
line=dict(color=c["major"], width=1.0), layer="below",
))
# Minor grid lines (mV)
mv_ticks_minor = np.arange(np.floor(mv_min / MINOR_MV) * MINOR_MV,
np.ceil(mv_max / MINOR_MV) * MINOR_MV + MINOR_MV,
MINOR_MV)
for my in mv_ticks_minor:
shapes.append(dict(
type="line", x0=0, x1=t_max, y0=my, y1=my,
line=dict(color=c["minor"], width=0.5), layer="below",
))
# Major grid lines (mV)
mv_ticks_major = np.arange(np.floor(mv_min / MAJOR_MV) * MAJOR_MV,
np.ceil(mv_max / MAJOR_MV) * MAJOR_MV + MAJOR_MV,
MAJOR_MV)
for my in mv_ticks_major:
shapes.append(dict(
type="line", x0=0, x1=t_max, y0=my, y1=my,
line=dict(color=c["major"], width=1.0), layer="below",
))
fig.update_layout(shapes=shapes)
# ECG signal trace
fig.add_trace(go.Scatter(
x=t_show,
y=mv_show,
mode="lines",
name="Lead II",
line=dict(color=c["signal"], width=1.5),
hovertemplate="t=%{x:.3f}s, %{y:.3f} mV<extra></extra>",
))
fig.update_layout(
paper_bgcolor=c["bg"],
plot_bgcolor=c["bg"],
xaxis=dict(
title="Time (s)",
range=[0, DURATION],
dtick=MAJOR_T,
tickformat=".1f",
zeroline=False,
showgrid=False,
),
yaxis=dict(
title="Amplitude (mV)",
zeroline=True,
zerolinecolor=c["major"],
showgrid=False,
),
legend=dict(orientation="h", y=1.08),
margin=dict(t=50, b=50, l=60, r=40),
height=400,
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly