Can u suggest me a best pdf for understanding complete ecg for different disorders
best free PDF ECG interpretation complete guide disorders download
| Book | Best For |
|---|---|
| The ECG Made Easy - Hamp ton | Absolute beginners |
| The ECG in Practice - Hamp ton | Intermediate, clinical correlation |
| Marriott's Practical Electrocardiography | Advanced/comprehensive |
| ECG Interpretation: The Self-Assessment Approach - Thaler | Self-study with Q&A format |
| The Only EKG Book You'll Ever Need - Thaler | Popular all-in-one text |
Create a summary PDF guide on ECG disorders & interpretation
mkdir -p /tmp/workspace/ecg-guide && ls /tmp/workspace/
~/ecg-guide/create_ecg_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate
import os
OUTPUT_PATH = "/tmp/workspace/ecg-guide/ECG_Complete_Guide.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
RED = colors.HexColor("#C0392B")
DARK_RED = colors.HexColor("#96281B")
NAVY = colors.HexColor("#1A2E4A")
BLUE = colors.HexColor("#2980B9")
LIGHT_BLUE= colors.HexColor("#D6EAF8")
TEAL = colors.HexColor("#148F77")
LIGHT_TEAL= colors.HexColor("#D1F2EB")
ORANGE = colors.HexColor("#E67E22")
LIGHT_ORANGE = colors.HexColor("#FDEBD0")
PURPLE = colors.HexColor("#6C3483")
LIGHT_PURPLE = colors.HexColor("#E8DAEF")
GREEN = colors.HexColor("#1E8449")
LIGHT_GREEN = colors.HexColor("#D5F5E3")
YELLOW_BG = colors.HexColor("#FEF9E7")
GRAY_LIGHT= colors.HexColor("#F2F3F4")
GRAY_MID = colors.HexColor("#BFC9CA")
WHITE = colors.white
BLACK = colors.HexColor("#1C1C1C")
# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def style(name, **kw):
s = ParagraphStyle(name, **kw)
return s
TITLE_STYLE = style("MyTitle",
fontName="Helvetica-Bold", fontSize=28, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=6, leading=34)
SUBTITLE_STYLE = style("MySubtitle",
fontName="Helvetica", fontSize=13, textColor=colors.HexColor("#D6EAF8"),
alignment=TA_CENTER, spaceAfter=4, leading=16)
CHAPTER_STYLE = style("Chapter",
fontName="Helvetica-Bold", fontSize=16, textColor=WHITE,
alignment=TA_LEFT, spaceAfter=4, spaceBefore=4, leading=20)
SECTION_STYLE = style("Section",
fontName="Helvetica-Bold", fontSize=12, textColor=NAVY,
alignment=TA_LEFT, spaceAfter=3, spaceBefore=6, leading=15)
SUBSECTION_STYLE = style("Subsection",
fontName="Helvetica-Bold", fontSize=10.5, textColor=TEAL,
alignment=TA_LEFT, spaceAfter=2, spaceBefore=4, leading=13)
BODY_STYLE = style("Body",
fontName="Helvetica", fontSize=9.5, textColor=BLACK,
alignment=TA_JUSTIFY, spaceAfter=3, leading=14)
BULLET_STYLE = style("Bullet",
fontName="Helvetica", fontSize=9.5, textColor=BLACK,
alignment=TA_LEFT, spaceAfter=2, leading=13,
leftIndent=14, bulletIndent=4)
BOLD_BODY = style("BoldBody",
fontName="Helvetica-Bold", fontSize=9.5, textColor=BLACK,
alignment=TA_LEFT, spaceAfter=2, leading=13)
TABLE_HEADER = style("TH",
fontName="Helvetica-Bold", fontSize=9, textColor=WHITE,
alignment=TA_CENTER, leading=12)
TABLE_CELL = style("TC",
fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_LEFT, leading=12)
TABLE_CELL_C = style("TCC",
fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_CENTER, leading=12)
CAPTION_STYLE = style("Caption",
fontName="Helvetica-Oblique", fontSize=8.5, textColor=colors.HexColor("#555555"),
alignment=TA_CENTER, spaceAfter=4, leading=11)
WARNING_STYLE = style("Warning",
fontName="Helvetica-Bold", fontSize=9.5, textColor=DARK_RED,
alignment=TA_LEFT, spaceAfter=2, leading=13, leftIndent=10)
# ── Helper functions ──────────────────────────────────────────────────────────
def chapter_header(title, color=NAVY):
"""Returns a styled chapter header block."""
tbl = Table([[Paragraph(title, CHAPTER_STYLE)]], colWidths=[170*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("ROUNDEDCORNERS", [4,4,4,4]),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 12),
]))
return tbl
def section_box(title, color=LIGHT_BLUE, tcolor=NAVY):
s = style("sb", fontName="Helvetica-Bold", fontSize=11,
textColor=tcolor, alignment=TA_LEFT, leading=14)
tbl = Table([[Paragraph(title, s)]], colWidths=[170*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 10),
("LINEBELOW", (0,0), (-1,-1), 1, tcolor),
]))
return tbl
def info_box(text, bg=YELLOW_BG, border=ORANGE):
s = style("ib", fontName="Helvetica", fontSize=9.5, textColor=BLACK,
alignment=TA_LEFT, leading=13, leftIndent=4)
tbl = Table([[Paragraph(text, s)]], colWidths=[165*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1.2, border),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
]))
return tbl
def bullet(text, indent=14):
return Paragraph(f"<bullet>•</bullet> {text}", BULLET_STYLE)
def subbullet(text):
s = style("sb2", fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_LEFT, leading=12, leftIndent=26, bulletIndent=16)
return Paragraph(f"<bullet>◦</bullet> {text}", s)
def sp(h=4):
return Spacer(1, h*mm)
def hr(color=GRAY_MID, thickness=0.5):
return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=2*mm, spaceBefore=2*mm)
def make_table(headers, rows, col_widths, header_bg=NAVY, alt_bg=GRAY_LIGHT):
header_row = [Paragraph(h, TABLE_HEADER) for h in headers]
data = [header_row]
for i, row in enumerate(rows):
bg = WHITE if i % 2 == 0 else alt_bg
data.append([Paragraph(str(c), TABLE_CELL) for c in row])
tbl = Table(data, colWidths=col_widths)
style_cmds = [
("BACKGROUND", (0,0), (-1,0), header_bg),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, alt_bg]),
("GRID", (0,0), (-1,-1), 0.4, GRAY_MID),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]
tbl.setStyle(TableStyle(style_cmds))
return tbl
# ── Page callbacks ────────────────────────────────────────────────────────────
def cover_page(canvas_obj, doc):
canvas_obj.saveState()
w, h = A4
# Background gradient simulation with rectangles
canvas_obj.setFillColor(NAVY)
canvas_obj.rect(0, 0, w, h, fill=1, stroke=0)
# Top accent bar
canvas_obj.setFillColor(RED)
canvas_obj.rect(0, h-12*mm, w, 12*mm, fill=1, stroke=0)
# Bottom accent bar
canvas_obj.setFillColor(TEAL)
canvas_obj.rect(0, 0, w, 8*mm, fill=1, stroke=0)
# ECG waveform decoration (simplified)
canvas_obj.setStrokeColor(colors.HexColor("#E74C3C"))
canvas_obj.setLineWidth(2)
canvas_obj.setFillColor(colors.HexColor("#E74C3C"))
# Draw a simple ECG trace across the page
y_base = h * 0.42
pts = [
(10, y_base), (50, y_base), (60, y_base+5), (65, y_base-30),
(70, y_base+60), (75, y_base-30), (82, y_base+8), (90, y_base),
(130, y_base), (140, y_base+4), (148, y_base-12),
(155, y_base+4), (165, y_base), (210, y_base),
(220, y_base+4), (228, y_base-12), (235, y_base+4), (245, y_base),
(290, y_base), (300, y_base+5), (308, y_base-30),
(313, y_base+60), (318, y_base-30), (325, y_base+8), (333, y_base),
(375, y_base), (385, y_base+4), (393, y_base-12),
(400, y_base+4), (410, y_base), (455, y_base),
(465, y_base+5), (473, y_base-30), (478, y_base+60),
(483, y_base-30), (490, y_base+8), (500, y_base), (595, y_base),
]
p = canvas_obj.beginPath()
p.moveTo(pts[0][0], pts[0][1])
for x, y in pts[1:]:
p.lineTo(x, y)
canvas_obj.drawPath(p, stroke=1, fill=0)
canvas_obj.setStrokeColor(colors.HexColor("#2980B9"))
canvas_obj.setLineWidth(0.5)
canvas_obj.setFillColor(colors.HexColor("#2980B9"))
y_base2 = y_base - 50
pts2 = [
(10, y_base2), (50, y_base2), (56, y_base2+3), (60, y_base2-18),
(64, y_base2+36), (68, y_base2-18), (74, y_base2+5), (80, y_base2),
(120, y_base2), (126, y_base2+3), (130, y_base2-18),
(134, y_base2+36), (138, y_base2-18), (144, y_base2+5), (150, y_base2),
(595, y_base2),
]
p2 = canvas_obj.beginPath()
p2.moveTo(pts2[0][0], pts2[0][1])
for x, y in pts2[1:]:
p2.lineTo(x, y)
canvas_obj.drawPath(p2, stroke=1, fill=0)
canvas_obj.restoreState()
def normal_page(canvas_obj, doc):
canvas_obj.saveState()
w, h = A4
# Top bar
canvas_obj.setFillColor(NAVY)
canvas_obj.rect(0, h-10*mm, w, 10*mm, fill=1, stroke=0)
# Header text
canvas_obj.setFont("Helvetica-Bold", 8)
canvas_obj.setFillColor(WHITE)
canvas_obj.drawString(15*mm, h-6.5*mm, "ECG COMPLETE GUIDE: Disorders & Interpretation")
canvas_obj.setFont("Helvetica", 8)
canvas_obj.drawRightString(w-15*mm, h-6.5*mm, "Orris Medical Reference")
# Bottom bar
canvas_obj.setFillColor(NAVY)
canvas_obj.rect(0, 0, w, 8*mm, fill=1, stroke=0)
# Page number
canvas_obj.setFont("Helvetica", 8)
canvas_obj.setFillColor(WHITE)
canvas_obj.drawCentredString(w/2, 2.8*mm, f"Page {doc.page}")
# Left accent line
canvas_obj.setFillColor(RED)
canvas_obj.rect(0, 8*mm, 3*mm, h-18*mm, fill=1, stroke=0)
canvas_obj.restoreState()
# ── Build content ─────────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
rightMargin=18*mm, leftMargin=22*mm,
topMargin=18*mm, bottomMargin=16*mm,
title="ECG Complete Guide: Disorders & Interpretation",
author="Orris Medical Reference",
subject="ECG Interpretation for Clinical Practice",
)
story = []
# ═══════════════════════════════════════════════════════
# COVER PAGE (uses cover_page callback via first page)
# ═══════════════════════════════════════════════════════
story.append(Spacer(1, 45*mm))
cover_title = style("ct", fontName="Helvetica-Bold", fontSize=34,
textColor=WHITE, alignment=TA_CENTER, leading=40)
cover_sub = style("cs", fontName="Helvetica", fontSize=15,
textColor=colors.HexColor("#AED6F1"), alignment=TA_CENTER, leading=20)
cover_tag = style("ctag", fontName="Helvetica-Oblique", fontSize=11,
textColor=colors.HexColor("#A9DFBF"), alignment=TA_CENTER, leading=14)
story.append(Paragraph("ECG Complete Guide", cover_title))
story.append(sp(3))
story.append(Paragraph("Disorders & Interpretation", cover_title))
story.append(sp(8))
story.append(Paragraph("A Systematic Reference for Medical Students, Nurses & Clinicians", cover_sub))
story.append(sp(35))
story.append(Paragraph("Covering: Normal ECG · Arrhythmias · Conduction Blocks · Ischemia / MI", cover_tag))
story.append(Paragraph("Hypertrophy · Electrolyte Disorders · Drug Effects · Systematic Approach", cover_tag))
story.append(sp(10))
cover_byline = style("cby", fontName="Helvetica", fontSize=10,
textColor=colors.HexColor("#85C1E9"), alignment=TA_CENTER, leading=13)
story.append(Paragraph("Orris Medical Reference | 2026", cover_byline))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 1 – ECG Basics & Systematic Approach
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 1: ECG Basics & Systematic Approach", NAVY))
story.append(sp(3))
story.append(section_box("What is an ECG?", LIGHT_BLUE, NAVY))
story.append(sp(2))
story.append(Paragraph(
"An electrocardiogram (ECG/EKG) records the electrical activity of the heart "
"over time via electrodes placed on the skin. A standard 12-lead ECG captures "
"electrical vectors from 12 different angles, providing information about rhythm, "
"conduction, ischemia, hypertrophy, and metabolic abnormalities.", BODY_STYLE))
story.append(sp(3))
story.append(section_box("ECG Paper & Measurements", LIGHT_TEAL, TEAL))
story.append(sp(2))
measurements = [
["Parameter", "Small Box", "Large Box", "Standard Value"],
["Time (horizontal)", "0.04 sec", "0.20 sec", "Paper speed: 25 mm/s"],
["Voltage (vertical)", "0.1 mV", "0.5 mV", "Calibration: 1 mV = 10 mm"],
["PR Interval", "—", "—", "0.12 – 0.20 sec (3–5 small boxes)"],
["QRS Duration", "—", "—", "< 0.12 sec (< 3 small boxes)"],
["QT Interval", "—", "—", "< 0.44 sec (corrected QTc)"],
["P Wave", "—", "—", "< 0.12 sec, < 2.5 mm height"],
]
story.append(make_table(
measurements[0], measurements[1:],
[42*mm, 28*mm, 28*mm, 72*mm], TEAL))
story.append(sp(3))
story.append(section_box("12-Lead ECG: Lead Groups & Views", LIGHT_BLUE, NAVY))
story.append(sp(2))
lead_data = [
["Lead Group", "Leads", "Heart Region Viewed"],
["Inferior", "II, III, aVF", "Inferior wall (RCA territory)"],
["Lateral", "I, aVL, V5, V6", "Lateral wall (LCx territory)"],
["Anterior (Septal)", "V1, V2", "Interventricular septum"],
["Anterior (Anterior)", "V3, V4", "Anterior wall (LAD territory)"],
["Right-sided", "V1, V3R–V6R", "Right ventricle"],
["Posterior", "V7–V9 (or reciprocal V1–V3)", "Posterior wall"],
["Augmented Limb", "aVR", "Cavity / global ischemia detector"],
]
story.append(make_table(lead_data[0], lead_data[1:], [32*mm, 42*mm, 96*mm], NAVY))
story.append(sp(3))
story.append(section_box("The 7-Step Systematic Approach", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
steps = [
("<b>Step 1 – Rate:</b>", "Count R–R intervals. 300 ÷ number of large boxes between R waves. Normal: 60–100 bpm."),
("<b>Step 2 – Rhythm:</b>", "Regular or irregular? P before every QRS? QRS after every P?"),
("<b>Step 3 – P Wave:</b>", "Present, morphology, axis (upright in I & II = sinus). Duration & amplitude."),
("<b>Step 4 – PR Interval:</b>", "0.12–0.20 s. Short = WPW or junctional. Long = 1st degree block."),
("<b>Step 5 – QRS Complex:</b>", "Duration < 0.12 s. Check axis, bundle branch blocks, delta waves."),
("<b>Step 6 – ST Segment & T Wave:</b>", "Elevation or depression? T wave inversion, peaked, or biphasic?"),
("<b>Step 7 – QT Interval:</b>", "Correct with Bazett formula: QTc = QT / √RR. Normal QTc < 440 ms (men) / < 460 ms (women)."),
]
for label, desc in steps:
row_tbl = Table([[Paragraph(label, BOLD_BODY), Paragraph(desc, BODY_STYLE)]],
colWidths=[40*mm, 128*mm])
row_tbl.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
story.append(row_tbl)
story.append(sp(2))
story.append(info_box(
"⚡ REMEMBER: Always check: rate · rhythm · P wave · PR · QRS · ST-T · QTc",
YELLOW_BG, ORANGE))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 2 – Normal ECG
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 2: Normal ECG & Key Waveforms", TEAL))
story.append(sp(3))
story.append(section_box("Normal Sinus Rhythm Criteria", LIGHT_TEAL, TEAL))
story.append(sp(2))
nsr = [
"Rate 60–100 bpm",
"Regular rhythm (R–R intervals vary < 10%)",
"Upright P wave in leads I, II, aVF",
"Inverted P wave in aVR",
"Constant PR interval (0.12–0.20 s)",
"Every P followed by a QRS; every QRS preceded by a P",
"QRS duration < 0.12 s (narrow complex)",
"Normal axis: –30° to +90°",
]
for b in nsr:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("Normal Waveform Characteristics", LIGHT_BLUE, NAVY))
story.append(sp(2))
wave_data = [
["Wave / Segment", "Normal Features", "Clinical Significance"],
["P Wave", "< 0.12 s, < 2.5 mm, biphasic in V1", "Atrial depolarisation"],
["PR Segment", "Isoelectric, 0.12–0.20 s", "AV node conduction delay"],
["Q Wave", "< 0.04 s, < 25% of R height", "Septal or pathological (wide/deep)"],
["R Wave", "Progressive increase V1→V5", "Ventricular depolarisation"],
["S Wave", "Decreases V4→V6", "Late ventricular activation"],
["ST Segment", "Isoelectric (±0.5 mm in limb; ±1 mm in precordial)", "Repolarisation; elevation/depression = ischaemia"],
["T Wave", "Upright except aVR, V1 (±III); ≥ 1/8 but ≤ 2/3 of R", "Ventricular repolarisation"],
["U Wave", "Small positive deflection after T (best seen V2–V3)", "Often hypokalaemia; inverted = ischaemia"],
["QTc", "< 440 ms ♂, < 460 ms ♀", "Prolonged = risk of Torsades de Pointes"],
]
story.append(make_table(wave_data[0], wave_data[1:], [38*mm, 58*mm, 74*mm], NAVY))
story.append(sp(3))
story.append(section_box("Axis Interpretation", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
axis_data = [
["Axis", "Degrees", "Leads I & aVF", "Common Causes"],
["Normal", "−30° to +90°", "Both positive", "Normal"],
["Left Axis Deviation (LAD)", "−30° to −90°", "I +ve, aVF –ve", "LBBB, LAHB, inferior MI, LVH"],
["Right Axis Deviation (RAD)", "+90° to +180°", "I –ve, aVF +ve", "RBBB, RVH, lateral MI, PE"],
["Extreme / NW", "−90° to ±180°", "Both negative", "VT, severe emphysema, dextrocardia"],
]
story.append(make_table(axis_data[0], axis_data[1:], [38*mm, 24*mm, 36*mm, 72*mm], PURPLE))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 3 – Arrhythmias
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 3: Arrhythmias", RED))
story.append(sp(3))
# ── Sinus arrhythmias
story.append(section_box("3.1 Sinus Arrhythmias", LIGHT_BLUE, NAVY))
story.append(sp(2))
sinus_data = [
["Arrhythmia", "Rate", "Key ECG Feature", "Clinical Note"],
["Sinus Bradycardia", "< 60 bpm", "Normal P–QRS; slow rate", "Athletes, hypothyroidism, beta-blockers, vasovagal"],
["Sinus Tachycardia", "> 100 bpm", "Normal P–QRS; fast rate", "Pain, fever, PE, heart failure, anaemia, drugs"],
["Sinus Arrhythmia", "60–100 bpm", "Irregular R–R, varies with breathing", "Normal variant; prominent in young/athletes"],
["Sick Sinus Syndrome", "Variable", "Brady-tachy alternation, pauses > 2 s, SA block", "Requires pacemaker if symptomatic"],
]
story.append(make_table(sinus_data[0], sinus_data[1:], [38*mm, 20*mm, 58*mm, 54*mm], RED))
story.append(sp(3))
# ── Supraventricular
story.append(section_box("3.2 Supraventricular Arrhythmias", LIGHT_TEAL, TEAL))
story.append(sp(2))
sva_data = [
["Arrhythmia", "Rate", "Key ECG Features", "Notes"],
["PAC (Premature Atrial Complex)", "—", "Early P' wave, abnormal morphology, compensatory pause incomplete", "Benign; can trigger SVT/AF"],
["AVNRT (SVT)", "150–250 bpm", "Regular narrow QRS; P buried in / just after QRS (pseudo-r' in V1, pseudo-s in II)", "Most common paroxysmal SVT; vagal manoeuvres / adenosine"],
["AVRT (WPW-related SVT)", "150–300 bpm", "Narrow QRS (orthodromic) or wide (antidromic); pre-excitation during sinus = delta wave, short PR", "Avoid AV nodal agents in pre-excited AF"],
["Atrial Flutter", "Atrial 300 bpm; ventricular 150 bpm (2:1)", "Sawtooth flutter waves in II, III, aVF; regular ventricular response", "Ablation highly effective; anticoagulate"],
["Atrial Fibrillation (AF)", "Atrial 350–600 bpm; ventricular variable", "Irregularly irregular rhythm; absent P waves; fibrillatory baseline", "Most common sustained arrhythmia; stroke risk → CHA₂DS₂-VASc"],
["Multifocal AT (MAT)", "100–200 bpm", "Irregular; ≥ 3 distinct P wave morphologies; variable PR", "COPD, electrolyte disorders, elderly"],
["Junctional Rhythm", "40–60 bpm", "Absent or inverted P (before/during/after QRS); narrow QRS", "AV nodal escape; seen in inferior MI, digoxin toxicity"],
]
story.append(make_table(sva_data[0], sva_data[1:], [40*mm, 22*mm, 58*mm, 50*mm], TEAL))
story.append(sp(3))
# ── Ventricular arrhythmias
story.append(section_box("3.3 Ventricular Arrhythmias", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
va_data = [
["Arrhythmia", "Rate", "Key ECG Features", "Management"],
["PVC (Premature Ventricular Complex)", "—", "Wide bizarre QRS ≥ 0.12 s; full compensatory pause; no P before", "Treat underlying cause; frequent PVCs (> 10,000/day) can cause CMP"],
["Accelerated Idioventricular Rhythm (AIVR)", "40–120 bpm", "Wide QRS, regular, no P preceding; often post-MI reperfusion", "Usually benign & self-limiting"],
["Ventricular Tachycardia (VT)", "> 100 bpm (usually 130–250)", "Wide complex tachycardia (QRS ≥ 0.12 s); AV dissociation, fusion / capture beats", "If pulse: amiodarone/cardioversion. Pulseless: defib"],
["Ventricular Fibrillation (VF)", "Chaotic", "Chaotic undulations; no identifiable QRS; no pulse", "Immediate defibrillation + CPR (ACLS)"],
["Torsades de Pointes (TdP)", "200–250 bpm", "Polymorphic VT; QRS twists around isoelectric axis; preceded by long QTc", "Magnesium IV; remove QT-prolonging drugs; overdrive pacing"],
["Brugada Syndrome", "—", "Coved-type ST elevation V1–V2; RBBB pattern; can trigger sudden VF", "ICD implantation; avoid sodium channel blockers"],
]
story.append(make_table(va_data[0], va_data[1:], [42*mm, 22*mm, 60*mm, 46*mm], ORANGE))
story.append(sp(2))
story.append(info_box(
"⚠ VT vs SVT with aberrancy: Brugada criteria, Vereckei algorithm. "
"Key clues for VT: AV dissociation, fusion beats, concordance, extreme axis, QRS ≥ 0.16 s.",
colors.HexColor("#FDEDEC"), DARK_RED))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 4 – Conduction Blocks
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 4: Conduction Blocks", PURPLE))
story.append(sp(3))
story.append(section_box("4.1 AV Blocks", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
av_data = [
["Block", "PR Interval", "QRS Dropped?", "Rhythm", "Clinical Significance"],
["1st Degree AV Block", "> 0.20 s; constant", "No", "Regular", "Often benign; monitor; can be caused by vagal tone, inferior MI, drugs"],
["2nd Degree Mobitz I (Wenckebach)", "Progressive lengthening", "Yes – periodically", "Irregular grouped beating", "AV node dysfunction; usually benign; inferior MI; vagal"],
["2nd Degree Mobitz II", "Constant (normal or long)", "Yes – suddenly, without warning", "Regular except for dropped beats", "HIS/Purkinje disease; often progresses to complete heart block → pacemaker"],
["2:1 AV Block", "Constant", "Every alternate P dropped", "Regular at half atrial rate", "Cannot classify as Mobitz I or II without ≥ 3 P waves together; needs EP study"],
["3rd Degree (Complete) AV Block", "No relationship (AV dissociation)", "QRS unrelated to P waves; escape rhythm", "Atrial regular; ventricular regular but independent", "Emergency: junctional escape (narrow, 40–60) or ventricular escape (wide, < 40); requires pacing"],
]
story.append(make_table(av_data[0], av_data[1:], [38*mm, 30*mm, 26*mm, 26*mm, 50*mm], PURPLE))
story.append(sp(3))
story.append(section_box("4.2 Bundle Branch Blocks", LIGHT_BLUE, NAVY))
story.append(sp(2))
story.append(Paragraph("<b>Right Bundle Branch Block (RBBB)</b>", SUBSECTION_STYLE))
for b in [
"QRS ≥ 0.12 s (complete RBBB) or 0.10–0.12 s (incomplete RBBB)",
"rSR' ('M' pattern / rabbit ears) in V1–V2",
"Wide, slurred S wave in I, aVL, V5–V6",
"Secondary ST-T changes (ST depression + T inversion in V1–V3)",
"Causes: normal variant, PE, RVH, ASD, anterior MI, post-cardiac surgery",
]:
story.append(bullet(b))
story.append(sp(2))
story.append(Paragraph("<b>Left Bundle Branch Block (LBBB)</b>", SUBSECTION_STYLE))
for b in [
"QRS ≥ 0.12 s",
"Broad, notched R wave ('M' shape) in I, aVL, V5–V6 (no septal Q waves)",
"rS or QS pattern in V1–V3",
"Discordant ST-T changes (opposite to main QRS deflection)",
"New LBBB + chest pain = treat as STEMI equivalent (Sgarbossa criteria)",
"Causes: IHD, dilated CMP, hypertension, aortic valve disease",
]:
story.append(bullet(b))
story.append(sp(2))
story.append(Paragraph("<b>Fascicular Blocks (Hemiblocks)</b>", SUBSECTION_STYLE))
fascicular_data = [
["Block", "Axis", "Lead I", "Lead II/III", "Notes"],
["Left Anterior Hemiblock (LAHB)", "LAD (−30° to −90°)", "+ve (qR)", "rS", "Most common; narrow QRS; no right axis shift"],
["Left Posterior Hemiblock (LPHB)", "RAD (+90° to +120°)", "rS", "+ve (qR)", "Rare; must exclude other RAD causes"],
["Bifascicular Block", "LAD", "RBBB + LAHB pattern", "—", "RBBB + LAHB; risk of complete block"],
["Trifascicular Block", "Variable", "1st-degree block + bifascicular", "—", "High risk of complete heart block; consider pacing"],
]
story.append(make_table(fascicular_data[0], fascicular_data[1:], [40*mm, 24*mm, 24*mm, 24*mm, 58*mm], NAVY))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 5 – Ischaemia & MI
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 5: Ischaemia, Injury & Myocardial Infarction", DARK_RED))
story.append(sp(3))
story.append(section_box("5.1 Spectrum of Ischaemic ECG Changes", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
isch_data = [
["Stage", "ECG Findings", "Pathophysiology"],
["Hyper-Acute (minutes)", "Tall, peaked 'hyperacute' T waves; ST straightening", "Acute transmural ischaemia; earliest sign"],
["Acute Injury (minutes–hours)", "ST elevation ≥ 1 mm in ≥ 2 contiguous leads (≥ 2 mm in V1–V4)", "Transmural injury current; STEMI criteria"],
["Established Infarction (hours–days)", "Pathological Q waves (≥ 0.04 s, ≥ 25% of R); ST normalises", "Myocyte necrosis; Q wave = dead tissue"],
["Chronic / Resolved (weeks–months)", "Persistent Q waves; T wave normalisation (may invert)", "Scar tissue; Q waves often permanent"],
["Subendocardial Ischaemia (NSTEMI/UA)", "Horizontal/downsloping ST depression ≥ 0.5 mm; T inversion", "Partial thickness ischaemia; no Q waves typically"],
]
story.append(make_table(isch_data[0], isch_data[1:], [40*mm, 72*mm, 58*mm], ORANGE))
story.append(sp(3))
story.append(section_box("5.2 MI Localisation by Territory", LIGHT_BLUE, NAVY))
story.append(sp(2))
mi_data = [
["Territory", "Artery (Usual)", "Leads with ST Changes", "Reciprocal Changes"],
["Inferior MI", "RCA (85%) / LCx (15%)", "II, III, aVF", "I, aVL"],
["Anterior MI", "LAD (proximal)", "V1–V4 (or V1–V6)", "II, III, aVF (sometimes)"],
["Anteroseptal MI", "LAD (septal branches)", "V1–V3", "None specific"],
["Anterolateral MI", "LAD or LCx", "V1–V6, I, aVL", "II, III, aVF"],
["Lateral MI", "LCx / diagonal branch", "I, aVL, V5–V6", "V1–V3, II, III, aVF"],
["Posterior MI", "RCA / LCx", "V1–V3 ST depression; tall R wave", "ST elevation V7–V9 (posterior leads)"],
["Right Ventricular MI", "Proximal RCA", "V1 ST elevation; ST elevation V3R–V4R", "Usually with inferior MI (II, III, aVF)"],
["High Lateral / Apical", "Diagonal / OM branch", "I, aVL only (or none)", "II, III"],
]
story.append(make_table(mi_data[0], mi_data[1:], [36*mm, 36*mm, 50*mm, 48*mm], DARK_RED))
story.append(sp(3))
story.append(section_box("5.3 Sgarbossa Criteria (MI in LBBB)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
sgarbossa = [
("<b>Criterion 1 (5 pts):</b>", "ST elevation ≥ 1 mm concordant with QRS direction"),
("<b>Criterion 2 (3 pts):</b>", "ST depression ≥ 1 mm in V1, V2, or V3 (concordant negative)"),
("<b>Criterion 3 (2 pts):</b>", "ST elevation ≥ 5 mm discordant with QRS direction"),
]
for label, desc in sgarbossa:
row = Table([[Paragraph(label, BOLD_BODY), Paragraph(desc, BODY_STYLE)]],
colWidths=[40*mm, 128*mm])
row.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
story.append(row)
story.append(sp(1))
story.append(info_box("Score ≥ 3 = high specificity for AMI. Modified Sgarbossa: use proportional criterion (ST/S ratio ≥ 0.25) instead of absolute 5 mm rule.", YELLOW_BG, ORANGE))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 6 – Hypertrophy & Enlargement
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 6: Chamber Hypertrophy & Enlargement", TEAL))
story.append(sp(3))
story.append(section_box("6.1 Ventricular Hypertrophy", LIGHT_TEAL, TEAL))
story.append(sp(2))
vh_data = [
["Criteria", "LVH", "RVH"],
["Voltage (main)", "Sokolow-Lyon: S(V1) + R(V5/V6) ≥ 35 mm\nCornell: R(aVL) ≥ 11 mm\nR(aVL) + S(V3) > 28 mm (M) / > 20 mm (F)", "R > S in V1; R in V1 ≥ 7 mm\nS in V5/V6 > 7 mm\nR:S ratio in V1 ≥ 1"],
["Axis", "LAD or normal", "RAD > +90°"],
["ST-T Changes", "Strain pattern: ST depression + T inversion in I, aVL, V5–V6", "Strain: ST depression + T inversion in V1–V4"],
["P Wave", "Often normal; may have P mitrale if raised LVEDP", "May show P pulmonale (tall P ≥ 2.5 mm)"],
["Causes", "HTN, aortic stenosis, HCM, AR", "Cor pulmonale, PE, pulmonary HTN, PS"],
]
tbl = Table(
[[Paragraph(vh_data[0][0], TABLE_HEADER), Paragraph(vh_data[0][1], TABLE_HEADER), Paragraph(vh_data[0][2], TABLE_HEADER)]] +
[[Paragraph(r[0], TABLE_CELL), Paragraph(r[1], TABLE_CELL), Paragraph(r[2], TABLE_CELL)] for r in vh_data[1:]],
colWidths=[30*mm, 72*mm, 68*mm]
)
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TEAL),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_TEAL]),
("GRID", (0,0), (-1,-1), 0.4, GRAY_MID),
("TOPPADDING", (0,0), (-1,-1), 5), ("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6), ("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(tbl)
story.append(sp(3))
story.append(section_box("6.2 Atrial Enlargement", LIGHT_BLUE, NAVY))
story.append(sp(2))
atrial_data = [
["Feature", "Left Atrial Enlargement (LAE)", "Right Atrial Enlargement (RAE)"],
["P Wave Duration", "> 0.12 s (P mitrale)", "Normal or short"],
["P Wave Height", "Normal (or terminal negative portion in V1 > 1 mm × 1 mm box)", "≥ 2.5 mm in II, III, or aVF (P pulmonale)"],
["P Wave Morphology", "Bifid/notched P in II; biphasic in V1 with broad negative terminal", "Peaked, tall, narrow P"],
["Common Causes", "Mitral stenosis/regurgitation, HTN, LHF", "COPD, pulmonary HTN, tricuspid disease, RHF"],
]
story.append(make_table(atrial_data[0], atrial_data[1:], [34*mm, 68*mm, 68*mm], NAVY))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 7 – Electrolyte & Metabolic Disorders
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 7: Electrolyte & Metabolic Disorders", colors.HexColor("#1A5276")))
story.append(sp(3))
story.append(section_box("7.1 Potassium Disorders", LIGHT_BLUE, NAVY))
story.append(sp(2))
k_data = [
["K+ Level", "ECG Changes", "Clinical Sequence"],
["Hypokalaemia\n< 3.5 mEq/L", "U wave prominence (V2–V3); T–U fusion; T wave flattening; QTU prolongation; ST depression; PVCs; TdP", "K 3–3.5: U waves → K 2.5–3: flat T, U > T → K < 2.5: T/U fusion, ST depression → K < 2: VF risk"],
["Hyperkalaemia\n> 5.5 mEq/L", "Peaked narrow T waves → PR prolongation → P wave flattening/disappearance → QRS widening → sine wave pattern → VF/asystole", "K 5.5–6.5: tall T → K 6.5–7.5: PR long, P flat → K > 7.5: wide QRS, sine wave → K > 9: cardiac arrest"],
]
tbl_k = Table(
[[Paragraph(k_data[0][0], TABLE_HEADER), Paragraph(k_data[0][1], TABLE_HEADER), Paragraph(k_data[0][2], TABLE_HEADER)]] +
[[Paragraph(r[0], TABLE_CELL), Paragraph(r[1], TABLE_CELL), Paragraph(r[2], TABLE_CELL)] for r in k_data[1:]],
colWidths=[30*mm, 72*mm, 68*mm]
)
tbl_k.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_BLUE]),
("GRID", (0,0), (-1,-1), 0.4, GRAY_MID),
("TOPPADDING", (0,0), (-1,-1), 6), ("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 6), ("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(tbl_k)
story.append(sp(3))
story.append(section_box("7.2 Other Electrolyte & Metabolic Disorders", LIGHT_TEAL, TEAL))
story.append(sp(2))
met_data = [
["Disorder", "ECG Findings", "Memory Aid"],
["Hypercalcaemia", "Short QT interval; short ST segment; prolonged PR; Osborn wave (rarely)", "Ca UP → QT DOWN"],
["Hypocalcaemia", "Prolonged QT (prolonged ST segment); T wave normal or inverted", "Ca DOWN → QT UP"],
["Hypomagnesaemia", "Prolonged QTc; T wave changes; TdP risk; similar to hypokalaemia", "Often coexists with hypokalaemia"],
["Hypothyroidism", "Sinus bradycardia; low voltage; prolonged QTc; T wave flattening/inversion; pericardial effusion pattern", "Think slow + low"],
["Hyperthyroidism", "Sinus tachycardia; AF; shortened QTc; high voltage", "Think fast + irregular"],
["Hypothermia", "Sinus bradycardia; Osborn (J) wave at QRS/ST junction; AF; prolonged intervals; VF risk", "Osborn waves pathognomonic"],
["Digoxin Effect", "Sagging ('reverse tick' or 'Salvador Dali moustache') ST depression; T wave inversion; shortened QT; PR prolongation", "ST scooping = digoxin effect (not toxicity)"],
["Digoxin Toxicity", "Any arrhythmia: PAT with block, regularised AF, VT/VF, junctional rhythms, PVCs (bigeminy)", "Classic: PAT with 2:1 block"],
]
story.append(make_table(met_data[0], met_data[1:], [36*mm, 80*mm, 54*mm], TEAL))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 8 – Special Conditions
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 8: Special & Specific Conditions", colors.HexColor("#117A65")))
story.append(sp(3))
story.append(section_box("8.1 Pericarditis", LIGHT_TEAL, TEAL))
story.append(sp(2))
for b in [
"<b>Saddle-shaped (concave up) diffuse ST elevation</b> in most leads except aVR and V1 (which show ST depression)",
"<b>PR depression</b> – most specific finding; best seen in II and aVL",
"<b>No reciprocal changes</b> (unlike MI) – a key differentiator",
"<b>T wave inversion</b> occurs later (after ST normalises; Stage III)",
"<b>Stages:</b> I (ST elevation, PR depression) → II (normalisation) → III (T inversion) → IV (normalisation)",
"<b>Spodick's sign:</b> downsloping TP segment (best in II); sensitive early finding",
]:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("8.2 Pulmonary Embolism (PE)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
for b in [
"<b>Sinus tachycardia</b> – most common ECG finding in PE (present in ~40%)",
"<b>S1Q3T3 pattern:</b> S wave in I + Q wave in III + T inversion in III (present in ~20% – not sensitive or specific alone)",
"<b>Right heart strain:</b> new RBBB (complete or incomplete), RAD, right axis shift",
"<b>T wave inversions V1–V4</b> – suggests RV strain; can mimic anterior ischaemia",
"<b>AF or atrial flutter</b> may precipitate",
"<b>Low voltage / sinus tachycardia alone</b> – should prompt CT-PA if clinically suspicious",
]:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("8.3 Wolff-Parkinson-White (WPW) Syndrome", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
for b in [
"<b>Short PR interval</b> (< 0.12 s) – accessory pathway bypasses AV node",
"<b>Delta wave</b> – slurred upstroke of QRS; initial slow conduction via accessory pathway",
"<b>Wide QRS</b> (> 0.12 s) due to delta wave",
"<b>Secondary ST-T changes</b> (discordant to QRS)",
"<b>Risk:</b> AF with rapid ventricular response → VF if accessory pathway has short refractory period",
"<b>AVOID:</b> Adenosine, digoxin, verapamil, beta-blockers in pre-excited AF (can accelerate conduction)",
]:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("8.4 Long QT Syndromes", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
lqt_data = [
["Type", "Gene", "Trigger", "T Wave Morphology"],
["LQT1 (KCNQ1)", "IKs loss of function", "Exercise / emotion", "Broad-based T wave"],
["LQT2 (KCNH2 / hERG)", "IKr loss of function", "Sudden sound / auditory startle", "Notched / bifid T wave"],
["LQT3 (SCN5A)", "INa gain of function", "Sleep / rest / bradycardia", "Long flat ST, late T wave"],
["Acquired (drugs, electrolytes)", "Multiple channels", "Drug initiation, hypokalaemia, hypomagnesaemia", "Variable QT prolongation"],
]
story.append(make_table(lqt_data[0], lqt_data[1:], [38*mm, 36*mm, 44*mm, 52*mm], ORANGE))
story.append(sp(2))
story.append(info_box(
"Common QT-prolonging drugs: antiarrhythmics (sotalol, amiodarone, quinidine), "
"antibiotics (azithromycin, fluoroquinolones), antipsychotics (haloperidol, quetiapine), "
"antidepressants (tricyclics, citalopram), antiemetics (ondansetron, metoclopramide).",
YELLOW_BG, ORANGE))
story.append(sp(3))
story.append(section_box("8.5 Early Repolarisation vs. STEMI", LIGHT_GREEN, GREEN))
story.append(sp(2))
er_data = [
["Feature", "Early Repolarisation", "STEMI"],
["ST Morphology", "Concave (smiley face) upward", "Convex (frowning face) or flat upward"],
["Distribution", "Inferior and lateral leads; often widespread", "Localised to specific territory"],
["Reciprocal Changes", "Absent", "Present (key differentiator)"],
["J-Point Notching", "Characteristic notch or slur at J-point", "Usually absent"],
["Evolution", "Stable; no dynamic change", "Dynamic: evolving over time"],
["ST:T ratio (V6)", "< 0.25", "≥ 0.25 suggests STEMI/pericarditis"],
["Clinical Context", "Young, athletic, asymptomatic (usually)", "Chest pain, troponin rise, symptoms"],
]
story.append(make_table(er_data[0], er_data[1:], [40*mm, 65*mm, 65*mm], GREEN))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 9 – Quick Reference & Differentials
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 9: Quick Reference & Clinical Differentials", NAVY))
story.append(sp(3))
story.append(section_box("ST Elevation Differential Diagnosis (STEMI Mimics)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
stemi_dd = [
["Cause", "Distinguishing ECG Feature", "Clinical Clue"],
["STEMI", "Convex ST elevation, reciprocal changes, evolution, Q waves", "Chest pain, troponin +ve"],
["Pericarditis", "Diffuse concave ST elevation, PR depression, no reciprocals", "Pleuritic pain, friction rub, viral prodrome"],
["Early Repolarisation", "Concave ST, J-point notch, stable, young", "Asymptomatic, athlete"],
["LBBB (new)", "Sgarbossa criteria; convex ST in concordant leads", "May need primary PCI if new LBBB + chest pain"],
["Brugada Pattern", "Coved ST in V1–V2 only, RBBB morphology", "Family history of sudden death, fever precipitates"],
["LVH Strain", "V5–V6 ST elevation possible; high voltage", "Hypertension, no acute symptoms"],
["Vasospasm / Prinzmetal", "Transient ST elevation, resolves with nitrates", "Occurs at rest, often at night"],
["Hyperkalaemia", "Wide QRS, peaked T, sine wave; may mimic STEMI", "Renal failure, peaked T waves universally"],
["Takotsubo (Stress) CMP", "Initial: ST elevation V1–V4; later: diffuse T inversion, QTc prolongation", "Emotional stress, post-menopausal women, apical ballooning"],
]
story.append(make_table(stemi_dd[0], stemi_dd[1:], [36*mm, 66*mm, 68*mm], ORANGE))
story.append(sp(3))
story.append(section_box("Wide Complex Tachycardia (WCT) Differential", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
wct_data = [
["Criterion", "Favours VT", "Favours SVT with Aberrancy"],
["AV Dissociation", "Present (P waves unrelated to QRS)", "Absent"],
["Fusion Beats", "Present", "Absent"],
["Capture Beats", "Present", "Absent"],
["QRS Duration", "> 0.16 s", "Usually < 0.14 s"],
["Axis", "Extreme NW axis (−90° to ±180°)", "LAD possible (LBBB morphology)"],
["Onset", "Abrupt; no initiating PAC", "Often initiated by PAC; RP < PR"],
["V Lead Concordance", "All positive or all negative V1–V6", "Mixed"],
["RS Nadir to S", "> 100 ms in any V lead (Brugada sign)", "< 100 ms"],
["Prior ECG", "Sinus ECG different from WCT morphology", "WCT matches pre-existing BBB"],
["History", "Structural heart disease strongly favours VT", "No structural heart disease"],
]
story.append(make_table(wct_data[0], wct_data[1:], [40*mm, 70*mm, 60*mm], PURPLE))
story.append(sp(3))
story.append(section_box("Common Drug Effects on ECG", LIGHT_TEAL, TEAL))
story.append(sp(2))
drug_data = [
["Drug / Class", "ECG Effects", "Key Concern"],
["Digoxin (therapeutic)", "Scooping ST depression ('reverse tick'); short QT; PR prolongation; T inversion", "Toxicity: any arrhythmia; PAT with block classic"],
["Beta-Blockers", "Sinus bradycardia; PR prolongation; AV block", "Overdose: severe bradycardia, hypotension"],
["Calcium Channel Blockers (non-DHP)", "Sinus bradycardia; PR prolongation; AV block (verapamil > diltiazem)", "Overdose: fatal bradycardia; may cause AF"],
["Tricyclic Antidepressants (TCA)", "Wide QRS; prolonged QTc; right axis; deep S in I; tall R in aVR", "R:S ratio in aVR > 0.7 predicts seizure/arrhythmia"],
["Amiodarone", "Sinus bradycardia; prolonged PR, QRS, QTc; T wave changes; corneal microdeposits", "Pro-arrhythmic despite anti-arrhythmic classification"],
["Adenosine", "Transient AV block; brief asystole (1–3 s); sinus bradycardia", "Diagnostic/therapeutic in SVT; CI in WPW pre-excited AF"],
["Cocaine", "Sinus tachycardia; STEMI (coronary spasm); prolonged QTc; VT/VF", "Cocaine + beta-blocker = unopposed alpha → dangerous"],
]
story.append(make_table(drug_data[0], drug_data[1:], [38*mm, 72*mm, 60*mm], TEAL))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CHAPTER 10 – Summary Cheatsheet
# ═══════════════════════════════════════════════════════
story.append(chapter_header("Chapter 10: Rapid ECG Cheatsheet", RED))
story.append(sp(3))
story.append(section_box("One-Line Summaries – High-Yield for Exams & Clinics", LIGHT_BLUE, NAVY))
story.append(sp(2))
cheat_data = [
["Finding", "Diagnosis / Condition"],
["Irregularly irregular + no P waves", "Atrial Fibrillation"],
["Sawtooth waves at 300 bpm + regular ventricular response", "Atrial Flutter (2:1 block → 150 bpm)"],
["P buried in QRS or pseudo-r' V1 + pseudo-s II (narrow, fast)", "AVNRT (commonest SVT)"],
["Short PR + delta wave + wide QRS", "WPW (pre-excitation)"],
["Wide QRS > 0.12 s + rSR' in V1 + wide S in I, V6", "Right Bundle Branch Block (RBBB)"],
["Wide QRS + broad notched R in I/V6 + no septal Q waves", "Left Bundle Branch Block (LBBB)"],
["ST elevation + concave + PR depression + no reciprocals", "Pericarditis"],
["ST elevation + convex + reciprocal changes + Q waves", "STEMI"],
["Horizontal ST depression + T inversion (no Q waves)", "NSTEMI / Subendocardial ischaemia"],
["Progressive PR lengthening → dropped QRS (grouped beating)", "2nd degree AV block – Mobitz I (Wenckebach)"],
["Constant PR + sudden dropped QRS (no warning)", "2nd degree AV block – Mobitz II"],
["P waves unrelated to QRS + escape rhythm", "3rd degree (complete) AV block"],
["Peaked T → wide QRS → sine wave + hyperkalaemia history", "Hyperkalaemia"],
["Prominent U waves + flat T (U > T)", "Hypokalaemia"],
["Short QT interval", "Hypercalcaemia (or congenital short QT syndrome)"],
["Prolonged QT + polymorphic VT twisting axis", "Torsades de Pointes (Long QT)"],
["Coved ST elevation V1–V2 + RBBB morphology", "Brugada Syndrome"],
["S1Q3T3 + tachycardia + right heart strain", "Pulmonary Embolism (PE)"],
["Osborn (J) wave at J point + bradycardia + prolonged intervals", "Hypothermia"],
["Sagging ST ('reverse tick') + short QT + PR prolongation", "Digoxin Effect (therapeutic)"],
["Sokolow-Lyon voltage ≥ 35 mm + strain pattern", "Left Ventricular Hypertrophy (LVH)"],
["R > S in V1 + RAD + RV strain pattern", "Right Ventricular Hypertrophy (RVH)"],
["Diffuse T wave inversion + QTc prolongation + apical ballooning", "Takotsubo (Stress) Cardiomyopathy"],
["Delta + wide QRS + AF with rapid irregular wide QRS", "WPW + AF → risk of VF"],
["Wide complex tachycardia + AV dissociation + fusion beats", "Ventricular Tachycardia (VT) – proven"],
]
story.append(make_table(cheat_data[0], cheat_data[1:],
[100*mm, 70*mm], DARK_RED, colors.HexColor("#FDEDEC")))
story.append(sp(3))
story.append(info_box(
"📌 GOLDEN RULE: In any wide complex tachycardia with haemodynamic instability → "
"treat as VT and cardiovert. Never delay treatment to achieve a diagnosis.",
colors.HexColor("#FDEDEC"), DARK_RED))
story.append(sp(4))
story.append(info_box(
"📚 Recommended Reading:\n"
"• The ECG Made Easy – John Hampton (beginner)\n"
"• The ECG in Practice – John Hampton (intermediate)\n"
"• 150 ECG Problems – Hampton (case-based practice)\n"
"• Marriott's Practical Electrocardiography (advanced)\n"
"• LITFL ECG Library – litfl.com/ecg-library (free online, disorder-by-disorder)",
LIGHT_TEAL, TEAL))
# ── Final page ──
story.append(sp(6))
story.append(hr(NAVY, 1))
footer_style = style("footer", fontName="Helvetica-Oblique", fontSize=8.5,
textColor=colors.HexColor("#555555"), alignment=TA_CENTER, leading=12)
story.append(Paragraph(
"This guide is intended as an educational reference only. "
"Always correlate ECG findings with clinical history, physical examination, and other investigations. "
"Orris Medical Reference | ECG Complete Guide | 2026",
footer_style))
# ── Build with page callbacks ──────────────────────────────────────────────
doc.build(story,
onFirstPage=cover_page,
onLaterPages=normal_page)
print(f"PDF created: {OUTPUT_PATH}")
print(f"File size: {os.path.getsize(OUTPUT_PATH):,} bytes")
if __name__ == "__main__":
build_pdf()
cd /tmp/workspace/ecg-guide && python create_ecg_pdf.py
ECG Complete Guide
PDF Document · PDF
| Chapter | Content |
|---|---|
| 1 - ECG Basics | Paper measurements, 12-lead views, 7-step systematic approach |
| 2 - Normal ECG | NSR criteria, waveform characteristics, axis interpretation |
| 3 - Arrhythmias | Sinus, supraventricular (AF, flutter, SVT, WPW), ventricular (VT, VF, TdP, Brugada) |
| 4 - Conduction Blocks | AV blocks (1st/2nd/3rd degree), RBBB, LBBB, hemiblocks, trifascicular |
| 5 - Ischaemia & MI | Hyper-acute to chronic stages, MI localisation by territory, Sgarbossa criteria |
| 6 - Hypertrophy | LVH, RVH, LAE, RAE with voltage criteria and strain patterns |
| 7 - Electrolytes & Metabolic | Hypo/hyperkalaemia, calcium, magnesium, thyroid, hypothermia, digoxin |
| 8 - Special Conditions | Pericarditis, PE, WPW, Long QT syndromes, early repolarisation vs STEMI |
| 9 - Quick Reference | STEMI mimics, WCT differential (VT vs SVT), drug effects on ECG |
| 10 - Cheatsheet | 25 one-line high-yield summaries for exams and clinical practice |
Can u add the required ecg pattern for the above pdf
~/ecg-guide/generate_ecg_patterns.py
"""
Generate ECG waveform pattern images for all major disorders.
Each pattern is drawn using matplotlib with a proper ECG grid background.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import os
OUT_DIR = "/tmp/workspace/ecg-guide/ecg_images"
os.makedirs(OUT_DIR, exist_ok=True)
# ── ECG grid helper ────────────────────────────────────────────────────────────
def ecg_grid(ax, xlim, ylim=(-1.5, 2.5), title="", label_color="#C0392B"):
ax.set_facecolor("#FFFDF0")
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
# Minor grid (1 mm = 0.04s / 0.1mV)
minor_x = np.arange(xlim[0], xlim[1]+0.001, 0.04)
minor_y = np.arange(ylim[0], ylim[1]+0.001, 0.1)
for x in minor_x:
ax.axvline(x, color="#FADBD8", linewidth=0.3, zorder=0)
for y in minor_y:
ax.axhline(y, color="#FADBD8", linewidth=0.3, zorder=0)
# Major grid (5 mm = 0.20s / 0.5mV)
major_x = np.arange(xlim[0], xlim[1]+0.001, 0.20)
major_y = np.arange(ylim[0], ylim[1]+0.001, 0.5)
for x in major_x:
ax.axvline(x, color="#E59866", linewidth=0.7, zorder=0)
for y in major_y:
ax.axhline(y, color="#E59866", linewidth=0.7, zorder=0)
ax.axhline(0, color="#555555", linewidth=0.6, zorder=1)
ax.set_xlabel("Time (s)", fontsize=7, color="#555")
ax.set_ylabel("mV", fontsize=7, color="#555")
ax.tick_params(labelsize=6)
if title:
ax.set_title(title, fontsize=9, fontweight="bold", color=label_color, pad=4)
# ── Single PQRST beat builder ──────────────────────────────────────────────────
def pqrst(t_start=0.0,
p_amp=0.15, p_dur=0.08,
pr=0.16,
q_amp=-0.1, q_dur=0.02,
r_amp=1.0, r_dur=0.04,
s_amp=-0.2, s_dur=0.02,
st_len=0.10, st_elev=0.0,
t_amp=0.35, t_dur=0.16,
tp_len=0.20,
delta=False, delta_amp=0.3, delta_dur=0.06,
lbbb=False, rbbb=False):
"""Return (time_array, voltage_array) for one PQRST complex."""
t, v = [], []
def gauss(center, amp, sigma, n=60):
ts = np.linspace(center - 3*sigma, center + 3*sigma, n)
vs = amp * np.exp(-0.5*((ts - center)/sigma)**2)
return ts, vs
def ramp(t0, t1, v0, v1, n=20):
return np.linspace(t0, t1, n), np.linspace(v0, v1, n)
# Baseline before P
t.append([t_start]); v.append([0.0])
# P wave (Gaussian)
p_center = t_start + p_dur/2 + 0.02
pt, pv = gauss(p_center, p_amp, p_dur/4)
t.append(pt); v.append(pv)
# PR segment
pr_end = t_start + pr
t.append([p_center + p_dur/2 + 0.01, pr_end]); v.append([0.0, 0.0])
# Optional delta wave (WPW)
if delta:
dt, dv = ramp(pr_end, pr_end + delta_dur, 0.0, delta_amp)
t.append(dt); v.append(dv)
qrs_start = pr_end + delta_dur
else:
qrs_start = pr_end
if lbbb:
# Broad notched R in I/V6: ramp up, notch, ramp up again, then S
w = 0.14
bt, bv = np.linspace(qrs_start, qrs_start+w, 80), np.array([0]*5 + list(np.linspace(0,0.6,20)) + list(np.linspace(0.6,0.4,10)) + list(np.linspace(0.4,1.0,20)) + list(np.linspace(1.0,0.4,15)) + list(np.linspace(0.4,0,10)))
if len(bt) > len(bv): bt = bt[:len(bv)]
if len(bv) > len(bt): bv = bv[:len(bt)]
t.append(bt); v.append(bv)
end_qrs = qrs_start + w
elif rbbb:
# rSR' pattern: small r, S, tall R'
w = 0.12
bt = np.linspace(qrs_start, qrs_start+w, 80)
bv = np.array([0]*5 + list(np.linspace(0,0.3,10)) + list(np.linspace(0.3,-0.15,10)) + list(np.linspace(-0.15,0,8)) + list(np.linspace(0,0.9,15)) + list(np.linspace(0.9,0.5,12)) + list(np.linspace(0.5,0,10)) + [0]*10)
if len(bt) > len(bv): bt = bt[:len(bv)]
if len(bv) > len(bt): bv = bv[:len(bt)]
t.append(bt); v.append(bv)
end_qrs = qrs_start + w
else:
# Q
qt, qv = ramp(qrs_start, qrs_start + q_dur, 0.0, q_amp)
t.append(qt); v.append(qv)
# R upstroke
rt1, rv1 = ramp(qrs_start + q_dur, qrs_start + q_dur + r_dur, q_amp, r_amp)
t.append(rt1); v.append(rv1)
# R downstroke to S
rt2, rv2 = ramp(qrs_start + q_dur + r_dur, qrs_start + q_dur + r_dur + r_dur, r_amp, s_amp)
t.append(rt2); v.append(rv2)
# S upstroke
st0, sv0 = ramp(qrs_start + q_dur + 2*r_dur, qrs_start + q_dur + 2*r_dur + s_dur, s_amp, 0.0)
t.append(st0); v.append(sv0)
end_qrs = qrs_start + q_dur + 2*r_dur + s_dur
# ST segment
st_end = end_qrs + st_len
st_pts = np.linspace(end_qrs, st_end, 20)
sv_pts = np.linspace(st_elev, st_elev, 20)
t.append(st_pts); v.append(sv_pts)
# T wave
t_center = st_end + t_dur/2
tt, tv = gauss(t_center, t_amp + st_elev*0.5, t_dur/4)
t.append(tt); v.append(tv)
# TP segment
tp_end = t_center + t_dur/2 + tp_len
t.append([t_center + t_dur/2 + 0.01, tp_end])
v.append([0.0, 0.0])
t_all = np.concatenate(t)
v_all = np.concatenate(v)
# Sort by time to avoid jumps
idx = np.argsort(t_all)
return t_all[idx], v_all[idx]
def beat_duration(pr, q_dur, r_dur, s_dur, st_len, t_dur, tp_len, delta_dur=0, extra=0):
return pr + q_dur + 2*r_dur + s_dur + st_len + t_dur + tp_len + delta_dur + extra
# ── Save helper ───────────────────────────────────────────────────────────────
def save_fig(fig, name):
path = os.path.join(OUT_DIR, f"{name}.png")
fig.savefig(path, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
plt.close(fig)
print(f" Saved: {name}.png")
return path
# ══════════════════════════════════════════════════════════════════════════════
# 1. NORMAL SINUS RHYTHM
# ══════════════════════════════════════════════════════════════════════════════
def make_nsr():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0
all_t, all_v = [], []
for _ in range(5):
t, v = pqrst(t_start=x, tp_len=0.28)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Normal Sinus Rhythm (HR ~72 bpm)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
# Annotations
ax.annotate("P", (0.06, 0.18), fontsize=7, color="#1A5276", fontweight="bold")
ax.annotate("Q", (0.18, -0.16), fontsize=7, color="#922B21")
ax.annotate("R", (0.20, 1.05), fontsize=7, color="#922B21", fontweight="bold")
ax.annotate("S", (0.24, -0.26), fontsize=7, color="#922B21")
ax.annotate("T", (0.36, 0.40), fontsize=7, color="#117A65", fontweight="bold")
ax.annotate("U", (0.50, 0.10), fontsize=7, color="#7D6608")
save_fig(fig, "01_normal_sinus")
# ══════════════════════════════════════════════════════════════════════════════
# 2. SINUS BRADYCARDIA
# ══════════════════════════════════════════════════════════════════════════════
def make_sinus_brady():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0
all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, tp_len=0.72) # long TP = slow rate
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Sinus Bradycardia (HR ~45 bpm)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Regular P before each QRS\nLong R-R interval", (0.5, 1.3),
fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "02_sinus_bradycardia")
# ══════════════════════════════════════════════════════════════════════════════
# 3. SINUS TACHYCARDIA
# ══════════════════════════════════════════════════════════════════════════════
def make_sinus_tachy():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0
all_t, all_v = [], []
for _ in range(7):
t, v = pqrst(t_start=x, tp_len=0.06)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.02), title="Sinus Tachycardia (HR ~120 bpm)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Short R-R intervals\nNormal P morphology", (0.2, 1.3),
fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "03_sinus_tachycardia")
# ══════════════════════════════════════════════════════════════════════════════
# 4. ATRIAL FIBRILLATION
# ══════════════════════════════════════════════════════════════════════════════
def make_af():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
np.random.seed(42)
# Fibrillatory baseline
t_base = np.linspace(0, 3.5, 3500)
v_base = 0.07 * np.sin(2*np.pi*8*t_base) * np.sin(2*np.pi*0.7*t_base) + \
0.04 * np.random.randn(len(t_base))
# Irregular QRS complexes (narrow)
rr_times = [0.10, 0.58, 0.93, 1.38, 1.62, 2.14, 2.48, 2.85, 3.10, 3.40]
rr_times += [0.0 + np.random.uniform(0.0, 0.05) for _ in range(len(rr_times))]
rr_times = sorted(set([round(r, 3) for r in rr_times if r < 3.5]))
for rr in rr_times:
idx_s = int(rr * 1000)
idx_e = min(idx_s + 80, len(v_base))
qrs = np.array([0,0,0,-0.08,0.8,-0.18,0,0.20,-0.08,0,0,0,0,0,0,0,0])
for i, q in enumerate(qrs):
if idx_s+i < idx_e:
v_base[idx_s+i] += q
ecg_grid(ax, (0, 3.5), ylim=(-0.8, 1.3), title="Atrial Fibrillation (irregularly irregular, no P waves)")
ax.plot(t_base, v_base, color="#1A2E4A", linewidth=1.0)
ax.annotate("Fibrillatory\nbaseline (no P)", (0.3, 0.5),
fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(0.6, 0.9),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Irregular\nR-R intervals", (2.3, 0.7),
fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "04_atrial_fibrillation")
# ══════════════════════════════════════════════════════════════════════════════
# 5. ATRIAL FLUTTER
# ══════════════════════════════════════════════════════════════════════════════
def make_flutter():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
t = np.linspace(0, 3.5, 3500)
# Sawtooth flutter waves at 300 bpm (0.20s cycle)
v = -0.20 * (2*(t % 0.20)/0.20 - 1) # sawtooth
# Every 2nd flutter wave: add QRS
qrs_times = [0.40, 0.80, 1.20, 1.60, 2.00, 2.40, 2.80, 3.20]
for qt in qrs_times:
i = int(qt * 1000)
qrs = [0,0,-0.08, 0.9,-0.25,0.0, 0.25, 0.10, 0, 0, 0, 0]
for j, q in enumerate(qrs):
if i+j < len(v):
v[i+j] += q
ecg_grid(ax, (0, 3.5), ylim=(-0.7, 1.2), title="Atrial Flutter (sawtooth waves ~300 bpm, 2:1 block → ventricular ~150 bpm)")
ax.plot(t, v, color="#1A2E4A", linewidth=1.2)
ax.annotate("Sawtooth\nflutter waves", (0.55, -0.4),
fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(1.0, -0.75),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Regular QRS\n(2:1 ratio)", (2.05, 0.7),
fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "05_atrial_flutter")
# ══════════════════════════════════════════════════════════════════════════════
# 6. AVNRT (SVT)
# ══════════════════════════════════════════════════════════════════════════════
def make_avnrt():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0
all_t, all_v = [], []
for _ in range(8):
# P wave buried in/just after QRS: p_amp very small, very close to QRS
t, v = pqrst(t_start=x, p_amp=0.0, pr=0.10, tp_len=0.06, r_amp=0.9,
st_elev=0.0, t_amp=0.25)
# Add pseudo-r' after QRS
qrs_peak = x + 0.10 + 0.04
idx = np.argmin(np.abs(t - (qrs_peak + 0.03)))
v[idx] += 0.12
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()), ylim=(-0.8, 1.4), title="AVNRT / SVT (narrow QRS ~180 bpm, P buried in QRS, pseudo-r' in V1)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.3)
ax.annotate("No visible P wave\n(buried in QRS)", (0.4, 0.9),
fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Pseudo-r'\n(retrograde P)", (0.68, 0.5),
fontsize=7, color="#117A65",
arrowprops=dict(arrowstyle="->", color="#117A65"),
xytext=(0.9, 0.85),
bbox=dict(boxstyle="round,pad=0.3", fc="#D1F2EB", ec="#148F77", lw=0.8))
save_fig(fig, "06_avnrt")
# ══════════════════════════════════════════════════════════════════════════════
# 7. WPW / PRE-EXCITATION
# ══════════════════════════════════════════════════════════════════════════════
def make_wpw():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0
all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, pr=0.10, delta=True, delta_amp=0.28, delta_dur=0.06,
r_amp=0.9, tp_len=0.25)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="WPW Syndrome (short PR, delta wave, wide QRS, secondary ST changes)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Short PR\n(< 0.12s)", (0.04, -0.5),
fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(0.15, -1.1),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Delta\nwave", (0.13, 0.30),
fontsize=7, color="#6C3483",
arrowprops=dict(arrowstyle="->", color="#6C3483"),
xytext=(0.27, 0.7),
bbox=dict(boxstyle="round,pad=0.3", fc="#E8DAEF", ec="#6C3483", lw=0.8))
save_fig(fig, "07_wpw")
# ══════════════════════════════════════════════════════════════════════════════
# 8. VENTRICULAR TACHYCARDIA
# ══════════════════════════════════════════════════════════════════════════════
def make_vt():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
# Wide, bizarre QRS complexes at ~180 bpm
np.random.seed(1)
t = np.linspace(0, 3.0, 3000)
v = np.zeros(len(t))
cycle = 0.33 # ~180 bpm
starts = np.arange(0.05, 3.0, cycle)
for s in starts:
i = int(s * 1000)
# Wide bizarre complex: slow slurred rise then fall
wqrs = np.concatenate([
np.linspace(0, 0.15, 10),
np.linspace(0.15, 1.1, 20),
np.linspace(1.1, -0.3, 30),
np.linspace(-0.3, 0.1, 20),
np.linspace(0.1, 0, 10),
])
for j, q in enumerate(wqrs):
if i+j < len(v):
v[i+j] += q + np.random.uniform(-0.02, 0.02)
# Small independent P waves (AV dissociation)
p_times = [0.08, 0.41, 0.74, 1.07, 1.40, 1.73, 2.06, 2.39, 2.72]
for pt in p_times:
i = int(pt * 1000)
pw = np.array([0,0.04,0.09,0.12,0.09,0.04,0])
for j, q in enumerate(pw):
if i+j < len(v):
v[i+j] += q
ecg_grid(ax, (0, 3.0), ylim=(-0.8, 1.5), title="Ventricular Tachycardia (wide complex ~180 bpm, AV dissociation)")
ax.plot(t, v, color="#C0392B", linewidth=1.3)
ax.annotate("Wide bizarre\nQRS ≥ 0.12s", (0.45, 1.0),
fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Independent\nP waves (↑)", (1.45, 0.28),
fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "08_ventricular_tachycardia")
# ══════════════════════════════════════════════════════════════════════════════
# 9. VENTRICULAR FIBRILLATION
# ══════════════════════════════════════════════════════════════════════════════
def make_vf():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
np.random.seed(7)
t = np.linspace(0, 3.0, 3000)
v = (0.4 * np.sin(2*np.pi*6.5*t) + 0.3 * np.sin(2*np.pi*4.2*t+0.7) +
0.2 * np.sin(2*np.pi*9.1*t+1.4) + 0.15 * np.random.randn(len(t)))
ecg_grid(ax, (0, 3.0), ylim=(-1.2, 1.2), title="Ventricular Fibrillation (chaotic undulations, no identifiable QRS, no pulse)")
ax.plot(t, v, color="#C0392B", linewidth=1.1)
ax.annotate("Chaotic\nundulations", (1.2, 0.7),
fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("No P, QRS,\nor T waves", (2.1, -0.7),
fontsize=7, color="#7D6608",
bbox=dict(boxstyle="round,pad=0.3", fc="#FEF9E7", ec="#E67E22", lw=0.8))
save_fig(fig, "09_ventricular_fibrillation")
# ══════════════════════════════════════════════════════════════════════════════
# 10. TORSADES DE POINTES
# ══════════════════════════════════════════════════════════════════════════════
def make_tdp():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
np.random.seed(3)
t = np.linspace(0, 3.5, 3500)
v = np.zeros(len(t))
# Twisting axis: amplitude modulated sinusoid
freq = 7.0
amp_mod = 0.7 * np.sin(2*np.pi*0.9*t)
v = amp_mod * np.sin(2*np.pi*freq*t) + 0.05*np.random.randn(len(t))
# Add a few long-QT normal beats at the start
for s in [0.05, 0.55]:
i = int(s*1000)
beat = [0,0.12,0.12,0,-0.07,0.85,-0.2,0,0,0.05,0.18,0.22,0.18,0.1,0,0,0,0,0]
for j, q in enumerate(beat):
if i+j < len(v):
v[i+j] = q
# Break between normal and TdP
v[800:1000] = 0.02 * np.random.randn(200)
ecg_grid(ax, (0, 3.5), ylim=(-1.4, 1.4), title="Torsades de Pointes (polymorphic VT twisting around isoelectric axis, preceded by long QTc)")
ax.plot(t, v, color="#922B21", linewidth=1.1)
ax.annotate("Long QT\nbefore TdP", (0.3, 0.7), fontsize=7, color="#1A5276",
arrowprops=dict(arrowstyle="->", color="#1A5276"), xytext=(0.55, 1.1),
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Twisting QRS\namplitude", (2.0, 1.0), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "10_torsades_de_pointes")
# ══════════════════════════════════════════════════════════════════════════════
# 11. 1ST DEGREE AV BLOCK
# ══════════════════════════════════════════════════════════════════════════════
def make_av1():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, pr=0.28, tp_len=0.30)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="1st Degree AV Block (prolonged PR > 0.20s, every P conducts)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
# PR bracket
ax.annotate("", xy=(0.28+0.02, -0.7), xytext=(0.02, -0.7),
arrowprops=dict(arrowstyle="<->", color="#E67E22", lw=1.5))
ax.text(0.13, -0.90, "PR > 0.20s (prolonged)", fontsize=7, color="#E67E22",
ha="center", bbox=dict(boxstyle="round,pad=0.2", fc="#FDEBD0", ec="#E67E22", lw=0.7))
save_fig(fig, "11_av_block_1st")
# ══════════════════════════════════════════════════════════════════════════════
# 12. WENCKEBACH (MOBITZ I)
# ══════════════════════════════════════════════════════════════════════════════
def make_wenckebach():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
# PR progressively lengthens: 0.18, 0.24, 0.32 then P with no QRS
prs = [0.18, 0.24, 0.32]
x = 0.0; all_t, all_v = [], []
for pr in prs:
t, v = pqrst(t_start=x, pr=pr, tp_len=0.22)
all_t.append(t); all_v.append(v)
x = t[-1]
# Non-conducted P wave only
p_t = np.linspace(x+0.05, x+0.21, 60)
p_v = 0.15 * np.exp(-0.5*((p_t - (x+0.13))/0.025)**2)
all_t.append(p_t); all_v.append(p_v)
# Pause then next group
x = x + 0.45
for pr in [0.18, 0.24]:
t, v = pqrst(t_start=x, pr=pr, tp_len=0.22)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.2, 1.8),
title="2nd Degree AV Block – Mobitz I / Wenckebach (progressive PR lengthening → dropped QRS)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Progressive PR\nlengthening", (0.55, 1.3), fontsize=7, color="#E67E22",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEBD0", ec="#E67E22", lw=0.8))
ax.annotate("Dropped QRS\n(P not conducted)", (1.12, 0.6), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(1.30, 1.2),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "12_wenckebach")
# ══════════════════════════════════════════════════════════════════════════════
# 13. MOBITZ II
# ══════════════════════════════════════════════════════════════════════════════
def make_mobitz2():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, pr=0.22, tp_len=0.28)
all_t.append(t); all_v.append(v)
x = t[-1]
# Non-conducted P
p_t = np.linspace(x+0.05, x+0.21, 60)
p_v = 0.15 * np.exp(-0.5*((p_t-(x+0.13))/0.025)**2)
all_t.append(p_t); all_v.append(p_v)
x += 0.48
for _ in range(2):
t, v = pqrst(t_start=x, pr=0.22, tp_len=0.28)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.0, 1.8),
title="2nd Degree AV Block – Mobitz II (fixed PR, sudden dropped QRS without warning)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Fixed PR\ninterval", (0.35, 1.3), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Sudden\ndropped QRS", (1.27, 0.6), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(1.45, 1.2),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "13_mobitz2")
# ══════════════════════════════════════════════════════════════════════════════
# 14. 3RD DEGREE / COMPLETE HEART BLOCK
# ══════════════════════════════════════════════════════════════════════════════
def make_chb():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
t = np.linspace(0, 4.0, 4000)
v = np.zeros(len(t))
# P waves at 75 bpm (0.80s)
for pt in np.arange(0.10, 4.0, 0.80):
i = int(pt*1000)
pw = [0,0.05,0.12,0.15,0.12,0.06,0]
for j, q in enumerate(pw):
if i+j < len(v): v[i+j] += q
# Escape QRS at 38 bpm (1.6s) – wide/junctional
for qt in np.arange(0.45, 4.0, 1.58):
i = int(qt*1000)
esc = [0,0,-0.05,0.7,-0.15,0,0.28,0.15,0.05,0,0,0]
for j, q in enumerate(esc):
if i+j < len(v): v[i+j] += q
ecg_grid(ax, (0, 4.0), ylim=(-0.5, 1.2),
title="3rd Degree (Complete) AV Block (AV dissociation – P and QRS completely independent)")
ax.plot(t, v, color="#1A2E4A", linewidth=1.3)
ax.annotate("P waves\n(atrial rate)", (0.5, 0.3), fontsize=7, color="#1A5276",
arrowprops=dict(arrowstyle="->", color="#1A5276"),
xytext=(0.8, 0.65),
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Escape QRS\n(ventricular rate, unrelated to P)", (1.5, 0.85), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "14_complete_heart_block")
# ══════════════════════════════════════════════════════════════════════════════
# 15. RBBB
# ══════════════════════════════════════════════════════════════════════════════
def make_rbbb():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, rbbb=True, tp_len=0.25,
st_elev=-0.08, t_amp=-0.2)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Right Bundle Branch Block (RBBB) – rSR' 'M' pattern in V1, wide S in I/V6")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("rSR' ('M' / rabbit ears)\nin V1", (0.18, 0.8), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Wide S in I, V5-V6\n(slurred)", (0.65, -0.5), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "15_rbbb")
# ══════════════════════════════════════════════════════════════════════════════
# 16. LBBB
# ══════════════════════════════════════════════════════════════════════════════
def make_lbbb():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, lbbb=True, tp_len=0.25, st_elev=-0.12, t_amp=-0.25)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Left Bundle Branch Block (LBBB) – broad notched R in I/V5-V6, no septal Q, discordant ST-T")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Broad notched\n'M' shaped R wave", (0.22, 0.85), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Discordant ST-T\n(opposite to QRS)", (0.6, -0.6), fontsize=7, color="#117A65",
bbox=dict(boxstyle="round,pad=0.3", fc="#D1F2EB", ec="#148F77", lw=0.8))
save_fig(fig, "16_lbbb")
# ══════════════════════════════════════════════════════════════════════════════
# 17. STEMI (Inferior)
# ══════════════════════════════════════════════════════════════════════════════
def make_stemi():
fig, axes = plt.subplots(1, 2, figsize=(10, 2.5))
fig.patch.set_facecolor("#FFFDF0")
fig.suptitle("STEMI – Inferior (II, III, aVF): ST elevation with reciprocal depression in I, aVL",
fontsize=9, fontweight="bold", color="#C0392B")
# Inferior lead (ST elevation)
ax = axes[0]
x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, st_elev=0.35, t_amp=0.55, t_dur=0.20, tp_len=0.25)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.0, 2.0), title="Lead II/III/aVF (ST elevation)")
ax.plot(T, V, color="#C0392B", linewidth=1.5)
ax.annotate("Convex ST\nelevation", (0.33, 1.2), fontsize=7, color="#C0392B",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
# Reciprocal (ST depression in I/aVL)
ax2 = axes[1]
x = 0.0; all_t2, all_v2 = [], []
for _ in range(3):
t, v = pqrst(t_start=x, st_elev=-0.25, t_amp=-0.2, tp_len=0.25)
all_t2.append(t); all_v2.append(v)
x = t[-1]
T2 = np.concatenate(all_t2); V2 = np.concatenate(all_v2)
ecg_grid(ax2, (0, T2.max()+0.05), ylim=(-1.2, 1.5), title="Lead I/aVL (reciprocal ST depression)")
ax2.plot(T2, V2, color="#1A2E4A", linewidth=1.5)
ax2.annotate("Reciprocal\nST depression", (0.32, -0.55), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
plt.tight_layout(rect=[0,0,1,0.88])
save_fig(fig, "17_stemi")
# ══════════════════════════════════════════════════════════════════════════════
# 18. STEMI ANTERIOR with Q waves
# ══════════════════════════════════════════════════════════════════════════════
def make_stemi_anterior():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, q_amp=-0.45, q_dur=0.05, r_amp=0.5,
st_elev=0.40, t_amp=0.6, tp_len=0.25)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.0, 2.0),
title="Anterior STEMI (V1-V4): ST elevation + pathological Q waves")
ax.plot(T, V, color="#C0392B", linewidth=1.5)
ax.annotate("Pathological\nQ wave", (0.17, -0.6), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(0.28, -0.9),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Convex ST\nelevation", (0.38, 1.3), fontsize=7, color="#C0392B",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "18_stemi_anterior")
# ══════════════════════════════════════════════════════════════════════════════
# 19. NSTEMI / ST DEPRESSION
# ══════════════════════════════════════════════════════════════════════════════
def make_nstemi():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, st_elev=-0.20, t_amp=-0.18, tp_len=0.25)
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.0, 1.5),
title="NSTEMI / Subendocardial Ischaemia (horizontal/downsloping ST depression + T inversion)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("ST depression\n(horizontal)", (0.34, -0.38), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(0.55, -0.7),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("T inversion", (0.42, -0.30), fontsize=7, color="#1A5276",
arrowprops=dict(arrowstyle="->", color="#1A5276"),
xytext=(0.60, 0.5),
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "19_nstemi")
# ══════════════════════════════════════════════════════════════════════════════
# 20. PERICARDITIS
# ══════════════════════════════════════════════════════════════════════════════
def make_pericarditis():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
# Saddle-shape = concave ST elevation everywhere, PR depression
t, v = pqrst(t_start=x, p_amp=0.10, pr=0.17,
st_elev=0.22, t_amp=0.40, tp_len=0.22)
# Add PR depression: lower P amplitude relative to segment
# Subtract from the PR area
pr_start_i = np.argmin(np.abs(t - (x+0.08)))
pr_end_i = np.argmin(np.abs(t - (x+0.17)))
v[pr_start_i:pr_end_i] -= 0.07
all_t.append(t); all_v.append(v)
x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.8, 1.8),
title="Pericarditis (diffuse concave/saddle-shaped ST elevation, PR depression – most specific)")
ax.plot(T, V, color="#117A65", linewidth=1.5)
ax.annotate("Concave\n(smiley-face) ST↑", (0.38, 1.2), fontsize=7, color="#117A65",
bbox=dict(boxstyle="round,pad=0.3", fc="#D1F2EB", ec="#148F77", lw=0.8))
ax.annotate("PR depression\n(most specific)", (0.13, -0.15), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(0.28, -0.55),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "20_pericarditis")
# ══════════════════════════════════════════════════════════════════════════════
# 21. HYPERKALAEMIA SEQUENCE
# ══════════════════════════════════════════════════════════════════════════════
def make_hyperk():
fig, axes = plt.subplots(1, 3, figsize=(12, 2.5))
fig.patch.set_facecolor("#FFFDF0")
fig.suptitle("Hyperkalaemia ECG Progression",
fontsize=9, fontweight="bold", color="#1A2E4A")
# Stage 1: Tall peaked T
ax = axes[0]; x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, t_amp=0.80, t_dur=0.10, tp_len=0.30)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()), ylim=(-0.8, 1.5), title="Stage 1: Peaked T (K⁺ 5.5-6.5)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.3)
ax.annotate("Tall narrow\npeaked T", (0.38, 0.95), fontsize=7, color="#E67E22",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEBD0", ec="#E67E22", lw=0.8))
# Stage 2: Wide QRS + flat P
ax = axes[1]; x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, p_amp=0.05, pr=0.24,
q_dur=0.04, r_dur=0.07, s_dur=0.04,
t_amp=0.75, t_dur=0.10, tp_len=0.28)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()), ylim=(-0.8, 1.5), title="Stage 2: Flat P + Wide QRS (K⁺ 6.5-7.5)")
ax.plot(T, V, color="#E67E22", linewidth=1.3)
ax.annotate("Flat P\nLong PR", (0.15, 0.25), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
# Stage 3: Sine wave
ax = axes[2]
t = np.linspace(0, 2.5, 2500)
v = 0.7 * np.sin(2*np.pi*2.2*t) + 0.3 * np.sin(2*np.pi*4.4*t)
ecg_grid(ax, (0, 2.5), ylim=(-1.3, 1.3), title="Stage 3: Sine Wave (K⁺ > 7.5)")
ax.plot(t, v, color="#C0392B", linewidth=1.4)
ax.annotate("Sine wave\n(no P/T visible)", (0.8, 0.95), fontsize=7, color="#C0392B",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
plt.tight_layout(rect=[0,0,1,0.88])
save_fig(fig, "21_hyperkalaemia")
# ══════════════════════════════════════════════════════════════════════════════
# 22. HYPOKALAEMIA
# ══════════════════════════════════════════════════════════════════════════════
def make_hypok():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, t_amp=0.10, tp_len=0.28)
# Add prominent U wave after T
t_end = t[-1]
u_center = t_end - 0.12
u_t = np.linspace(u_center-0.05, u_center+0.05, 40)
u_v = 0.18 * np.exp(-0.5*((u_t - u_center)/0.025)**2)
t = np.concatenate([t, u_t])
v = np.concatenate([v, u_v])
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.8, 1.5),
title="Hypokalaemia (flat T wave, prominent U wave > T amplitude, ST depression)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Flat T wave", (0.37, 0.18), fontsize=7, color="#1A5276",
arrowprops=dict(arrowstyle="->", color="#1A5276"),
xytext=(0.50, 0.55),
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Prominent\nU wave", (0.54, 0.22), fontsize=7, color="#E67E22",
arrowprops=dict(arrowstyle="->", color="#E67E22"),
xytext=(0.68, 0.65),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEBD0", ec="#E67E22", lw=0.8))
save_fig(fig, "22_hypokalaemia")
# ══════════════════════════════════════════════════════════════════════════════
# 23. LVH with STRAIN
# ══════════════════════════════════════════════════════════════════════════════
def make_lvh():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, r_amp=1.8, s_amp=-0.5,
st_elev=-0.15, t_amp=-0.30, tp_len=0.25)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-1.2, 2.2),
title="LVH with Strain Pattern (tall R wave, ST depression + T inversion in I/aVL/V5-V6)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Tall R wave\n(Sokolow-Lyon ≥ 35mm)", (0.23, 1.7), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Strain: ST depression\n+ T inversion", (0.45, -0.50), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "23_lvh_strain")
# ══════════════════════════════════════════════════════════════════════════════
# 24. LONG QT / SHORT QT
# ══════════════════════════════════════════════════════════════════════════════
def make_qt():
fig, axes = plt.subplots(1, 2, figsize=(10, 2.5))
fig.patch.set_facecolor("#FFFDF0")
fig.suptitle("QT Interval Abnormalities", fontsize=9, fontweight="bold", color="#1A2E4A")
# Long QT
ax = axes[0]; x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, st_len=0.25, t_dur=0.24, tp_len=0.20)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Long QT (QTc > 440ms)")
ax.plot(T, V, color="#6C3483", linewidth=1.4)
ax.annotate("Prolonged QT\n(long flat ST + late T)", (0.35, 0.7), fontsize=7, color="#6C3483",
bbox=dict(boxstyle="round,pad=0.3", fc="#E8DAEF", ec="#6C3483", lw=0.8))
# Short QT
ax2 = axes[1]; x = 0.0; all_t2, all_v2 = [], []
for _ in range(4):
t, v = pqrst(t_start=x, st_len=0.02, t_dur=0.10, t_amp=0.50, tp_len=0.30)
all_t2.append(t); all_v2.append(v); x = t[-1]
T2 = np.concatenate(all_t2); V2 = np.concatenate(all_v2)
ecg_grid(ax2, (0, T2.max()+0.05), title="Short QT (QTc < 340ms, hypercalcaemia)")
ax2.plot(T2, V2, color="#1A5276", linewidth=1.4)
ax2.annotate("Short QT:\nT wave almost on QRS", (0.30, 0.7), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
plt.tight_layout(rect=[0,0,1,0.88])
save_fig(fig, "24_qt_abnormalities")
# ══════════════════════════════════════════════════════════════════════════════
# 25. BRUGADA PATTERN
# ══════════════════════════════════════════════════════════════════════════════
def make_brugada():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, rbbb=True,
st_elev=0.35, t_amp=-0.22, tp_len=0.28)
# Add coved shape: large downsloping ST in V1 area
st_start = x + 0.10 + 0.12 # after QRS
idx = np.where((t > st_start) & (t < st_start+0.12))[0]
if len(idx) > 0:
v[idx] += 0.15 * np.linspace(1, 0, len(idx))
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.8, 1.8),
title="Brugada Pattern (Type 1: coved ST elevation ≥ 2mm in V1-V2, RBBB morphology → risk of VF)")
ax.plot(T, V, color="#6C3483", linewidth=1.5)
ax.annotate("Coved ST elevation\nV1-V2 (Type 1)", (0.25, 1.3), fontsize=7, color="#6C3483",
bbox=dict(boxstyle="round,pad=0.3", fc="#E8DAEF", ec="#6C3483", lw=0.8))
ax.annotate("RBBB-like\nmorphology", (0.12, 0.65), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "25_brugada")
# ══════════════════════════════════════════════════════════════════════════════
# 26. PE (S1Q3T3)
# ══════════════════════════════════════════════════════════════════════════════
def make_pe():
fig, axes = plt.subplots(1, 2, figsize=(10, 2.5))
fig.patch.set_facecolor("#FFFDF0")
fig.suptitle("Pulmonary Embolism ECG Pattern", fontsize=9, fontweight="bold", color="#1A2E4A")
# Lead I: S wave
ax = axes[0]; x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, q_amp=-0.05, s_amp=-0.35, s_dur=0.04, tp_len=0.30)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.9, 1.3), title="Lead I: Deep S wave")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Deep S wave\n(S1)", (0.27, -0.52), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
# Lead III: Q + T inversion
ax2 = axes[1]; x = 0.0; all_t2, all_v2 = [], []
for _ in range(3):
t, v = pqrst(t_start=x, q_amp=-0.28, q_dur=0.04, r_amp=0.5, t_amp=-0.20, tp_len=0.30)
all_t2.append(t); all_v2.append(v); x = t[-1]
T2 = np.concatenate(all_t2); V2 = np.concatenate(all_v2)
ecg_grid(ax2, (0, T2.max()+0.05), ylim=(-0.9, 1.3), title="Lead III: Q wave + T inversion (Q3T3)")
ax2.plot(T2, V2, color="#1A2E4A", linewidth=1.4)
ax2.annotate("Q wave (Q3)", (0.19, -0.38), fontsize=7, color="#922B21",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax2.annotate("T inversion (T3)", (0.44, -0.28), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
plt.tight_layout(rect=[0,0,1,0.88])
save_fig(fig, "26_pe_s1q3t3")
# ══════════════════════════════════════════════════════════════════════════════
# 27. DIGOXIN EFFECT
# ══════════════════════════════════════════════════════════════════════════════
def make_digoxin():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(4):
t, v = pqrst(t_start=x, pr=0.22,
st_elev=-0.18, t_amp=-0.15, st_len=0.06, tp_len=0.25)
# Add scooping – make ST curve downward then up (reverse tick)
st_start = x + 0.22 + 0.08
idx = np.where((t > st_start) & (t < st_start + 0.12))[0]
if len(idx) > 4:
scoop = -0.10 * np.sin(np.pi * np.linspace(0, 1, len(idx)))
v[idx] += scoop
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.9, 1.5),
title="Digoxin Effect ('Reverse Tick' / Salvador Dalí moustache ST scooping, short QT)")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Scooped/sagging\nST (reverse tick)", (0.38, -0.5), fontsize=7, color="#E67E22",
arrowprops=dict(arrowstyle="->", color="#E67E22"),
xytext=(0.60, -0.78),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEBD0", ec="#E67E22", lw=0.8))
ax.annotate("Short QT\nProlonged PR", (0.10, 0.9), fontsize=7, color="#1A5276",
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
save_fig(fig, "27_digoxin_effect")
# ══════════════════════════════════════════════════════════════════════════════
# 28. HYPOTHERMIA (OSBORN WAVE)
# ══════════════════════════════════════════════════════════════════════════════
def make_hypothermia():
fig, ax = plt.subplots(figsize=(9, 2.5))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for _ in range(3):
t, v = pqrst(t_start=x, tp_len=0.55, r_amp=0.9, st_len=0.08)
# Add Osborn (J) wave at J point
j_point = x + 0.16 + 0.09 # approx end of QRS
ot = np.linspace(j_point, j_point+0.07, 40)
ov = 0.35 * np.exp(-0.5*((ot - (j_point+0.035))/0.015)**2)
t = np.concatenate([t, ot])
v = np.concatenate([v, ov])
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.8, 1.8),
title="Hypothermia (sinus bradycardia + Osborn / J wave at QRS-ST junction)")
ax.plot(T, V, color="#1A5276", linewidth=1.5)
ax.annotate("Osborn (J) wave\nat J-point", (0.28, 0.5), fontsize=7, color="#1A5276",
arrowprops=dict(arrowstyle="->", color="#1A5276"),
xytext=(0.45, 1.0),
bbox=dict(boxstyle="round,pad=0.3", fc="#D6EAF8", ec="#2980B9", lw=0.8))
ax.annotate("Sinus bradycardia\n(long R-R)", (1.0, 1.3), fontsize=7, color="#555",
bbox=dict(boxstyle="round,pad=0.3", fc="#F2F3F4", ec="#BFC9CA", lw=0.8))
save_fig(fig, "28_hypothermia_osborn")
# ══════════════════════════════════════════════════════════════════════════════
# 29. PAC
# ══════════════════════════════════════════════════════════════════════════════
def make_pac():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for i in range(5):
if i == 2:
# PAC: early, abnormal P morphology
t, v = pqrst(t_start=x, tp_len=0.18, p_amp=0.20, pr=0.14)
# Abnormal P: invert it slightly
p_region = np.where(t < x+0.10)[0]
v[p_region[-20:]] *= 0.3
else:
t, v = pqrst(t_start=x, tp_len=0.35)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), title="Premature Atrial Complex (PAC) – early P' wave, abnormal morphology, incomplete pause")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("PAC: early\nabnormal P'", (0.88, 0.8), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(1.1, 1.2),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
save_fig(fig, "29_pac")
# ══════════════════════════════════════════════════════════════════════════════
# 30. PVC
# ══════════════════════════════════════════════════════════════════════════════
def make_pvc():
fig, ax = plt.subplots(figsize=(9, 2.2))
fig.patch.set_facecolor("#FFFDF0")
x = 0.0; all_t, all_v = [], []
for i in range(5):
if i == 2:
# PVC: wide, bizarre, no P, full compensatory pause
t = np.linspace(x, x+0.55, 300)
v = np.zeros(300)
# Wide negative-then-positive bizarre QRS
qrs = np.concatenate([
np.linspace(0,0,10), np.linspace(0,-0.5,15),
np.linspace(-0.5,1.2,20), np.linspace(1.2,-0.3,15),
np.linspace(-0.3,0,10), np.zeros(230)
])[:300]
v += qrs
all_t.append(t); all_v.append(v); x = t[-1]
else:
t, v = pqrst(t_start=x, tp_len=0.28)
all_t.append(t); all_v.append(v); x = t[-1]
T = np.concatenate(all_t); V = np.concatenate(all_v)
ecg_grid(ax, (0, T.max()+0.05), ylim=(-0.9, 1.5),
title="Premature Ventricular Complex (PVC) – wide bizarre QRS, no preceding P, full compensatory pause")
ax.plot(T, V, color="#1A2E4A", linewidth=1.4)
ax.annotate("Wide bizarre\nQRS (PVC)", (0.90, 0.9), fontsize=7, color="#922B21",
arrowprops=dict(arrowstyle="->", color="#922B21"),
xytext=(1.1, 1.3),
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEDEC", ec="#C0392B", lw=0.8))
ax.annotate("Full compensatory\npause", (1.15, -0.3), fontsize=7, color="#E67E22",
bbox=dict(boxstyle="round,pad=0.3", fc="#FDEBD0", ec="#E67E22", lw=0.8))
save_fig(fig, "30_pvc")
# ── Run all ────────────────────────────────────────────────────────────────────
print("Generating ECG pattern images...")
make_nsr()
make_sinus_brady()
make_sinus_tachy()
make_af()
make_flutter()
make_avnrt()
make_wpw()
make_vt()
make_vf()
make_tdp()
make_av1()
make_wenckebach()
make_mobitz2()
make_chb()
make_rbbb()
make_lbbb()
make_stemi()
make_stemi_anterior()
make_nstemi()
make_pericarditis()
make_hyperk()
make_hypok()
make_lvh()
make_qt()
make_brugada()
make_pe()
make_digoxin()
make_hypothermia()
make_pac()
make_pvc()
print("All done!")
import os
imgs = os.listdir("/tmp/workspace/ecg-guide/ecg_images")
print(f"Total images: {len(imgs)}")
cd /tmp/workspace/ecg-guide && python generate_ecg_patterns.py 2>&1
~/ecg-guide/build_full_pdf.py
"""
Rebuild the ECG Complete Guide PDF with ECG waveform images embedded
alongside each disorder's text description.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether, Image
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
import os
IMG_DIR = "/tmp/workspace/ecg-guide/ecg_images"
OUTPUT_PATH = "/tmp/workspace/ecg-guide/ECG_Complete_Guide_with_Patterns.pdf"
# ── Colour palette ─────────────────────────────────────────────────────────────
RED = colors.HexColor("#C0392B")
DARK_RED = colors.HexColor("#96281B")
NAVY = colors.HexColor("#1A2E4A")
BLUE = colors.HexColor("#2980B9")
LIGHT_BLUE = colors.HexColor("#D6EAF8")
TEAL = colors.HexColor("#148F77")
LIGHT_TEAL = colors.HexColor("#D1F2EB")
ORANGE = colors.HexColor("#E67E22")
LIGHT_ORANGE = colors.HexColor("#FDEBD0")
PURPLE = colors.HexColor("#6C3483")
LIGHT_PURPLE = colors.HexColor("#E8DAEF")
GREEN = colors.HexColor("#1E8449")
LIGHT_GREEN = colors.HexColor("#D5F5E3")
YELLOW_BG = colors.HexColor("#FEF9E7")
GRAY_LIGHT = colors.HexColor("#F2F3F4")
GRAY_MID = colors.HexColor("#BFC9CA")
WHITE = colors.white
BLACK = colors.HexColor("#1C1C1C")
# ── Paragraph styles ──────────────────────────────────────────────────────────
def S(name, **kw):
return ParagraphStyle(name, **kw)
CHAPTER_STYLE = S("Chapter", fontName="Helvetica-Bold", fontSize=16, textColor=WHITE,
alignment=TA_LEFT, spaceAfter=4, spaceBefore=4, leading=20)
SECTION_STYLE = S("Section", fontName="Helvetica-Bold", fontSize=11, textColor=NAVY,
alignment=TA_LEFT, spaceAfter=2, spaceBefore=5, leading=14)
SUBSEC_STYLE = S("Subsec", fontName="Helvetica-Bold", fontSize=10, textColor=TEAL,
alignment=TA_LEFT, spaceAfter=2, spaceBefore=3, leading=13)
BODY_STYLE = S("Body", fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_JUSTIFY, spaceAfter=2, leading=13)
BULLET_STYLE = S("Bullet", fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_LEFT, spaceAfter=1, leading=12,
leftIndent=12, bulletIndent=4)
BOLD_BODY = S("Bold", fontName="Helvetica-Bold", fontSize=9, textColor=BLACK,
alignment=TA_LEFT, spaceAfter=2, leading=12)
TABLE_HEADER = S("TH", fontName="Helvetica-Bold", fontSize=8.5, textColor=WHITE,
alignment=TA_CENTER, leading=11)
TABLE_CELL = S("TC", fontName="Helvetica", fontSize=8.5, textColor=BLACK,
alignment=TA_LEFT, leading=11)
CAPTION_STYLE = S("Cap", fontName="Helvetica-Oblique", fontSize=8, textColor=colors.HexColor("#555"),
alignment=TA_CENTER, spaceAfter=3, leading=10)
# ── Helper builders ────────────────────────────────────────────────────────────
def chapter_header(title, color=NAVY):
tbl = Table([[Paragraph(title, CHAPTER_STYLE)]], colWidths=[170*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 7), ("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 12),
]))
return tbl
def section_box(title, bg=LIGHT_BLUE, fg=NAVY):
s = S("sb", fontName="Helvetica-Bold", fontSize=10.5, textColor=fg,
alignment=TA_LEFT, leading=13)
tbl = Table([[Paragraph(title, s)]], colWidths=[170*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 5), ("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 10),
("LINEBELOW", (0,0), (-1,-1), 1, fg),
]))
return tbl
def info_box(text, bg=YELLOW_BG, border=ORANGE):
s = S("ib", fontName="Helvetica", fontSize=9, textColor=BLACK,
alignment=TA_LEFT, leading=12, leftIndent=4)
tbl = Table([[Paragraph(text, s)]], colWidths=[165*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1.2, border),
("TOPPADDING", (0,0), (-1,-1), 6), ("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10), ("RIGHTPADDING", (0,0), (-1,-1), 10),
]))
return tbl
def bullet(text):
return Paragraph(f"<bullet>•</bullet> {text}", BULLET_STYLE)
def sp(h=3):
return Spacer(1, h*mm)
def hr():
return HRFlowable(width="100%", thickness=0.5, color=GRAY_MID,
spaceAfter=2*mm, spaceBefore=2*mm)
def img(fname, width=170*mm, caption=""):
path = os.path.join(IMG_DIR, fname)
items = []
if os.path.exists(path):
items.append(Image(path, width=width, height=width*0.265))
if caption:
items.append(Paragraph(caption, CAPTION_STYLE))
return items
def make_table(headers, rows, col_widths, hdr_bg=NAVY, alt=GRAY_LIGHT):
data = [[Paragraph(h, TABLE_HEADER) for h in headers]]
for i, row in enumerate(rows):
data.append([Paragraph(str(c), TABLE_CELL) for c in row])
tbl = Table(data, colWidths=col_widths)
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), hdr_bg),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, alt]),
("GRID", (0,0), (-1,-1), 0.4, GRAY_MID),
("TOPPADDING", (0,0), (-1,-1), 4), ("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5), ("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return tbl
# ── Page callbacks ─────────────────────────────────────────────────────────────
def cover_page(c, doc):
c.saveState()
w, h = A4
c.setFillColor(NAVY); c.rect(0, 0, w, h, fill=1, stroke=0)
c.setFillColor(RED); c.rect(0, h-12*mm, w, 12*mm, fill=1, stroke=0)
c.setFillColor(TEAL); c.rect(0, 0, w, 8*mm, fill=1, stroke=0)
# ECG trace decoration
import numpy as np
c.setStrokeColor(colors.HexColor("#E74C3C"))
c.setLineWidth(2)
y0 = h * 0.40
pts = [(10,y0),(50,y0),(60,y0+5),(65,y0-28),(70,y0+58),(75,y0-28),
(82,y0+7),(90,y0),(130,y0),(140,y0+4),(148,y0-11),(155,y0+4),
(165,y0),(210,y0),(220,y0+4),(228,y0-11),(235,y0+4),(245,y0),
(290,y0),(300,y0+5),(308,y0-28),(313,y0+58),(318,y0-28),
(325,y0+7),(333,y0),(375,y0),(385,y0+4),(393,y0-11),
(400,y0+4),(410,y0),(455,y0),(465,y0+5),(473,y0-28),
(478,y0+58),(483,y0-28),(490,y0+7),(500,y0),(595,y0)]
p = c.beginPath(); p.moveTo(*pts[0])
for pt in pts[1:]: p.lineTo(*pt)
c.drawPath(p, stroke=1, fill=0)
c.restoreState()
def normal_page(c, doc):
c.saveState()
w, h = A4
c.setFillColor(NAVY); c.rect(0, h-10*mm, w, 10*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 7.5); c.setFillColor(WHITE)
c.drawString(15*mm, h-6.5*mm, "ECG COMPLETE GUIDE: Disorders & Interpretation")
c.setFont("Helvetica", 7.5)
c.drawRightString(w-15*mm, h-6.5*mm, "Orris Medical Reference")
c.setFillColor(NAVY); c.rect(0, 0, w, 8*mm, fill=1, stroke=0)
c.setFont("Helvetica", 8); c.setFillColor(WHITE)
c.drawCentredString(w/2, 2.8*mm, f"Page {doc.page}")
c.setFillColor(RED); c.rect(0, 8*mm, 3*mm, h-18*mm, fill=1, stroke=0)
c.restoreState()
# ══════════════════════════════════════════════════════════════════════════════
# Build the story
# ══════════════════════════════════════════════════════════════════════════════
def build():
doc = SimpleDocTemplate(
OUTPUT_PATH, pagesize=A4,
rightMargin=18*mm, leftMargin=22*mm,
topMargin=18*mm, bottomMargin=16*mm,
title="ECG Complete Guide with Patterns",
author="Orris Medical Reference")
story = []
# ── COVER ────────────────────────────────────────────────────────────────
story.append(Spacer(1, 40*mm))
CT = S("ct", fontName="Helvetica-Bold", fontSize=32, textColor=WHITE,
alignment=TA_CENTER, leading=38)
CS = S("cs", fontName="Helvetica", fontSize=14,
textColor=colors.HexColor("#AED6F1"), alignment=TA_CENTER, leading=18)
CT2 = S("ct2", fontName="Helvetica-Oblique", fontSize=11,
textColor=colors.HexColor("#A9DFBF"), alignment=TA_CENTER, leading=14)
story += [
Paragraph("ECG Complete Guide", CT), sp(2),
Paragraph("Disorders, Patterns & Interpretation", CT), sp(6),
Paragraph("A Visual Reference for Medical Students, Nurses & Clinicians", CS), sp(30),
Paragraph("30 ECG Waveform Patterns · Arrhythmias · Conduction Blocks", CT2),
Paragraph("Ischaemia / MI · Hypertrophy · Electrolytes · Special Conditions", CT2),
sp(8),
Paragraph("Orris Medical Reference | 2026",
S("by", fontName="Helvetica", fontSize=10,
textColor=colors.HexColor("#85C1E9"), alignment=TA_CENTER)),
PageBreak()
]
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 1 – Basics & Approach
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 1: ECG Basics & Systematic Approach", NAVY))
story.append(sp(3))
story.append(section_box("ECG Paper & Measurements", LIGHT_TEAL, TEAL))
story.append(sp(2))
m_data = [
["Parameter", "Small Box (1mm)", "Large Box (5mm)", "Standard Value"],
["Time (horizontal)", "0.04 s", "0.20 s", "Paper speed 25 mm/s"],
["Voltage (vertical)", "0.1 mV", "0.5 mV", "Calibration: 1 mV = 10 mm"],
["PR Interval", "—", "—", "0.12 – 0.20 s"],
["QRS Duration", "—", "—", "< 0.12 s"],
["QT Interval (QTc)", "—", "—", "< 440 ms (♂) / < 460 ms (♀)"],
["P Wave", "—", "—", "< 0.12 s; < 2.5 mm"],
]
story.append(make_table(m_data[0], m_data[1:], [42*mm,28*mm,28*mm,72*mm], TEAL))
story.append(sp(3))
story.append(section_box("7-Step Systematic Approach", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
for step in [
("<b>1 – Rate:</b>", "300 ÷ large boxes between R waves. Normal 60–100 bpm."),
("<b>2 – Rhythm:</b>", "Regular / irregular? P before every QRS?"),
("<b>3 – P Wave:</b>", "Present? Upright in I, II? Duration, amplitude, morphology."),
("<b>4 – PR Interval:</b>", "0.12–0.20 s. Short → WPW/junctional. Long → 1st-degree block."),
("<b>5 – QRS Complex:</b>", "Width, axis, BBB, delta wave."),
("<b>6 – ST Segment & T Wave:</b>", "Elevation / depression? T inversion / peaked?"),
("<b>7 – QT Interval:</b>", "QTc = QT/√RR. Normal < 440 ms."),
]:
row = Table([[Paragraph(step[0], BOLD_BODY), Paragraph(step[1], BODY_STYLE)]],
colWidths=[35*mm, 133*mm])
row.setStyle(TableStyle([
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),
]))
story.append(row)
story.append(sp(2))
story.append(info_box("⚡ Always: Rate · Rhythm · P wave · PR · QRS · ST-T · QTc", YELLOW_BG, ORANGE))
story.append(sp(3))
story.append(section_box("12-Lead Views", LIGHT_BLUE, NAVY))
story.append(sp(2))
leads = [
["Leads", "Territory", "Artery"],
["II, III, aVF", "Inferior wall", "RCA"],
["I, aVL, V5-V6", "Lateral wall", "LCx"],
["V1-V2", "Septum", "LAD septal branches"],
["V3-V4", "Anterior wall", "LAD"],
["V7-V9 (or reciprocal)", "Posterior wall", "RCA/LCx"],
["V3R-V6R", "Right ventricle", "Proximal RCA"],
]
story.append(make_table(leads[0], leads[1:], [40*mm, 60*mm, 70*mm], NAVY))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 2 – Normal ECG
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 2: Normal Sinus Rhythm", TEAL))
story.append(sp(3))
story += img("01_normal_sinus.png", caption="Figure 2.1 – Normal Sinus Rhythm: P–QRS–T complex with annotated waveforms, rate ~72 bpm")
story.append(sp(3))
story.append(section_box("Normal Sinus Rhythm Criteria", LIGHT_TEAL, TEAL))
story.append(sp(2))
for b in ["Rate 60–100 bpm; regular rhythm",
"Upright P wave in I, II, aVF; inverted in aVR",
"Constant PR 0.12–0.20 s; every P followed by QRS",
"Narrow QRS < 0.12 s; normal axis −30° to +90°",
"T waves upright in I, II, V3–V6"]:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("Normal Waveform Reference", LIGHT_BLUE, NAVY))
story.append(sp(2))
wave_data = [
["Wave", "Normal Parameters", "Significance"],
["P", "< 0.12 s, < 2.5 mm", "Atrial depolarisation"],
["PR", "0.12–0.20 s", "AV node conduction"],
["Q", "< 0.04 s, < 25% R height", "Septal activation (pathological if wider/deeper)"],
["QRS", "< 0.12 s", "Ventricular depolarisation"],
["ST", "Isoelectric ±0.5–1 mm", "Repolarisation; changes = ischaemia"],
["T", "< 2/3 of R wave height", "Ventricular repolarisation"],
["QTc", "< 440 ms ♂ / < 460 ms ♀", "Prolonged = TdP risk"],
]
story.append(make_table(wave_data[0], wave_data[1:], [20*mm, 52*mm, 98*mm], TEAL))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 3 – Sinus Arrhythmias
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 3: Sinus Arrhythmias", NAVY))
story.append(sp(3))
# Brady
story.append(section_box("3.1 Sinus Bradycardia", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("02_sinus_bradycardia.png", caption="Figure 3.1 – Sinus Bradycardia: Normal P-QRS-T morphology, rate < 60 bpm, long R-R intervals")
story.append(sp(2))
for b in ["Rate < 60 bpm; regular rhythm; normal P morphology",
"Causes: athletes, vagal tone, hypothyroidism, beta-blockers, inferior MI",
"Symptoms: only if haemodynamically significant (dizziness, syncope)",
"Treatment: atropine if symptomatic; pacemaker if refractory"]:
story.append(bullet(b))
story.append(sp(3))
# Tachy
story.append(section_box("3.2 Sinus Tachycardia", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("03_sinus_tachycardia.png", caption="Figure 3.2 – Sinus Tachycardia: Normal P-QRS-T, rate > 100 bpm, short R-R intervals")
story.append(sp(2))
for b in ["Rate > 100 bpm; regular; normal P in I and II",
"Causes: pain, fever, PE, heart failure, anaemia, thyrotoxicosis, drugs",
"Treatment: address underlying cause – never ablate sinus tachycardia"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 4 – Supraventricular Arrhythmias
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 4: Supraventricular Arrhythmias", colors.HexColor("#1A5276")))
story.append(sp(3))
# PAC
story.append(section_box("4.1 Premature Atrial Complex (PAC)", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("29_pac.png", caption="Figure 4.1 – PAC: Early P' wave with abnormal morphology, incomplete compensatory pause, narrow QRS")
story.append(sp(2))
for b in ["Early, abnormal P' wave; PR may differ from sinus",
"Narrow QRS (unless aberrant conduction); incomplete compensatory pause",
"Can trigger AF, flutter, or SVT",
"Benign; treat only if symptomatic (beta-blockers)"]:
story.append(bullet(b))
story.append(sp(3))
# AF
story.append(section_box("4.2 Atrial Fibrillation (AF)", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("04_atrial_fibrillation.png", caption="Figure 4.2 – Atrial Fibrillation: Absent P waves, fibrillatory baseline (f waves), irregularly irregular RR intervals")
story.append(sp(2))
af_data = [
["Feature", "Description"],
["Rhythm", "Irregularly irregular – no two R-R intervals the same"],
["P waves", "Absent – replaced by chaotic fibrillatory baseline (f waves)"],
["Ventricular rate", "Variable 60–170 bpm (uncontrolled > 100)"],
["QRS", "Narrow (unless BBB or aberrancy)"],
["Stroke risk", "CHA₂DS₂-VASc score → anticoagulation if ≥ 2 (♂) / ≥ 3 (♀)"],
]
story.append(make_table(af_data[0], af_data[1:], [40*mm, 130*mm], TEAL))
story.append(sp(3))
# Flutter
story.append(section_box("4.3 Atrial Flutter", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("05_atrial_flutter.png", caption="Figure 4.3 – Atrial Flutter: Sawtooth flutter waves at ~300 bpm (best in II/III/aVF), regular 2:1 block → ventricular rate ~150 bpm")
story.append(sp(2))
for b in ["Atrial rate ~300 bpm; sawtooth waves in II, III, aVF",
"Ventricular rate: 150 (2:1), 100 (3:1), or 75 (4:1) bpm",
"Regular ventricular response (key differentiator from AF)",
"Ablation of cavo-tricuspid isthmus: > 95% cure rate"]:
story.append(bullet(b))
story.append(sp(3))
# AVNRT
story.append(section_box("4.4 AVNRT (SVT)", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("06_avnrt.png", caption="Figure 4.4 – AVNRT: Regular narrow QRS tachycardia ~180 bpm, P wave buried in/just after QRS (pseudo-r' in V1, pseudo-s in II/III)")
story.append(sp(2))
for b in ["Rate 150–250 bpm; abrupt onset/termination; regular narrow QRS",
"P wave buried in QRS (RP < 70 ms) or just after QRS",
"Pseudo-r' in V1 and pseudo-s in II are highly specific",
"Acute: vagal → adenosine → verapamil/diltiazem → cardioversion",
"Recurrent: catheter ablation (> 95% success)"]:
story.append(bullet(b))
story.append(sp(3))
# WPW
story.append(section_box("4.5 WPW / Pre-Excitation", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("07_wpw.png", caption="Figure 4.5 – WPW: Short PR < 0.12 s, delta wave (slurred QRS onset), wide QRS, discordant ST-T changes")
story.append(sp(2))
for b in ["Short PR < 0.12 s + delta wave + wide QRS",
"Risk: AF with rapid accessory pathway conduction → VF",
"<b>AVOID</b> in pre-excited AF: adenosine, digoxin, verapamil, beta-blockers",
"Use: procainamide / ibutilide IV or DC cardioversion for pre-excited AF",
"Curative: catheter ablation of accessory pathway"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 5 – Ventricular Arrhythmias
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 5: Ventricular Arrhythmias", RED))
story.append(sp(3))
# PVC
story.append(section_box("5.1 Premature Ventricular Complex (PVC)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("30_pvc.png", caption="Figure 5.1 – PVC: Wide bizarre QRS without preceding P wave, full compensatory pause")
story.append(sp(2))
for b in ["Wide QRS ≥ 0.12 s; bizarre morphology; no preceding P wave",
"Full compensatory pause (the normal beat after PVC comes on time)",
"Frequent PVCs (> 10,000/day or > 10%) can cause PVC-induced cardiomyopathy",
"R-on-T phenomenon: PVC lands on vulnerable T wave → can trigger VF"]:
story.append(bullet(b))
story.append(sp(3))
# VT
story.append(section_box("5.2 Ventricular Tachycardia (VT)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("08_ventricular_tachycardia.png", caption="Figure 5.2 – VT: Wide complex tachycardia ~180 bpm, bizarre QRS, independent P waves (AV dissociation)")
story.append(sp(2))
vt_data = [
["Feature", "Detail"],
["Rate", "> 100 bpm (usually 130–250 bpm)"],
["QRS", "≥ 0.12 s (usually ≥ 0.14 s); bizarre morphology"],
["AV dissociation", "P waves march through at different rate – diagnostic of VT"],
["Fusion beats", "Normal QRS morphology + wide QRS = fusion – diagnostic of VT"],
["Capture beats", "Occasional narrow QRS (sinus conducts through AV node) – diagnostic"],
["Axis", "Extreme NW axis (−90° to ±180°) strongly favours VT"],
["Management", "Pulsed VT: amiodarone / procainamide / cardioversion. Pulseless: defib"],
]
story.append(make_table(vt_data[0], vt_data[1:], [40*mm, 130*mm], RED))
story.append(sp(3))
# VF
story.append(section_box("5.3 Ventricular Fibrillation (VF)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("09_ventricular_fibrillation.png", caption="Figure 5.3 – VF: Chaotic undulations, no identifiable P, QRS, or T waves. No pulse. CARDIAC ARREST.")
story.append(sp(2))
story.append(info_box("⚠ VF = cardiac arrest. Immediate defibrillation + CPR. No output.", colors.HexColor("#FDEDEC"), DARK_RED))
story.append(sp(3))
# TdP
story.append(section_box("5.4 Torsades de Pointes (TdP)", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("10_torsades_de_pointes.png", caption="Figure 5.4 – TdP: Polymorphic VT with QRS amplitude twisting around isoelectric axis, preceded by prolonged QTc")
story.append(sp(2))
for b in ["Polymorphic VT; QRS twists around isoelectric baseline",
"Preceded by long QTc (usually > 500 ms)",
"Classic pattern: short-long-short initiating sequence",
"<b>Treatment:</b> IV magnesium sulfate 2 g IV; correct K⁺; overdrive pacing",
"Remove all QT-prolonging drugs; treat electrolyte abnormalities"]:
story.append(bullet(b))
story.append(sp(3))
# Brugada
story.append(section_box("5.5 Brugada Syndrome", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("25_brugada.png", caption="Figure 5.5 – Brugada Type 1: Coved ST elevation ≥ 2mm in V1-V2 with RBBB morphology, risk of sudden VF during sleep")
story.append(sp(2))
for b in ["Coved-type ST elevation ≥ 2 mm in V1–V2 (Type 1 = diagnostic)",
"RBBB-like morphology; normal QTc",
"Risk: polymorphic VT/VF, typically during sleep or fever",
"Management: ICD; avoid sodium channel blockers, fever triggers",
"Sodium channel provocative test (ajmaline/flecainide) if Type 2/3"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 6 – AV Blocks
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 6: AV Conduction Blocks", PURPLE))
story.append(sp(3))
# 1st degree
story.append(section_box("6.1 1st Degree AV Block", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("11_av_block_1st.png", caption="Figure 6.1 – 1st Degree AV Block: Prolonged PR > 0.20s, every P conducts, regular rhythm")
story.append(sp(2))
for b in ["PR interval > 0.20 s; constant; every P conducts to ventricle",
"Causes: vagal tone, inferior MI, beta-blockers, digitalis, myocarditis",
"Usually benign; no treatment required; monitor for progression"]:
story.append(bullet(b))
story.append(sp(3))
# Wenckebach
story.append(section_box("6.2 2nd Degree AV Block – Mobitz I (Wenckebach)", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("12_wenckebach.png", caption="Figure 6.2 – Wenckebach: PR progressively lengthens until one P wave is not conducted (dropped QRS), then cycle repeats – 'grouped beating'")
story.append(sp(2))
for b in ["Progressive PR lengthening → sudden dropped QRS",
"Grouped beating (regularly irregular); shortest PR after the pause",
"Block at AV node; inferior MI, vagal, inferior ischaemia",
"Usually benign; pacing if symptomatic"]:
story.append(bullet(b))
story.append(sp(3))
# Mobitz II
story.append(section_box("6.3 2nd Degree AV Block – Mobitz II", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("13_mobitz2.png", caption="Figure 6.3 – Mobitz II: Fixed PR interval, sudden dropped QRS without warning, HIS-Purkinje disease – higher risk than Wenckebach")
story.append(sp(2))
for b in ["Fixed PR interval; sudden dropped QRS without PR prolongation",
"Block below AV node (His bundle / bundle branches)",
"Often progresses to complete heart block → pacemaker indicated",
"Associated with anterior MI, sclerodegenerative disease"]:
story.append(bullet(b))
story.append(sp(3))
# CHB
story.append(section_box("6.4 3rd Degree (Complete) AV Block", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("14_complete_heart_block.png", caption="Figure 6.4 – Complete Heart Block: P waves (atrial rate ~75 bpm) and QRS complexes (escape rate ~38 bpm) are completely independent (AV dissociation)")
story.append(sp(2))
for b in ["Complete AV dissociation: P waves and QRS are independent",
"Junctional escape (narrow, 40–60 bpm) or ventricular escape (wide, < 40)",
"Emergency: haemodynamic compromise common",
"Treatment: transcutaneous pacing → temporary transvenous pacing → permanent pacemaker"]:
story.append(bullet(b))
story.append(sp(3))
# BBBs
story.append(section_box("6.5 Bundle Branch Blocks", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("15_rbbb.png", caption="Figure 6.5a – RBBB: rSR' ('rabbit ears'/'M' pattern) in V1, wide slurred S wave in I and V6, QRS ≥ 0.12s")
story.append(sp(2))
for b in ["RBBB: rSR' in V1; wide S in I, V6; QRS ≥ 0.12 s",
"Causes: normal variant, PE, RVH, ASD, anterior MI"]:
story.append(bullet(b))
story.append(sp(2))
story += img("16_lbbb.png", caption="Figure 6.5b – LBBB: Broad notched R wave in I/V5-V6 (no septal Q waves), QS in V1-V3, discordant ST-T changes, QRS ≥ 0.12s")
story.append(sp(2))
for b in ["LBBB: broad notched R in I, aVL, V5–V6; no septal Q waves",
"Discordant ST-T changes; QRS ≥ 0.12 s",
"New LBBB + chest pain = STEMI equivalent (apply Sgarbossa criteria)",
"Causes: IHD, dilated CMP, hypertension, aortic valve disease"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 7 – Ischaemia & MI
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 7: Ischaemia, Injury & Myocardial Infarction", DARK_RED))
story.append(sp(3))
# STEMI
story.append(section_box("7.1 STEMI – ST Elevation MI", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("17_stemi.png", caption="Figure 7.1a – Inferior STEMI: Convex ST elevation in II/III/aVF (left) with reciprocal ST depression in I/aVL (right)")
story.append(sp(2))
story += img("18_stemi_anterior.png", caption="Figure 7.1b – Anterior STEMI: ST elevation V1-V4 with pathological Q waves (necrosis). LAD territory.")
story.append(sp(2))
stemi_data = [
["Territory", "ST Elevation Leads", "Reciprocal", "Artery"],
["Inferior", "II, III, aVF", "I, aVL", "RCA (85%)"],
["Anterior", "V1–V4", "None/II,III", "LAD"],
["Lateral", "I, aVL, V5–V6", "V1–V3", "LCx"],
["Posterior", "Tall R + ST ↓ V1–V3", "V7–V9 ↑", "RCA/LCx"],
["RV", "V1, V3R–V4R", "—", "Proximal RCA"],
]
story.append(make_table(stemi_data[0], stemi_data[1:], [30*mm,42*mm,34*mm,64*mm], DARK_RED))
story.append(sp(2))
story.append(info_box("STEMI criteria: ST elevation ≥ 1 mm in ≥ 2 contiguous limb leads OR ≥ 2 mm in ≥ 2 contiguous precordial leads (V1-V4). New LBBB is a STEMI equivalent.", colors.HexColor("#FDEDEC"), DARK_RED))
story.append(sp(3))
# NSTEMI
story.append(section_box("7.2 NSTEMI / Subendocardial Ischaemia", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("19_nstemi.png", caption="Figure 7.2 – NSTEMI/UA: Horizontal or downsloping ST depression ≥ 0.5 mm with T wave inversion. No Q waves typically.")
story.append(sp(2))
for b in ["Horizontal/downsloping ST depression ≥ 0.5 mm",
"T wave inversion; no Q waves; no ST elevation",
"Troponin elevated (NSTEMI) or normal (unstable angina)",
"Management: antiplatelet + anticoagulant; early invasive strategy if high-risk"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 8 – Pericarditis & PE
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 8: Pericarditis & Pulmonary Embolism", TEAL))
story.append(sp(3))
story.append(section_box("8.1 Pericarditis", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("20_pericarditis.png", caption="Figure 8.1 – Acute Pericarditis: Diffuse concave (saddle-shaped) ST elevation + PR depression (most specific) in multiple leads")
story.append(sp(2))
peri_data = [
["Feature", "Pericarditis", "STEMI"],
["ST shape", "Concave (smiley face)", "Convex (frowning face)"],
["ST distribution", "Diffuse (most leads)", "Localised (territory)"],
["Reciprocal changes", "Absent", "Present"],
["PR depression", "Present (most specific)", "Absent"],
["Q waves", "Not present", "May develop"],
["Evolution", "Stages I–IV over days/weeks", "Dynamic hours–days"],
]
story.append(make_table(peri_data[0], peri_data[1:], [36*mm, 66*mm, 68*mm], TEAL))
story.append(sp(3))
story.append(section_box("8.2 Pulmonary Embolism (PE)", LIGHT_ORANGE, ORANGE))
story.append(sp(2))
story += img("26_pe_s1q3t3.png", caption="Figure 8.2 – PE S1Q3T3 pattern: Deep S in Lead I (left) + Q wave and T inversion in Lead III (right). Sinus tachycardia is the most common finding.")
story.append(sp(2))
for b in ["Sinus tachycardia: most common ECG finding (~40%)",
"S1Q3T3: S wave lead I + Q wave + T inversion lead III (~20%, not specific alone)",
"Right heart strain: new RBBB, RAD, T inversion V1–V4",
"AF may occur; low voltage or normal ECG does not exclude PE"]:
story.append(bullet(b))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 9 – Hypertrophy
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 9: Chamber Hypertrophy & Enlargement", TEAL))
story.append(sp(3))
story.append(section_box("9.1 Left Ventricular Hypertrophy (LVH)", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("23_lvh_strain.png", caption="Figure 9.1 – LVH with Strain: Tall R wave (voltage criteria met), ST depression + T inversion in lateral leads (I/aVL/V5-V6)")
story.append(sp(2))
lvh_data = [
["Criterion", "Threshold"],
["Sokolow-Lyon", "S(V1) + R(V5 or V6) ≥ 35 mm"],
["Cornell (men)", "R(aVL) + S(V3) > 28 mm"],
["Cornell (women)", "R(aVL) + S(V3) > 20 mm"],
["aVL voltage", "R wave in aVL ≥ 11 mm"],
["Strain pattern", "ST depression + T inversion in I, aVL, V5–V6 (lateral leads)"],
]
story.append(make_table(lvh_data[0], lvh_data[1:], [50*mm, 120*mm], TEAL))
story.append(sp(3))
story.append(section_box("9.2 Atrial Enlargement", LIGHT_BLUE, NAVY))
story.append(sp(2))
atrial_data = [
["Feature", "Left Atrial Enlargement (P mitrale)", "Right Atrial Enlargement (P pulmonale)"],
["P Duration", "> 0.12 s; bifid/notched in II", "Normal or short"],
["P Height", "Normal; deep negative terminal in V1", "≥ 2.5 mm in II/III/aVF (tall, peaked)"],
["Causes", "Mitral stenosis, LHF, HTN", "COPD, pulmonary HTN, RHF"],
]
story.append(make_table(atrial_data[0], atrial_data[1:], [28*mm, 71*mm, 71*mm], NAVY))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 10 – Electrolytes & Metabolic
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 10: Electrolyte & Metabolic Disorders", colors.HexColor("#1A5276")))
story.append(sp(3))
# Hyperkalaemia
story.append(section_box("10.1 Hyperkalaemia", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("21_hyperkalaemia.png", caption="Figure 10.1 – Hyperkalaemia Progression: Stage 1 – Tall peaked T (K⁺ 5.5-6.5) | Stage 2 – Flat P, wide QRS (K⁺ 6.5-7.5) | Stage 3 – Sine wave (K⁺ > 7.5)")
story.append(sp(2))
for b in ["K⁺ 5.5–6.5: peaked narrow T waves (first sign)",
"K⁺ 6.5–7.5: P flattens, PR prolongs, QRS widens",
"K⁺ > 7.5: sine wave pattern – imminent cardiac arrest",
"K⁺ > 9: VF/asystole",
"Emergency treatment: calcium gluconate (membrane stabilisation) → insulin/dextrose → kayexalate/dialysis"]:
story.append(bullet(b))
story.append(sp(3))
# Hypokalaemia
story.append(section_box("10.2 Hypokalaemia", LIGHT_BLUE, NAVY))
story.append(sp(2))
story += img("22_hypokalaemia.png", caption="Figure 10.2 – Hypokalaemia: Flat T wave, prominent U wave (U > T wave amplitude), ST depression, QTU prolongation")
story.append(sp(2))
for b in ["T wave flattening → U wave prominence (U > T in V2–V3)",
"T-U fusion; QTU prolongation; ST depression",
"Risk of TdP (especially with QT-prolonging drugs)",
"Treatment: oral/IV potassium replacement; correct magnesium (coexists)"]:
story.append(bullet(b))
story.append(sp(3))
# QT
story.append(section_box("10.3 QT Abnormalities (Long & Short QT)", LIGHT_PURPLE, PURPLE))
story.append(sp(2))
story += img("24_qt_abnormalities.png", caption="Figure 10.3 – Long QT (left): prolonged flat ST segment + late T wave (QTc > 440ms) | Short QT (right): T wave immediately follows QRS (QTc < 340ms, hypercalcaemia)")
story.append(sp(2))
qt_data = [
["Condition", "ECG", "Causes"],
["Long QT (acquired)", "Prolonged QTc > 440 ms (♂) / 460 ms (♀); flat ST + late T", "Drugs, hypokalaemia, hypomagnesaemia, hypothyroidism"],
["LQT1 (congenital)", "Broad-based T wave", "KCNQ1; triggered by exercise"],
["LQT2 (congenital)", "Notched/bifid T wave", "hERG; triggered by auditory startle"],
["LQT3 (congenital)", "Long flat ST, late peaked T", "SCN5A; triggered by sleep/rest"],
["Short QT", "QTc < 340 ms; peaked T near QRS", "Hypercalcaemia, digoxin, congenital short QT"],
]
story.append(make_table(qt_data[0], qt_data[1:], [32*mm, 60*mm, 78*mm], PURPLE))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 11 – Special Conditions
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 11: Special & Drug-Related Conditions", colors.HexColor("#117A65")))
story.append(sp(3))
# Digoxin
story.append(section_box("11.1 Digoxin Effect", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("27_digoxin_effect.png", caption="Figure 11.1 – Digoxin Effect: Scooped/sagging 'reverse tick' ST depression (Salvador Dalí moustache), short QT, prolonged PR, T inversion")
story.append(sp(2))
for b in ["Sagging/scooped ST ('reverse tick' or 'Salvador Dalí moustache')",
"Short QT interval; PR prolongation; T wave inversion/flattening",
"This is a <b>drug effect</b> – not toxicity; do not stop drug for this alone",
"<b>Toxicity ECG:</b> any arrhythmia – PAT with block (classic), bigeminy, junctional rhythms",
"Digoxin toxicity treatment: digibind (specific antibody fragment)"]:
story.append(bullet(b))
story.append(sp(3))
# Hypothermia
story.append(section_box("11.2 Hypothermia", LIGHT_TEAL, TEAL))
story.append(sp(2))
story += img("28_hypothermia_osborn.png", caption="Figure 11.2 – Hypothermia: Sinus bradycardia with Osborn (J) wave at QRS-ST junction, prolonged intervals, risk of AF and VF at severe hypothermia")
story.append(sp(2))
for b in ["Sinus bradycardia; Osborn (J) wave at QRS-ST junction – pathognomonic",
"J wave amplitude increases as temperature falls",
"Prolonged PR, QRS, QTc; may develop AF, VF",
"Treatment: rewarm; core temperature < 30°C → high VF risk"]:
story.append(bullet(b))
story.append(sp(3))
story.append(section_box("11.3 Key Drug Effects Summary", LIGHT_BLUE, NAVY))
story.append(sp(2))
drug_data = [
["Drug / Class", "Key ECG Effects", "Danger"],
["Digoxin (therapeutic)", "Scooped ST, short QT, PR long", "Toxicity: PAT with block, any arrhythmia"],
["Beta-blockers", "Sinus bradycardia, PR long, AV block", "Overdose: severe bradycardia"],
["Verapamil/Diltiazem", "Sinus brady, PR long, AV block", "Fatal bradycardia in overdose"],
["Amiodarone", "Brady, long PR/QRS/QTc", "Pro-arrhythmic despite being anti-arrhythmic"],
["Tricyclic antidepressants", "Wide QRS, long QT, right axis, tall R in aVR", "R:S in aVR > 0.7 → seizure/VT risk"],
["Cocaine", "Sinus tachy, STEMI (vasospasm), long QT", "Avoid beta-blockers (unopposed alpha)"],
["Sotalol/Quinidine", "Prolonged QTc, T wave changes", "TdP risk"],
]
story.append(make_table(drug_data[0], drug_data[1:], [38*mm, 72*mm, 60*mm], NAVY))
story.append(PageBreak())
# ═════════════════════════════════════════════════════════════════════════
# CHAPTER 12 – Quick Reference Cheatsheet
# ═════════════════════════════════════════════════════════════════════════
story.append(chapter_header("Chapter 12: Rapid ECG Cheatsheet", RED))
story.append(sp(3))
story.append(section_box("One-Line ECG Pattern Recognition – High Yield", LIGHT_BLUE, NAVY))
story.append(sp(2))
cheat = [
["ECG Finding", "Diagnosis"],
["Irregularly irregular + absent P waves + fibrillatory baseline", "Atrial Fibrillation"],
["Sawtooth P waves 300/min + regular ventricular rate 150/min", "Atrial Flutter (2:1 block)"],
["Narrow tachycardia + P buried in QRS + pseudo-r' in V1", "AVNRT (SVT)"],
["Short PR + delta wave + wide QRS", "WPW Pre-excitation"],
["Wide QRS + rSR' in V1 + wide S in I/V6", "RBBB"],
["Wide QRS + broad notched R in I/V6 + no septal Q waves", "LBBB"],
["Wide QRS tachycardia + AV dissociation + fusion beats", "Ventricular Tachycardia (VT)"],
["Chaotic undulations – no QRS", "Ventricular Fibrillation"],
["Polymorphic VT twisting axis after long QTc", "Torsades de Pointes"],
["Coved ST ≥ 2mm V1–V2 + RBBB morphology", "Brugada Syndrome"],
["Convex ST elevation + reciprocal changes + Q waves", "STEMI"],
["Diffuse concave ST elevation + PR depression (no reciprocals)", "Pericarditis"],
["Horizontal ST depression + T inversion (no Q waves)", "NSTEMI / Ischaemia"],
["Progressive PR lengthening → dropped QRS (grouped beating)", "Wenckebach (Mobitz I)"],
["Fixed PR + sudden dropped QRS", "Mobitz II AV block"],
["P waves and QRS completely unrelated", "Complete Heart Block (3rd degree)"],
["Peaked narrow T waves → sine wave (hyperkalaemia history)", "Hyperkalaemia"],
["Flat T wave + prominent U wave > T", "Hypokalaemia"],
["Short QT interval", "Hypercalcaemia"],
["Prolonged QT + long flat ST segment", "Long QT Syndrome"],
["Osborn (J) wave + bradycardia", "Hypothermia"],
["Scooped 'reverse tick' ST + short QT + PR long", "Digoxin Effect"],
["Sokolow-Lyon ≥ 35mm + lateral strain", "LVH"],
["S1Q3T3 + sinus tachycardia + right heart strain", "Pulmonary Embolism"],
["Short PR + delta wave in AF + rapid wide complex AF", "WPW + AF → risk VF"],
]
story.append(make_table(cheat[0], cheat[1:], [105*mm, 65*mm], DARK_RED, colors.HexColor("#FDEDEC")))
story.append(sp(3))
story.append(info_box(
"📌 GOLDEN RULES:\n"
"1. Wide complex tachycardia + haemodynamic instability = treat as VT, cardiovert immediately.\n"
"2. New LBBB + chest pain = STEMI equivalent. Apply Sgarbossa criteria.\n"
"3. Pre-excited AF (wide irregular) – NEVER give adenosine/digoxin/verapamil → VF risk.\n"
"4. Any bradycardia in inferior MI – check right-sided leads (RV MI, complete block).\n"
"5. TdP = IV magnesium 2g + stop QT drugs + correct K⁺/Mg²⁺.",
colors.HexColor("#FDEDEC"), DARK_RED))
story.append(sp(5))
story.append(info_box(
"📚 Recommended Reading:\n"
"• The ECG Made Easy – Hampton (beginner foundation)\n"
"• The ECG in Practice – Hampton (intermediate clinical)\n"
"• 150 ECG Problems – Hampton (case-based practice)\n"
"• Marriott's Practical Electrocardiography (advanced reference)\n"
"• LITFL ECG Library – litfl.com/ecg-library (free online; disorder-by-disorder)",
LIGHT_TEAL, TEAL))
story.append(sp(5))
# Footer
story.append(HRFlowable(width="100%", thickness=1, color=NAVY, spaceAfter=3*mm))
story.append(Paragraph(
"This guide is for educational purposes only. Always correlate ECG findings with clinical history and investigations. "
"Orris Medical Reference | ECG Complete Guide with Patterns | 2026",
S("ft", fontName="Helvetica-Oblique", fontSize=7.5,
textColor=colors.HexColor("#555"), alignment=TA_CENTER, leading=11)))
doc.build(story, onFirstPage=cover_page, onLaterPages=normal_page)
sz = os.path.getsize(OUTPUT_PATH)
print(f"PDF saved: {OUTPUT_PATH}")
print(f"File size: {sz:,} bytes ({sz/1024/1024:.2f} MB)")
if __name__ == "__main__":
build()
cd /tmp/workspace/ecg-guide && python build_full_pdf.py 2>&1
ECG Complete Guide with Patterns
PDF Document · PDF
| # | Pattern | Chapter |
|---|---|---|
| 1 | Normal Sinus Rhythm (P/Q/R/S/T/U labelled) | Ch. 2 |
| 2 | Sinus Bradycardia | Ch. 3 |
| 3 | Sinus Tachycardia | Ch. 3 |
| 4 | Atrial Fibrillation (fibrillatory baseline, irregular RR) | Ch. 4 |
| 5 | Atrial Flutter (sawtooth waves, 2:1 block) | Ch. 4 |
| 6 | AVNRT/SVT (P buried in QRS, pseudo-r') | Ch. 4 |
| 7 | WPW (delta wave, short PR) | Ch. 4 |
| 8 | Ventricular Tachycardia (AV dissociation) | Ch. 5 |
| 9 | Ventricular Fibrillation (chaotic) | Ch. 5 |
| 10 | Torsades de Pointes (twisting axis) | Ch. 5 |
| 11 | Brugada Syndrome (coved ST V1-V2) | Ch. 5 |
| 12 | 1st Degree AV Block | Ch. 6 |
| 13 | Wenckebach/Mobitz I | Ch. 6 |
| 14 | Mobitz II | Ch. 6 |
| 15 | Complete Heart Block | Ch. 6 |
| 16-17 | RBBB + LBBB | Ch. 6 |
| 18 | Inferior STEMI (ST elevation + reciprocals) | Ch. 7 |
| 19 | Anterior STEMI (Q waves + ST elevation) | Ch. 7 |
| 20 | NSTEMI (ST depression + T inversion) | Ch. 7 |
| 21 | Pericarditis (concave ST + PR depression) | Ch. 8 |
| 22 | PE S1Q3T3 | Ch. 8 |
| 23 | LVH with Strain | Ch. 9 |
| 24-25 | Hyperkalaemia progression (3 stages) + Hypokalaemia | Ch. 10 |
| 26-27 | Long QT + Short QT | Ch. 10 |
| 28 | Digoxin Effect (scooped ST) | Ch. 11 |
| 29 | Hypothermia (Osborn J wave) | Ch. 11 |
| 30 | PAC + PVC | Ch. 4/5 |