10 common heart disease and ecg change
10 common heart diseases ECG changes summary 2024
| Stage | Change | Timing |
|---|---|---|
| Hyperacute | Peaked (hyperacute) T waves | Minutes |
| Acute | ST-segment elevation in leads over affected territory | Hours |
| Evolving | T-wave inversion, pathological Q waves (>40 ms, >25% R height) | Hours-days |
| Old | Persistent Q waves, T-wave normalization | Weeks-permanent |

| Stage | Finding |
|---|---|
| Stage 1 (days 1-2) | Diffuse ST elevation (saddle-shaped/concave up) in most leads except aVR and V1; PR depression in limb leads and V4-V6; PR elevation in aVR |
| Stage 2 (days/week) | ST normalization, PR depression persists |
| Stage 3 | T-wave inversion (diffuse) |
| Stage 4 | ECG normalizes |
| Condition | Key ECG Feature |
|---|---|
| STEMI | ST elevation (territory-specific) → Q waves |
| NSTEMI/Unstable Angina | ST depression, T-wave inversion, no Q waves |
| Atrial Fibrillation | Absent P waves, irregularly irregular rhythm |
| Atrial Flutter | Sawtooth F-waves ~300/min, 2:1 AV block |
| Acute Pericarditis | Diffuse ST elevation (concave), PR depression |
| Hypertensive Heart Disease/Heart Failure | LVH voltage + lateral strain pattern |
| Aortic Stenosis | LVH + strain, LBBB, left axis deviation |
| Hypertrophic Cardiomyopathy | LVH + deep septal Q waves in inferior/lateral leads |
| Pulmonary Embolism | S1Q3T3, sinus tachycardia, new RBBB, RV strain |
| WPW Syndrome | Short PR + delta wave + wide QRS |
Create a reference PDF with ECG waveforms and patterns for each condition
mkdir -p /home/daytona/workspace/ecg-reference && ls /home/daytona/workspace/ecg-reference/
~/ecg-reference/generate_ecg_pdf.py
#!/usr/bin/env python3
"""
ECG Reference PDF Generator
Produces realistic ECG waveform illustrations for 10 common heart diseases
using matplotlib + reportlab.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.gridspec import GridSpec
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Image,
Table, TableStyle, HRFlowable, PageBreak,
KeepTogether)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.platypus.flowables import Flowable
import io
import os
OUT_DIR = "/home/daytona/workspace/ecg-reference"
os.makedirs(OUT_DIR, exist_ok=True)
# ─── ECG WAVEFORM SYNTHESIS ────────────────────────────────────────────────────
def baseline(n, noise=0.015):
"""Flat baseline with tiny noise."""
return np.random.normal(0, noise, n)
def gaussian(x, mu, sigma, amp):
return amp * np.exp(-((x - mu) ** 2) / (2 * sigma ** 2))
def make_pqrst(t_start, hr_bpm=75, p_amp=0.15, pr_interval=0.16,
qrs_width=0.08, r_amp=1.0, q_amp=-0.15, s_amp=-0.25,
st_elevation=0.0, st_depression=0.0, t_amp=0.3,
t_invert=False, delta_wave=False, q_wide=False,
p_absent=False, p_notch=False):
"""Return (time_array, voltage_array) for one PQRST complex."""
rr = 60.0 / hr_bpm
t = np.linspace(t_start, t_start + rr, 500)
v = np.zeros(len(t))
offset = t_start
if not p_absent:
p_center = offset + 0.08
if p_notch:
v += gaussian(t, p_center, 0.025, p_amp * 0.6)
v += gaussian(t, p_center + 0.03, 0.025, p_amp * 0.55)
else:
v += gaussian(t, p_center, 0.03, p_amp)
# PR segment
q_center = offset + pr_interval + 0.04
r_center = q_center + (qrs_width * 0.35)
s_center = r_center + (qrs_width * 0.35)
# Delta wave (WPW) - slow initial upstroke
if delta_wave:
delta_start = q_center - 0.04
for i, ti in enumerate(t):
if delta_start <= ti <= r_center:
v[i] += r_amp * 0.3 * (ti - delta_start) / (r_center - delta_start)
# Q wave
q_sigma = 0.025 if not q_wide else 0.04
v += gaussian(t, q_center, q_sigma, q_amp)
# R wave
v += gaussian(t, r_center, 0.02, r_amp)
# S wave
v += gaussian(t, s_center, 0.02, s_amp)
# ST segment + T wave
t_center = s_center + 0.16
st_level = st_elevation - st_depression
# ST shift as a broad gaussian
v += gaussian(t, (s_center + t_center) / 2, 0.06, st_level * 0.8)
t_amp_final = -abs(t_amp) if t_invert else abs(t_amp)
v += gaussian(t, t_center, 0.05, t_amp_final)
return t, v
def build_ecg_signal(conditions_params, duration=6.0, fs=500, noise=0.012,
fibrillation=False, flutter=False, flutter_rate=300,
irregular_rr=False, baseline_wander=False):
"""
Stitch multiple PQRST complexes into a full strip.
conditions_params: list of dicts passed to make_pqrst
"""
t_all = np.linspace(0, duration, int(duration * fs))
v_all = np.random.normal(0, noise, len(t_all))
if fibrillation:
# AF: chaotic baseline, no P, irregular RR
fibrillation_noise = 0.12 * np.sin(2 * np.pi * 6 * t_all)
fibrillation_noise += 0.08 * np.sin(2 * np.pi * 8.3 * t_all + 1.1)
fibrillation_noise += 0.06 * np.sin(2 * np.pi * 10.7 * t_all + 2.3)
v_all += fibrillation_noise
# Irregular RR QRS complexes
np.random.seed(42)
t_pos = 0.4
while t_pos < duration - 0.5:
rr = np.random.uniform(0.5, 1.1) # irregular
kw = conditions_params[0].copy()
kw['p_absent'] = True
kw['t_start'] = t_pos
_, v_seg = make_pqrst(**kw)
seg_t = np.linspace(t_pos, t_pos + rr, len(v_seg))
idx = np.searchsorted(t_all, seg_t)
idx = np.clip(idx, 0, len(t_all) - 1)
np.add.at(v_all, idx, v_seg)
t_pos += rr
return t_all, v_all
if flutter:
# Sawtooth flutter waves
flutter_period = 60.0 / flutter_rate
flutter_wave = 0.25 * (2 * ((t_all / flutter_period) % 1) - 1)
v_all += flutter_wave
# Regular but slower ventricular response (2:1 -> ~150 bpm)
t_pos = 0.3
while t_pos < duration - 0.5:
kw = conditions_params[0].copy()
kw['p_absent'] = True
kw['t_start'] = t_pos
_, v_seg = make_pqrst(**kw)
seg_t = np.linspace(t_pos, t_pos + 0.4, len(v_seg))
idx = np.searchsorted(t_all, seg_t)
idx = np.clip(idx, 0, len(t_all) - 1)
np.add.at(v_all, idx, v_seg)
t_pos += 0.4
return t_all, v_all
if baseline_wander:
v_all += 0.1 * np.sin(2 * np.pi * 0.3 * t_all)
t_pos = 0.3
param_idx = 0
while t_pos < duration - 0.5:
kw = conditions_params[param_idx % len(conditions_params)].copy()
hr = kw.get('hr_bpm', 75)
rr = 60.0 / hr
if irregular_rr:
rr += np.random.uniform(-0.05, 0.05)
kw['t_start'] = t_pos
_, v_seg = make_pqrst(**kw)
seg_t = np.linspace(t_pos, t_pos + rr, len(v_seg))
idx = np.searchsorted(t_all, seg_t)
idx = np.clip(idx, 0, len(t_all) - 1)
np.add.at(v_all, idx, v_seg)
t_pos += rr
param_idx += 1
return t_all, v_all
# ─── CONDITION DEFINITIONS ────────────────────────────────────────────────────
CONDITIONS = [
{
"num": 1,
"name": "ST-Elevation MI (STEMI)",
"subtitle": "Acute Anterior STEMI (LAD Occlusion)",
"color": "#c0392b",
"ecg_params": {
"conditions_params": [{"hr_bpm": 95, "st_elevation": 0.35, "t_amp": 0.45,
"r_amp": 0.6, "q_amp": -0.35, "q_wide": True,
"p_amp": 0.12}],
"duration": 6.0,
},
"key_features": [
"ST elevation ≥1 mm in ≥2 contiguous leads",
"Hyperacute (peaked) T waves — earliest change",
"Pathological Q waves (>40 ms, >25% R-wave height)",
"Reciprocal ST depression in opposite leads",
"Anterior STEMI: ST ↑ in V1–V4 (LAD territory)",
"Inferior STEMI: ST ↑ in II, III, aVF (RCA territory)",
],
"summary": "Occlusion of a coronary artery causes transmural ischemia. Anoxic injury "
"raises the resting membrane potential of epicardial cells relative to normal "
"cells, producing apparent ST elevation. Electrically silent dead tissue "
"causes depolarization vectors to point away → deep Q waves.",
},
{
"num": 2,
"name": "NSTEMI / Unstable Angina",
"subtitle": "Non-ST Elevation ACS",
"color": "#e67e22",
"ecg_params": {
"conditions_params": [{"hr_bpm": 88, "st_depression": 0.20, "t_amp": 0.18,
"t_invert": True, "r_amp": 1.0, "p_amp": 0.14}],
"duration": 6.0,
},
"key_features": [
"Horizontal or downsloping ST depression ≥0.5 mm",
"Symmetrical T-wave inversion",
"No pathological Q waves",
"ECG may be normal at rest (dynamic changes during pain)",
"NSTEMI: same ECG + elevated troponin",
"Unstable angina: same ECG + normal biomarkers",
],
"summary": "Partial coronary occlusion causes subendocardial ischemia. The endocardium "
"(innermost layer) is most vulnerable to ischemia. Injury currents produce "
"ST depression and T-wave changes but NOT Q waves since full-thickness "
"necrosis does not occur.",
},
{
"num": 3,
"name": "Atrial Fibrillation (AF)",
"subtitle": "Uncontrolled Ventricular Rate",
"color": "#8e44ad",
"ecg_params": {
"conditions_params": [{"hr_bpm": 130, "r_amp": 0.9, "s_amp": -0.2,
"t_amp": 0.25, "p_absent": True}],
"duration": 6.0,
"fibrillation": True,
},
"key_features": [
"Absent P waves — replaced by irregular fibrillatory baseline (f-waves)",
"Irregularly irregular RR intervals — the hallmark",
"Ventricular rate 100–180 bpm if uncontrolled",
"Narrow QRS (unless aberrant conduction)",
"f-wave frequency >350/min, variable morphology",
"May show LVH if hypertensive etiology",
],
"summary": "Multiple chaotic re-entry wavelets in the atria produce fibrillatory "
"baseline. The AV node is bombarded by >350 impulses/min; it conducts "
"irregularly producing the hallmark irregular ventricular response. "
"Absent P waves confirm the diagnosis.",
},
{
"num": 4,
"name": "Atrial Flutter",
"subtitle": "Typical 2:1 AV Conduction (~150 bpm)",
"color": "#2980b9",
"ecg_params": {
"conditions_params": [{"hr_bpm": 150, "r_amp": 0.85, "t_amp": 0.2,
"p_absent": True}],
"duration": 6.0,
"flutter": True,
"flutter_rate": 300,
},
"key_features": [
"Sawtooth flutter waves (F-waves) at 250–350/min",
"Most visible in II, III, aVF and V1",
"Ventricular rate = atrial rate ÷ AV ratio (2:1 → ~150 bpm)",
"Regular ventricular rhythm (unless variable block)",
"No isoelectric baseline between flutter waves",
"Narrow QRS unless aberrant conduction",
],
"summary": "A single macro-re-entrant circuit in the right atrium (cavotricuspid "
"isthmus) produces regular atrial activity at ~300/min. The AV node "
"filters this, most commonly allowing every second impulse through "
"(2:1 block), giving a ventricular rate of ~150 bpm.",
},
{
"num": 5,
"name": "Acute Pericarditis",
"subtitle": "Stage 1 — Diffuse ST Elevation + PR Depression",
"color": "#16a085",
"ecg_params": {
"conditions_params": [{"hr_bpm": 90, "st_elevation": 0.22, "t_amp": 0.4,
"r_amp": 0.9, "p_amp": -0.08, # PR depression via negative P tail
"q_amp": -0.05}],
"duration": 6.0,
},
"key_features": [
"Diffuse (saddle-shaped / concave-up) ST elevation in most leads",
"PR depression in limb leads + V4–V6",
"PR elevation in aVR (reciprocal to PR depression)",
"ST elevation NOT focal — affects multiple territories",
"No reciprocal ST depression (unlike STEMI)",
"Stages: ST↑ → normalise → T inversion → ECG normalises",
],
"summary": "Inflammation of the pericardium causes a current of injury across the "
"epicardial surface. Because the entire heart surface is affected, "
"ST changes are diffuse (not territory-specific). PR depression "
"reflects atrial injury — a hallmark that distinguishes pericarditis "
"from STEMI.",
},
{
"num": 6,
"name": "LVH — Hypertensive Heart Disease",
"subtitle": "Left Ventricular Hypertrophy with Strain Pattern",
"color": "#d35400",
"ecg_params": {
"conditions_params": [{"hr_bpm": 72, "r_amp": 2.4, "s_amp": -0.5,
"st_depression": 0.12, "t_invert": True,
"t_amp": 0.35, "p_amp": 0.18, "p_notch": True}],
"duration": 6.0,
},
"key_features": [
"Tall R waves in lateral leads (V5/V6) ≥26 mm",
"Deep S waves in right precordial leads (V1/V2)",
"Sokolow-Lyon: S(V1) + R(V5/V6) ≥35 mm",
"Strain pattern: ST depression + T-wave inversion in I, aVL, V5–V6",
"Left axis deviation",
"Broad notched P-wave (P-mitrale) — left atrial enlargement",
],
"summary": "Chronic pressure overload from hypertension causes concentric LVH. "
"More myocardial mass means more depolarization voltage → tall R waves. "
"The strain pattern (lateral ST depression + T inversion) reflects "
"subendocardial ischemia from demand outstripping supply in the "
"thickened wall.",
},
{
"num": 7,
"name": "Aortic Stenosis",
"subtitle": "Severe AS — LVH + Conduction Disease",
"color": "#7f8c8d",
"ecg_params": {
"conditions_params": [{"hr_bpm": 68, "r_amp": 2.1, "s_amp": -0.45,
"st_depression": 0.15, "t_invert": True,
"t_amp": 0.30, "p_amp": 0.16,
"qrs_width": 0.13}], # LBBB-like wide QRS
"duration": 6.0,
},
"key_features": [
"LVH voltage criteria (pressure overload pattern)",
"LV strain: ST depression + T inversion in lateral leads",
"Left bundle branch block (LBBB) — wide QRS ≥120 ms",
"Left axis deviation",
"PR prolongation (1st degree AV block) may coexist",
"Atrial fibrillation in decompensated/late disease",
],
"summary": "Outflow obstruction creates severe pressure overload → concentric LVH "
"with strain. Progressive fibrosis of the conduction system causes "
"bundle branch blocks. LBBB in the setting of AS is a poor prognostic "
"sign indicating advanced myocardial remodelling.",
},
{
"num": 8,
"name": "Hypertrophic Cardiomyopathy (HCM)",
"subtitle": "Asymmetric Septal Hypertrophy",
"color": "#1abc9c",
"ecg_params": {
"conditions_params": [{"hr_bpm": 78, "r_amp": 2.2, "q_amp": -0.55,
"q_wide": False, "s_amp": -0.3,
"st_depression": 0.08, "t_invert": True,
"t_amp": 0.4, "p_amp": 0.17}],
"duration": 6.0,
},
"key_features": [
"LVH voltage (often striking — largest voltages in cardiology)",
"Deep narrow ('septal') Q waves in inferior + lateral leads",
"Q waves due to hypertrophied septum, NOT infarction",
"T-wave inversion in lateral leads (I, aVL, V5–V6)",
"Left axis deviation",
"Giant negative T waves in apical variant (Yamaguchi syndrome)",
],
"summary": "Asymmetric septal hypertrophy produces abnormal septal depolarization "
"vectors. Because the hypertrophied septum depolarizes early and "
"abnormally (right-to-left), leads overlying the lateral wall see "
"a large initial negative deflection → deep septal Q waves. "
"Unlike infarction Q waves, these are narrow (<40 ms).",
},
{
"num": 9,
"name": "Pulmonary Embolism (PE)",
"subtitle": "Acute Right Heart Strain — S1Q3T3 Pattern",
"color": "#2c3e50",
"ecg_params": {
"conditions_params": [{"hr_bpm": 110, "r_amp": 0.7, "s_amp": -0.55,
"q_amp": -0.3, "t_invert": True, "t_amp": 0.25,
"st_elevation": 0.04, "p_amp": 0.22}],
"duration": 6.0,
},
"key_features": [
"Sinus tachycardia — most common and sensitive finding",
"S1Q3T3 pattern: deep S in I, Q wave + T inversion in III",
"New right bundle branch block (complete or incomplete)",
"Right axis deviation",
"T-wave inversion in V1–V4 (right ventricular strain)",
"P pulmonale: peaked P waves >2.5 mm in lead II",
],
"summary": "Massive PE obstructs the pulmonary vasculature, acutely raising RV "
"afterload. The RV dilates and shifts the interventricular septum leftward "
"(D-sign). Delayed RV conduction → RBBB. Right axis shift and "
"precordial T-wave inversions reflect RV strain. S1Q3T3 is "
"classic but present in only ~20% of cases.",
},
{
"num": 10,
"name": "Wolff-Parkinson-White (WPW)",
"subtitle": "Ventricular Pre-excitation via Accessory Pathway",
"color": "#27ae60",
"ecg_params": {
"conditions_params": [{"hr_bpm": 80, "r_amp": 1.1, "q_amp": -0.05,
"t_amp": 0.20, "t_invert": False,
"delta_wave": True, "qrs_width": 0.13,
"p_amp": 0.15}],
"duration": 6.0,
},
"key_features": [
"Short PR interval (<120 ms) — early ventricular activation",
"Delta wave — slurred initial QRS upstroke",
"Wide QRS (≥120 ms) — total conduction time increased",
"Secondary ST-T changes (not ischemic — repolarization abnormality)",
"Pseudo-Q waves in some leads (can mimic infarction)",
"Risk: AF → rapid conduction → ventricular fibrillation",
],
"summary": "An accessory pathway (Bundle of Kent) bypasses the AV node, allowing "
"early ventricular activation. This produces the delta wave (slow initial "
"conduction through working myocardium) and short PR. Once the normal "
"His-Purkinje system fires, conduction accelerates — producing the "
"characteristic 'fused' wide QRS.",
},
]
# ─── PLOT GENERATION ─────────────────────────────────────────────────────────
ECG_GRID_COLOR = "#ffb3b3"
ECG_TRACE_COLOR = "#111111"
LEAD_BG = "#fff5f5"
def plot_ecg_strip(condition, width_px=1400, height_px=220, dpi=150):
"""
Generate a single-lead ECG strip for a condition.
Returns a BytesIO PNG buffer.
"""
np.random.seed(12345)
params = condition["ecg_params"]
t, v = build_ecg_signal(**params)
fig, ax = plt.subplots(figsize=(width_px / dpi, height_px / dpi), dpi=dpi)
fig.patch.set_facecolor(LEAD_BG)
ax.set_facecolor(LEAD_BG)
# ECG grid
major_interval_t = 0.2 # 200 ms large square
major_interval_v = 0.5 # 0.5 mV large square
minor_interval_t = 0.04 # 40 ms small square
minor_interval_v = 0.1 # 0.1 mV small square
for xv in np.arange(0, t[-1] + minor_interval_t, minor_interval_t):
ax.axvline(xv, color=ECG_GRID_COLOR, linewidth=0.3, zorder=0)
for yv in np.arange(-1.5, 2.5, minor_interval_v):
ax.axhline(yv, color=ECG_GRID_COLOR, linewidth=0.3, zorder=0)
for xv in np.arange(0, t[-1] + major_interval_t, major_interval_t):
ax.axvline(xv, color="#ff8080", linewidth=0.7, zorder=0)
for yv in np.arange(-1.5, 2.5, major_interval_v):
ax.axhline(yv, color="#ff8080", linewidth=0.7, zorder=0)
# Zero line
ax.axhline(0, color="#cc0000", linewidth=0.5, alpha=0.5, zorder=1)
# Trace
ax.plot(t, v, color=ECG_TRACE_COLOR, linewidth=1.2, zorder=2)
# Calibration pulse (1 mV, 0.2s)
cal_t = [0.02, 0.02, 0.10, 0.10, 0.22, 0.22]
cal_v = [0, 1.0, 1.0, 0, 0, 0]
ax.plot(cal_t, cal_v, color=ECG_TRACE_COLOR, linewidth=1.2, zorder=3)
ax.set_xlim(0, t[-1])
ax.set_ylim(-1.4, 1.8)
ax.axis('off')
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.02,
facecolor=LEAD_BG)
plt.close(fig)
buf.seek(0)
return buf
def plot_annotation_diagram(condition, width_px=420, height_px=200, dpi=120):
"""
Small annotated PQRST diagram showing the key abnormality labels.
Returns a BytesIO PNG buffer.
"""
np.random.seed(999)
name = condition["name"]
color = condition["color"]
fig, ax = plt.subplots(figsize=(width_px / dpi, height_px / dpi), dpi=dpi)
fig.patch.set_facecolor('white')
ax.set_facecolor('white')
# Single normal PQRST for reference
params_ref = {"hr_bpm": 72, "p_amp": 0.15, "pr_interval": 0.16,
"qrs_width": 0.08, "r_amp": 1.0, "q_amp": -0.1,
"s_amp": -0.2, "t_amp": 0.3, "t_start": 0.15}
# Override with condition-specific params for the annotation
cp = condition["ecg_params"]["conditions_params"][0].copy()
cp["t_start"] = 0.15
cp["hr_bpm"] = 72
t, v = make_pqrst(**cp)
t2 = t - t[0]
ax.plot(t2, v, color=color, linewidth=2.0, zorder=2)
ax.axhline(0, color='#888', linewidth=0.5, linestyle='--', zorder=1)
ax.set_xlim(-0.05, t2[-1] * 0.75)
ymin, ymax = min(v) - 0.3, max(v) + 0.4
ax.set_ylim(ymin, ymax)
ax.axis('off')
ax.set_title(f"Key Waveform: {name}", fontsize=7, color=color,
fontweight='bold', pad=3)
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.05,
facecolor='white')
plt.close(fig)
buf.seek(0)
return buf
# ─── PDF BUILDER ─────────────────────────────────────────────────────────────
def build_pdf():
output_path = os.path.join(OUT_DIR, "ECG_Reference_Guide.pdf")
# Custom page template with header/footer
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=1.8 * cm,
rightMargin=1.8 * cm,
topMargin=2.2 * cm,
bottomMargin=2.0 * cm,
title="ECG Reference Guide — 10 Common Heart Diseases",
author="Orris Medical Reference",
subject="Electrocardiography",
)
W, H = A4
page_w = W - 3.6 * cm # usable width
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
'TitleCustom', parent=styles['Title'],
fontSize=22, leading=28, spaceAfter=4,
textColor=colors.HexColor('#1a1a2e'),
fontName='Helvetica-Bold',
)
subtitle_style = ParagraphStyle(
'SubTitle', parent=styles['Normal'],
fontSize=11, leading=14, spaceAfter=16,
textColor=colors.HexColor('#555555'),
alignment=TA_CENTER,
)
cond_title_style = ParagraphStyle(
'CondTitle', parent=styles['Heading1'],
fontSize=14, leading=18, spaceBefore=4, spaceAfter=2,
fontName='Helvetica-Bold',
)
cond_subtitle_style = ParagraphStyle(
'CondSubTitle', parent=styles['Normal'],
fontSize=9, leading=12, spaceAfter=6,
textColor=colors.HexColor('#666666'),
fontName='Helvetica-Oblique',
)
body_style = ParagraphStyle(
'BodyCustom', parent=styles['Normal'],
fontSize=8.5, leading=13, spaceAfter=4,
textColor=colors.HexColor('#222222'),
alignment=TA_JUSTIFY,
fontName='Helvetica',
)
feature_style = ParagraphStyle(
'Feature', parent=styles['Normal'],
fontSize=8, leading=12,
textColor=colors.HexColor('#1a1a1a'),
fontName='Helvetica',
leftIndent=8,
spaceAfter=1,
)
section_label_style = ParagraphStyle(
'SectionLabel', parent=styles['Normal'],
fontSize=7.5, leading=10, spaceAfter=2,
textColor=colors.HexColor('#888888'),
fontName='Helvetica-Bold',
)
toc_style = ParagraphStyle(
'TOC', parent=styles['Normal'],
fontSize=9.5, leading=16,
fontName='Helvetica',
textColor=colors.HexColor('#333333'),
)
footer_note_style = ParagraphStyle(
'FooterNote', parent=styles['Normal'],
fontSize=7, leading=9,
textColor=colors.HexColor('#999999'),
alignment=TA_CENTER,
)
story = []
# ── Cover Page ──────────────────────────────────────────────────────────
story.append(Spacer(1, 1.5 * cm))
# Top accent bar (via a colored table)
accent_data = [['']]
accent_table = Table(accent_data, colWidths=[page_w], rowHeights=[6])
accent_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#c0392b')),
('LINEABOVE', (0, 0), (-1, -1), 0, colors.white),
]))
story.append(accent_table)
story.append(Spacer(1, 0.5 * cm))
story.append(Paragraph("ECG Reference Guide", title_style))
story.append(Paragraph("10 Common Heart Diseases & Their Electrocardiographic Patterns", subtitle_style))
story.append(Spacer(1, 0.3 * cm))
# Subtitle bar
bar_data = [[Paragraph(
"Waveform Illustrations · Diagnostic Features · Clinical Mechanisms · Lead Localisation",
ParagraphStyle('BarText', parent=styles['Normal'], fontSize=8.5,
textColor=colors.white, alignment=TA_CENTER, fontName='Helvetica')
)]]
bar = Table(bar_data, colWidths=[page_w], rowHeights=[22])
bar.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#2c3e50')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
]))
story.append(bar)
story.append(Spacer(1, 0.8 * cm))
# Table of Contents
toc_rows = []
for c in CONDITIONS:
toc_rows.append([
Paragraph(f"<b>{c['num']:02d}.</b>", toc_style),
Paragraph(f"<b>{c['name']}</b>", toc_style),
Paragraph(c['subtitle'], ParagraphStyle('TOCSub', parent=toc_style,
fontSize=8, textColor=colors.HexColor('#777777'))),
])
toc_table = Table(toc_rows, colWidths=[1.2 * cm, 7.5 * cm, None])
toc_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LINEBELOW', (0, 0), (-1, -2), 0.3, colors.HexColor('#dddddd')),
('ROWBACKGROUNDS', (0, 0), (-1, -1),
[colors.HexColor('#f9f9f9'), colors.white]),
]))
story.append(toc_table)
story.append(Spacer(1, 1.0 * cm))
# Reference note
ref_note = ("Waveforms are schematic illustrations for educational purposes. "
"ECG patterns are generated to represent canonical textbook appearances. "
"Sources: Braunwald's Heart Disease; Goldman-Cecil Medicine; "
"Medical Physiology (Boron & Boulpaep); Miller's Anesthesia; "
"Tintinalli's Emergency Medicine.")
story.append(Paragraph(ref_note, footer_note_style))
story.append(PageBreak())
# ── Quick Reference Summary Table ────────────────────────────────────────
story.append(Paragraph("Quick Reference Summary", cond_title_style))
story.append(HRFlowable(width=page_w, thickness=2, color=colors.HexColor('#c0392b'),
spaceAfter=8))
hdr = [
Paragraph('<b>#</b>', ParagraphStyle('th', parent=styles['Normal'],
fontSize=8, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Condition</b>', ParagraphStyle('th', parent=styles['Normal'],
fontSize=8, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Most Diagnostic ECG Finding</b>', ParagraphStyle('th', parent=styles['Normal'],
fontSize=8, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Leads / Location</b>', ParagraphStyle('th', parent=styles['Normal'],
fontSize=8, fontName='Helvetica-Bold', textColor=colors.white)),
]
summary_data = [hdr]
row_highlights = [
("STEMI", "ST elevation ≥1 mm → pathological Q waves", "V1–V4 (ant) / II,III,aVF (inf)"),
("NSTEMI / UA", "ST depression + T-wave inversion, no Q waves", "Any leads, dynamic"),
("Atrial Fibrillation", "Absent P waves, irregularly irregular RR", "All leads"),
("Atrial Flutter", "Sawtooth F-waves ~300/min, 2:1 AV block", "II, III, aVF, V1"),
("Acute Pericarditis", "Diffuse concave ST elevation + PR depression", "All leads except aVR/V1"),
("LVH / Hyp. Heart Disease", "Tall R (V5/V6) + deep S (V1) + lateral strain", "Lateral leads"),
("Aortic Stenosis", "LVH voltage + LV strain + LBBB", "Lateral + precordial"),
("Hypertrophic CM", "Deep narrow septal Q waves + LVH voltage", "II,III,aVF; I,aVL,V5–V6"),
("Pulmonary Embolism", "Sinus tachycardia + S1Q3T3 + RBBB + RV strain", "I, III, V1–V4"),
("WPW Syndrome", "Short PR + delta wave + wide QRS", "Depends on pathway location"),
]
for i, (cond, finding, leads) in enumerate(row_highlights):
c_obj = CONDITIONS[i]
row = [
Paragraph(f"<b>{i+1}</b>", ParagraphStyle('tdc', parent=styles['Normal'],
fontSize=8, textColor=colors.HexColor(c_obj['color']),
fontName='Helvetica-Bold')),
Paragraph(f"<b>{cond}</b>", ParagraphStyle('tdc', parent=styles['Normal'],
fontSize=8, fontName='Helvetica-Bold')),
Paragraph(finding, ParagraphStyle('tdc', parent=styles['Normal'], fontSize=7.5)),
Paragraph(leads, ParagraphStyle('tdc', parent=styles['Normal'],
fontSize=7.5, textColor=colors.HexColor('#555'))),
]
summary_data.append(row)
summary_table = Table(summary_data,
colWidths=[0.8 * cm, 4.8 * cm, 7.2 * cm, 4.0 * cm])
summary_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.HexColor('#f7f7f7'), colors.white]),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('LINEBELOW', (0, 0), (-1, -1), 0.3, colors.HexColor('#dddddd')),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#cccccc')),
]))
story.append(summary_table)
story.append(PageBreak())
# ── Individual Condition Pages ────────────────────────────────────────────
for cond in CONDITIONS:
print(f" Rendering: {cond['name']} ...")
# Section header bar
header_data = [[
Paragraph(
f"<font color='white'><b>{cond['num']:02d}</b></font>",
ParagraphStyle('hnum', parent=styles['Normal'],
fontSize=16, fontName='Helvetica-Bold',
textColor=colors.white, alignment=TA_CENTER)
),
Paragraph(
f"<font color='white'><b>{cond['name']}</b></font>",
ParagraphStyle('hname', parent=styles['Normal'],
fontSize=13, fontName='Helvetica-Bold',
textColor=colors.white)
),
]]
header_table = Table(header_data, colWidths=[1.6 * cm, page_w - 1.6 * cm],
rowHeights=[28])
header_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, 0), colors.HexColor(cond['color'])),
('BACKGROUND', (1, 0), (1, 0), colors.HexColor('#2c3e50')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (0, 0), 5),
('LEFTPADDING', (1, 0), (1, 0), 10),
]))
story.append(header_table)
story.append(Paragraph(cond['subtitle'], cond_subtitle_style))
story.append(Spacer(1, 0.15 * cm))
# ECG Strip
story.append(Paragraph("RHYTHM STRIP (Lead II equivalent — schematic illustration)",
section_label_style))
ecg_buf = plot_ecg_strip(cond)
ecg_img = Image(ecg_buf, width=page_w, height=3.5 * cm)
story.append(ecg_img)
story.append(Spacer(1, 0.25 * cm))
# Two-column layout: features + mechanism
features_content = [
Paragraph("KEY ECG FEATURES", section_label_style),
]
for feat in cond['key_features']:
features_content.append(
Paragraph(f"• {feat}", feature_style)
)
mechanism_content = [
Paragraph("MECHANISM", section_label_style),
Paragraph(cond['summary'], body_style),
]
# Annotated waveform
ann_buf = plot_annotation_diagram(cond)
ann_img = Image(ann_buf, width=8.0 * cm, height=3.8 * cm)
mechanism_content.append(ann_img)
feat_table = Table(
[[features_content, mechanism_content]],
colWidths=[page_w * 0.48, page_w * 0.52],
)
feat_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 8),
('LINERIGHT', (0, 0), (0, -1), 0.5, colors.HexColor('#dddddd')),
]))
story.append(feat_table)
# Bottom rule
story.append(Spacer(1, 0.3 * cm))
story.append(HRFlowable(width=page_w, thickness=1,
color=colors.HexColor(cond['color']),
spaceAfter=4))
# Footer note
story.append(Paragraph(
f"Section {cond['num']}/10 • ECG Reference Guide • Educational use only",
footer_note_style
))
story.append(PageBreak())
# ── Final Reference Page ─────────────────────────────────────────────────
story.append(Paragraph("ECG Lead Localisation Reference", cond_title_style))
story.append(HRFlowable(width=page_w, thickness=2, color=colors.HexColor('#2c3e50'),
spaceAfter=10))
localisation_data = [
[Paragraph('<b>Territory</b>', ParagraphStyle('lh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Leads with Changes</b>', ParagraphStyle('lh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Artery (typical)</b>', ParagraphStyle('lh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Reciprocal Changes</b>', ParagraphStyle('lh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white))],
["Anterior", "V1, V2, V3, V4", "LAD", "II, III, aVF"],
["Lateral", "I, aVL, V5, V6", "LCx", "V1, V2"],
["Inferior", "II, III, aVF", "RCA (80%)", "I, aVL"],
["Posterior", "V7, V8, V9 (↑); V1–V3 mirror ↓", "RCA / LCx", "V1–V3 tall R"],
["Septal", "V1, V2", "LAD septal perforators", "None"],
["RV infarct", "V1, V3R, V4R", "Proximal RCA", "—"],
]
loc_table = Table(localisation_data,
colWidths=[3.5 * cm, 5.5 * cm, 4.5 * cm, 4.5 * cm])
loc_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.HexColor('#f0f4f8'), colors.white]),
('FONTSIZE', (0, 0), (-1, -1), 8.5),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#aaaaaa')),
('LINEBELOW', (0, 0), (-1, -1), 0.3, colors.HexColor('#dddddd')),
]))
story.append(loc_table)
story.append(Spacer(1, 0.8 * cm))
# AV Block summary
story.append(Paragraph("Heart Block — ECG Summary", cond_title_style))
story.append(HRFlowable(width=page_w, thickness=2, color=colors.HexColor('#8e44ad'),
spaceAfter=10))
hb_rows = [
[Paragraph('<b>Type</b>', ParagraphStyle('hbh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>PR Interval</b>', ParagraphStyle('hbh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>QRS Drop?</b>', ParagraphStyle('hbh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white)),
Paragraph('<b>Key Feature</b>', ParagraphStyle('hbh', parent=styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white))],
["1st Degree AV Block", ">200 ms (fixed)", "No", "PR prolonged but all P waves conduct"],
["2nd Degree — Mobitz I (Wenckebach)", "Progressive lengthening", "Yes (periodic)", "PR lengthens → QRS dropped → resets"],
["2nd Degree — Mobitz II", "Fixed (normal or long)", "Yes (sudden)", "Sudden QRS drop without PR change"],
["3rd Degree (Complete)", "Variable (AV dissociation)", "Yes (all)", "P and QRS independent; escape rhythm"],
]
hb_table = Table(hb_rows, colWidths=[4.5 * cm, 3.5 * cm, 3.0 * cm, 7.0 * cm])
hb_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#8e44ad')),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.HexColor('#f9f5ff'), colors.white]),
('FONTSIZE', (0, 0), (-1, -1), 8),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#aaaaaa')),
('LINEBELOW', (0, 0), (-1, -1), 0.3, colors.HexColor('#dddddd')),
]))
story.append(hb_table)
story.append(Spacer(1, 1.2 * cm))
story.append(HRFlowable(width=page_w, thickness=1, color=colors.HexColor('#cccccc'),
spaceAfter=6))
story.append(Paragraph(
"ECG Reference Guide · Orris Medical Reference · 2026 · "
"Sources: Braunwald's Heart Disease; Goldman-Cecil Medicine; Medical Physiology "
"(Boron & Boulpaep); Tintinalli's Emergency Medicine; Miller's Anesthesia · "
"For educational and clinical reference use only.",
footer_note_style
))
print("Building PDF...")
doc.build(story)
print(f"Done: {output_path}")
return output_path
if __name__ == "__main__":
path = build_pdf()
print(f"OUTPUT: {path}")
cd /home/daytona/workspace/ecg-reference && python generate_ecg_pdf.py 2>&1
ls -lh /home/daytona/workspace/ecg-reference/ECG_Reference_Guide.pdf
ECG Reference Guide
PDF Document · PDF
| Section | Content |
|---|---|
| Cover page | Title, subtitle bar, full table of contents with all 10 conditions |
| Quick Reference Summary | Single-table overview of every condition's most diagnostic ECG finding and lead location |
| 10 individual condition pages | One page per condition (see below) |
| Lead Localisation Reference | Territory → leads → culprit artery → reciprocal changes table |
| Heart Block Summary | 1st, 2nd (Mobitz I/II), and 3rd degree AV block comparison table |