Here is a comprehensive guide to 10 common heart diseases with their characteristic ECG changes and illustrative ECG images.

Reading File
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Reading File
Reading File
Reading File
Reading File
Finding Sources
Finding Sources
Reading File
Finding Sources
Finding Sources
Loading Image
Loading Image
Loading Image
Loading Image
Loading Image
Loading Image
Reading File
Reading File
Loading Image
Loading Image
Finding Sources
Reading File
Reading File
Reading File
Reading File
Loading Image
Loading Image
This guide covers 12 ECG entities across 10 disease categories, with direct textbook ECG images retrieved from Harrison's, Guyton & Hall, Ganong's, and Tintinalli's. Here's a quick breakdown of what's included:
  1. Acute MI (STEMI/NSTEMI) - ST evolution, hyperacute T waves, Q waves, localization by territory
  2. Atrial Fibrillation - Absent P waves, irregularly irregular RR
  3. Atrial Flutter - Classic sawtooth F-waves, 2:1 block pattern
  4. Ventricular Fibrillation - Chaotic oscillations, coarse vs. fine VF
  5. 1° AV Block - PR > 200 ms (illustrated with PR = 0.38 s image)
  6. 2° AV Block - Wenckebach (progressive PR) and 2:1 block (both illustrated)
  7. 3° (Complete) AV Block - P-QRS dissociation (illustrated with atrial rate 107, ventricular rate 43)
  8. WPW Syndrome - Short PR + delta wave + wide QRS (full 12-lead image included)
  9. Left Ventricular Hypertrophy - Voltage criteria (Sokolow-Lyon, Cornell), strain pattern
  10. Bundle Branch Block (RBBB & LBBB) - rSR' vs. broad R/QS pattern
The serial MI evolution image (A through E panels showing normal → hyperacute → Q waves → deep inversion → recovery) is particularly useful for understanding the time course of ischemic ECG changes.

Generate a PDF reference card with ECG patterns and diagnostic criteria

Reading File
Running Command

ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or missing"

Writing File

~/ecg-reference/build_ecg_pdf.py

"""
ECG Reference Card PDF Generator
Uses ReportLab for professional multi-page PDF layout.
"""

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.flowables import Flowable
from reportlab.graphics.shapes import Drawing, Rect, Line, String, Path
from reportlab.graphics import renderPDF
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import io
import urllib.request
import os

# ── Color palette ──────────────────────────────────────────────────────────────
NAVY       = colors.HexColor("#0D2B4E")
CRIMSON    = colors.HexColor("#C0392B")
TEAL       = colors.HexColor("#1A6E7A")
GOLD       = colors.HexColor("#D4A017")
LIGHT_BLUE = colors.HexColor("#EAF4FB")
LIGHT_GRAY = colors.HexColor("#F5F5F5")
MID_GRAY   = colors.HexColor("#CCCCCC")
DARK_GRAY  = colors.HexColor("#444444")
WHITE      = colors.white
ECG_RED    = colors.HexColor("#CC2222")
ECG_BLUE   = colors.HexColor("#1A3A8A")
GRID_COLOR = colors.HexColor("#FFCCCC")

OUTPUT = "/home/daytona/workspace/ecg-reference/ECG_Reference_Card.pdf"
W, H   = A4  # 595.27 x 841.89 pts

# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()

def S(name, **kw):
    return ParagraphStyle(name, **kw)

TITLE_STYLE  = S("Title",  fontName="Helvetica-Bold", fontSize=22, textColor=WHITE,
                  alignment=TA_CENTER, spaceAfter=4)
SUB_STYLE    = S("Sub",    fontName="Helvetica", fontSize=10, textColor=colors.HexColor("#D0E8F5"),
                  alignment=TA_CENTER, spaceAfter=2)
HDR_STYLE    = S("Hdr",    fontName="Helvetica-Bold", fontSize=12, textColor=WHITE,
                  alignment=TA_LEFT, spaceBefore=0, spaceAfter=0, leftPadding=6)
BODY_STYLE   = S("Body",   fontName="Helvetica", fontSize=8, textColor=DARK_GRAY,
                  leading=11, spaceAfter=2)
SMALL_STYLE  = S("Small",  fontName="Helvetica", fontSize=7, textColor=DARK_GRAY,
                  leading=10)
BOLD_STYLE   = S("Bold",   fontName="Helvetica-Bold", fontSize=8, textColor=NAVY,
                  leading=11)
CAPTION      = S("Caption",fontName="Helvetica-Oblique", fontSize=6.5,
                  textColor=colors.HexColor("#666666"), alignment=TA_CENTER, spaceAfter=2)
FOOT_STYLE   = S("Foot",   fontName="Helvetica", fontSize=6.5, textColor=MID_GRAY,
                  alignment=TA_CENTER)
CRIT_STYLE   = S("Crit",   fontName="Helvetica-Bold", fontSize=7.5,
                  textColor=CRIMSON, leading=10)
NOTE_STYLE   = S("Note",   fontName="Helvetica-Oblique", fontSize=7,
                  textColor=TEAL, leading=10)

# ── ECG waveform drawing helpers ───────────────────────────────────────────────
def ecg_grid(d, x, y, w, h, rows=4, cols=5):
    """Draw ECG paper grid."""
    d.add(Rect(x, y, w, h, fillColor=colors.HexColor("#FFF8F8"),
               strokeColor=colors.HexColor("#FFBBBB"), strokeWidth=0.3))
    # minor grid
    small = w / (cols * 5)
    for i in range(cols * 5 + 1):
        lw = 0.4 if i % 5 == 0 else 0.15
        lc = colors.HexColor("#FF9999") if i % 5 == 0 else colors.HexColor("#FFCCCC")
        d.add(Line(x + i*small, y, x + i*small, y + h, strokeColor=lc, strokeWidth=lw))
    row_h = h / (rows * 5)
    for i in range(rows * 5 + 1):
        lw = 0.4 if i % 5 == 0 else 0.15
        lc = colors.HexColor("#FF9999") if i % 5 == 0 else colors.HexColor("#FFCCCC")
        d.add(Line(x, y + i*row_h, x + w, y + i*row_h, strokeColor=lc, strokeWidth=lw))

def draw_ecg_line(d, pts, color=ECG_RED, lw=1.2):
    """Draw a polyline from a list of (x,y) tuples."""
    if len(pts) < 2:
        return
    p = Path(strokeColor=color, strokeWidth=lw, fillColor=None)
    p.moveTo(pts[0][0], pts[0][1])
    for px, py in pts[1:]:
        p.lineTo(px, py)
    d.add(p)

def normal_qrs(cx, cy, scale=1.0, color=ECG_RED):
    """Return points for a single normal PQRST complex centered near (cx,cy)."""
    s = scale
    pts = [
        (cx - 22*s, cy),
        (cx - 18*s, cy + 3*s),   # P start
        (cx - 14*s, cy + 6*s),   # P peak
        (cx - 10*s, cy + 3*s),   # P end
        (cx - 6*s,  cy),          # PR segment
        (cx - 3*s,  cy - 2*s),   # Q
        (cx,        cy + 20*s),  # R peak
        (cx + 3*s,  cy - 4*s),   # S
        (cx + 6*s,  cy),          # ST start
        (cx + 12*s, cy + 4*s),   # T start
        (cx + 17*s, cy + 7*s),   # T peak
        (cx + 22*s, cy + 3*s),   # T end
        (cx + 28*s, cy),
    ]
    return pts

# ── ECG Diagram functions (each returns a Drawing) ────────────────────────────
def make_normal_ecg(w=160, h=50):
    """Normal sinus rhythm strip."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    pts = []
    for i in range(3):
        cx = 20 + i * 48
        pts += normal_qrs(cx, baseline, scale=0.80)
        if i < 2:
            pts.append((cx + 30, baseline))
    draw_ecg_line(d, pts, ECG_RED)
    return d

def make_afib_ecg(w=160, h=50):
    """Atrial fibrillation - irregular QRS, no P waves, fibrillatory baseline."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.42
    import math, random
    random.seed(42)
    # fibrillatory baseline
    fib_pts = []
    for x in range(int(w)):
        noise = math.sin(x * 0.9) * 1.5 + math.sin(x * 1.7) * 1.0 + math.sin(x * 3.1) * 0.7
        fib_pts.append((x, baseline + noise))
    draw_ecg_line(d, fib_pts, colors.HexColor("#FF6666"), lw=0.7)
    # irregular QRS - variable spacing
    qrs_x = [22, 46, 73, 94, 118, 140]
    for cx in qrs_x:
        qrs = [
            (cx - 2, baseline),
            (cx - 1, baseline - 2),
            (cx,     baseline + 16),
            (cx + 1, baseline - 5),
            (cx + 3, baseline),
        ]
        draw_ecg_line(d, qrs, ECG_BLUE, lw=1.4)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_aflutter_ecg(w=160, h=50):
    """Atrial flutter - sawtooth F-waves, 2:1 block."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # sawtooth
    saw = []
    x = 4
    while x < w - 10:
        saw += [(x, baseline), (x + 6, baseline + 8), (x + 12, baseline)]
        x += 12
    draw_ecg_line(d, saw, colors.HexColor("#DD4444"), lw=0.9)
    # QRS every 2nd flutter wave
    for cx in [28, 52, 76, 100, 124, 148]:
        qrs = [
            (cx - 2, baseline),
            (cx,     baseline + 18),
            (cx + 2, baseline - 4),
            (cx + 4, baseline),
            (cx + 8, baseline + 5),
            (cx + 12, baseline),
        ]
        draw_ecg_line(d, qrs, ECG_BLUE, lw=1.4)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_vfib_ecg(w=160, h=50):
    """Ventricular fibrillation - chaotic oscillations."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    import math, random
    random.seed(7)
    baseline = h * 0.5
    pts = []
    for x in range(int(w)):
        chaos = (math.sin(x * 0.3) * 9 + math.sin(x * 0.7 + 1) * 7
                 + math.sin(x * 1.3 + 2) * 5 + math.sin(x * 2.1) * 3
                 + (random.random() - 0.5) * 4)
        pts.append((x, baseline + chaos))
    draw_ecg_line(d, pts, ECG_RED, lw=1.0)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_vtach_ecg(w=160, h=50):
    """Ventricular tachycardia - wide, rapid, regular QRS."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    import math
    pts = [(0, baseline)]
    period = 22
    for i in range(7):
        cx = 8 + i * period
        pts += [
            (cx,      baseline),
            (cx + 3,  baseline + 16),
            (cx + 6,  baseline + 16),
            (cx + 9,  baseline - 10),
            (cx + 12, baseline - 10),
            (cx + 15, baseline + 5),
            (cx + 18, baseline + 5),
            (cx + 20, baseline),
        ]
    draw_ecg_line(d, pts, ECG_BLUE, lw=1.3)
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7,
                 fontName="Helvetica-Bold"))
    return d

def make_stemi_ecg(w=160, h=60):
    """STEMI - ST elevation pattern."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.38
    st_elev = 12  # ST elevation in pts
    pts = []
    for i in range(3):
        cx = 18 + i * 50
        pts += [
            (cx - 14, baseline),
            (cx - 10, baseline + 5),  # P
            (cx - 6,  baseline + 2),
            (cx - 3,  baseline - 2),  # Q
            (cx,      baseline + 22), # R tall
            (cx + 3,  baseline + st_elev),  # S (elevated)
            (cx + 8,  baseline + st_elev + 3),  # ST elevated
            (cx + 13, baseline + st_elev + 1),
            (cx + 17, baseline - 3),  # T inversion
            (cx + 22, baseline),
        ]
        if i < 2:
            pts.append((cx + 28, baseline))
    draw_ecg_line(d, pts, ECG_RED, lw=1.3)
    # label ST elevation arrow
    d.add(Line(18, baseline + st_elev, 18, baseline,
               strokeColor=colors.HexColor("#006600"), strokeWidth=1.0,
               strokeDashArray=[2, 2]))
    d.add(String(20, baseline + st_elev/2, "↑ST", fillColor=colors.HexColor("#006600"),
                 fontSize=6, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "V4", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_first_degree_block_ecg(w=160, h=50):
    """First-degree AV block - long PR interval."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    # Long PR = wider gap between P and QRS
    for i in range(3):
        cx = 22 + i * 50
        p_start = cx - 24
        pts = [
            (p_start,      baseline),
            (p_start + 4,  baseline + 5),   # P peak
            (p_start + 8,  baseline),        # P end
            # long PR segment
            (cx - 6,       baseline),
            (cx - 3,       baseline - 2),   # Q
            (cx,           baseline + 20),  # R
            (cx + 3,       baseline - 4),   # S
            (cx + 6,       baseline),
            (cx + 11,      baseline + 6),   # T
            (cx + 15,      baseline),
        ]
        draw_ecg_line(d, pts, ECG_RED, lw=1.2)
        # PR bracket
        if i == 1:
            d.add(Line(p_start, baseline - 8, cx, baseline - 8,
                       strokeColor=TEAL, strokeWidth=0.8))
            d.add(String(p_start + 4, baseline - 15, "PR > 200ms",
                         fillColor=TEAL, fontSize=6, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_complete_block_ecg(w=160, h=60):
    """Third-degree (complete) AV block - P-QRS dissociation."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # P waves at ~100 bpm (fast)
    p_xs = [10, 22, 34, 46, 58, 70, 82, 94, 106, 118, 130, 142, 154]
    for px in p_xs:
        draw_ecg_line(d, [(px, baseline), (px+3, baseline+6),
                          (px+6, baseline)], colors.HexColor("#AA3300"), lw=0.9)
    # QRS at ~40 bpm (slow) - wide
    qrs_xs = [18, 58, 98, 138]
    for cx in qrs_xs:
        draw_ecg_line(d, [
            (cx, baseline), (cx+2, baseline - 3),
            (cx+4, baseline + 14), (cx+6, baseline - 5),
            (cx+10, baseline), (cx+14, baseline + 4), (cx+18, baseline)
        ], ECG_BLUE, lw=1.4)
    # labels
    d.add(String(4, h - 8, "aVF", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    d.add(String(72, h - 8, "Atrial: 107/min  Ventricular: 43/min",
                 fillColor=DARK_GRAY, fontSize=5.5, fontName="Helvetica-Oblique"))
    return d

def make_wenckebach_ecg(w=160, h=55):
    """Second-degree AV block Mobitz I (Wenckebach)."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # 3 beats then dropped: PR progressively longer
    pr_lengths = [8, 13, 18]  # increasing PR
    cx_starts  = [14, 50, 92]
    for idx, (cx, pr) in enumerate(zip(cx_starts, pr_lengths)):
        # P wave
        draw_ecg_line(d, [(cx, baseline), (cx+3, baseline+7), (cx+6, baseline)],
                      colors.HexColor("#AA3300"), lw=1.0)
        # QRS after PR gap
        qcx = cx + pr
        draw_ecg_line(d, [
            (qcx, baseline), (qcx+1, baseline-2),
            (qcx+2, baseline+16), (qcx+3, baseline-4),
            (qcx+5, baseline), (qcx+9, baseline+6), (qcx+13, baseline)
        ], ECG_BLUE, lw=1.3)
        # PR label
        d.add(String(cx+1, baseline - 14, f"PR={pr*5}ms",
                     fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    # 4th P wave - no QRS (dropped)
    d.add(String(134, baseline + 10, "P", fillColor=colors.HexColor("#AA3300"),
                 fontSize=8, fontName="Helvetica-Bold"))
    draw_ecg_line(d, [(136, baseline), (139, baseline+7), (142, baseline)],
                  colors.HexColor("#AA3300"), lw=1.0)
    d.add(String(140, baseline - 14, "BLOCKED",
                 fillColor=CRIMSON, fontSize=5.5, fontName="Helvetica-Bold"))
    d.add(String(4, h - 8, "II", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_wpw_ecg(w=160, h=55):
    """WPW - short PR, delta wave, wide QRS."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.40
    for i in range(3):
        cx = 20 + i * 50
        pts = [
            (cx - 18, baseline),
            (cx - 14, baseline + 5),   # P
            (cx - 10, baseline + 2),   # short PR
            # delta wave (slow slur)
            (cx - 7,  baseline),
            (cx - 2,  baseline + 8),   # delta wave slur
            (cx,      baseline + 22),  # R peak
            (cx + 4,  baseline - 4),   # S
            (cx + 8,  baseline),
            (cx + 12, baseline + 5),   # T
            (cx + 16, baseline),
        ]
        draw_ecg_line(d, pts, ECG_RED, lw=1.3)
        # delta wave annotation
        if i == 1:
            d.add(String(cx - 8, baseline - 14, "δ wave",
                         fillColor=GOLD, fontSize=6, fontName="Helvetica-Bold"))
            d.add(Line(cx - 7, baseline - 1, cx - 1, baseline + 7,
                       strokeColor=GOLD, strokeWidth=0.8))
    d.add(String(4, h - 8, "I", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    return d

def make_lvh_ecg(w=160, h=65):
    """LVH - tall R in V5, deep S in V1, strain pattern."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.55
    # V1 - deep S
    cx1 = 28
    pts_v1 = [
        (cx1 - 12, baseline),
        (cx1 - 8,  baseline + 4),   # small r
        (cx1 - 4,  baseline),
        (cx1,      baseline - 22),  # deep S
        (cx1 + 4,  baseline),
        (cx1 + 9,  baseline - 3),   # T negative
        (cx1 + 14, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 14, 6, "V1", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    # V5 - tall R, strain
    cx5 = 110
    pts_v5 = [
        (cx5 - 12, baseline),
        (cx5 - 8,  baseline + 4),
        (cx5 - 4,  baseline),
        (cx5 - 2,  baseline - 2),   # small Q
        (cx5,      baseline + 40),  # TALL R
        (cx5 + 3,  baseline - 3),
        (cx5 + 6,  baseline - 5),   # ST depression (strain)
        (cx5 + 12, baseline - 12),  # T inversion (strain)
        (cx5 + 16, baseline - 3),
        (cx5 + 20, baseline),
    ]
    draw_ecg_line(d, pts_v5, ECG_RED, lw=1.3)
    d.add(String(cx5 - 8, 6, "V5", fillColor=NAVY, fontSize=7, fontName="Helvetica-Bold"))
    # measurement arrows
    d.add(Line(cx5 + 1, baseline, cx5 + 1, baseline + 40,
               strokeColor=TEAL, strokeWidth=0.7, strokeDashArray=[2, 2]))
    d.add(String(cx5 + 4, baseline + 18, "R>25mm",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    # strain label
    d.add(String(cx5 + 8, baseline - 16, "Strain",
                 fillColor=CRIMSON, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

def make_lbbb_ecg(w=160, h=55):
    """LBBB - broad monophasic R in V6, QS in V1."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.48
    # V1 - QS pattern
    cx1 = 28
    pts_v1 = [
        (cx1 - 10, baseline),
        (cx1,      baseline - 20),  # deep QS
        (cx1 + 10, baseline),
        (cx1 + 14, baseline + 5),   # T positive (discordant)
        (cx1 + 18, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 14, 6, "V1\n(QS)", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # V6 - broad positive R
    cx6 = 116
    pts_v6 = [
        (cx6 - 14, baseline),
        # broad, notched R (no Q)
        (cx6 - 8,  baseline + 2),
        (cx6 - 4,  baseline + 14),
        (cx6,      baseline + 22),  # R peak notched
        (cx6 + 4,  baseline + 18),
        (cx6 + 8,  baseline + 22),  # second notch (M-pattern)
        (cx6 + 12, baseline + 2),
        (cx6 + 14, baseline),
        (cx6 + 16, baseline - 6),  # ST depression
        (cx6 + 20, baseline - 10), # T inversion
        (cx6 + 24, baseline),
    ]
    draw_ecg_line(d, pts_v6, ECG_RED, lw=1.3)
    d.add(String(cx6 - 8, 6, "V6\n(RR')", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # QRS width annotation
    d.add(Line(cx6 - 14, baseline - 16, cx6 + 14, baseline - 16,
               strokeColor=TEAL, strokeWidth=0.7))
    d.add(String(cx6 - 8, baseline - 24, "QRS ≥ 120ms",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

def make_rbbb_ecg(w=160, h=55):
    """RBBB - rSR' in V1, wide S in V6."""
    d = Drawing(w, h)
    ecg_grid(d, 0, 0, w, h, rows=2, cols=4)
    baseline = h * 0.45
    # V1 - rSR' pattern
    cx1 = 32
    pts_v1 = [
        (cx1 - 14, baseline),
        (cx1 - 8,  baseline + 10),  # small r
        (cx1 - 4,  baseline + 2),
        (cx1,      baseline - 4),   # S
        (cx1 + 4,  baseline + 18),  # R' tall
        (cx1 + 10, baseline + 2),
        (cx1 + 14, baseline - 6),   # T inversion
        (cx1 + 18, baseline),
    ]
    draw_ecg_line(d, pts_v1, ECG_RED, lw=1.3)
    d.add(String(cx1 - 16, 6, "V1\n(rSR')", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    # V6 - wide S wave
    cx6 = 118
    pts_v6 = [
        (cx6 - 14, baseline),
        (cx6 - 10, baseline + 4),   # q
        (cx6 - 7,  baseline + 2),
        (cx6 - 4,  baseline),
        (cx6,      baseline + 18),  # R
        (cx6 + 4,  baseline),
        (cx6 + 8,  baseline - 12),  # wide, deep S
        (cx6 + 14, baseline - 4),
        (cx6 + 18, baseline),
        (cx6 + 20, baseline + 5),   # T
        (cx6 + 24, baseline),
    ]
    draw_ecg_line(d, pts_v6, ECG_RED, lw=1.3)
    d.add(String(cx6 - 6, 6, "V6\n(qRS)", fillColor=NAVY, fontSize=6.5,
                 fontName="Helvetica-Bold"))
    d.add(String(cx6 + 4, baseline - 20, "Wide S",
                 fillColor=TEAL, fontSize=5.5, fontName="Helvetica-Bold"))
    return d

# ── ECG Drawing Flowable ───────────────────────────────────────────────────────
from reportlab.platypus import Flowable as RLFlowable

class ECGDrawing(RLFlowable):
    def __init__(self, drawing, width=None, height=None):
        RLFlowable.__init__(self)
        self.drawing = drawing
        self.width = width or drawing.width
        self.height = height or drawing.height
    def wrap(self, aW, aH):
        return (self.width, self.height)
    def draw(self):
        renderPDF.draw(self.drawing, self.canv, 0, 0)

# ── Section header ─────────────────────────────────────────────────────────────
def section_header(title, number, color=NAVY):
    data = [[
        Paragraph(f"<b>{number}</b>", ParagraphStyle("Num", fontName="Helvetica-Bold",
                  fontSize=14, textColor=GOLD, alignment=TA_CENTER)),
        Paragraph(title, HDR_STYLE)
    ]]
    t = Table(data, colWidths=[18*mm, None])
    t.setStyle(TableStyle([
        ("BACKGROUND",  (0,0), (-1,-1), color),
        ("VALIGN",      (0,0), (-1,-1), "MIDDLE"),
        ("TOPPADDING",  (0,0), (-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING", (0,0), (0,-1), 3),
        ("RIGHTPADDING",(-1,0),(-1,-1), 6),
        ("ROUNDEDCORNERS", [3]),
    ]))
    return t

def criteria_table(rows, col_widths=None):
    """Two-column criteria table: feature | value."""
    styled_rows = []
    for r in rows:
        if len(r) == 1:  # sub-header
            styled_rows.append([
                Paragraph(f"<b>{r[0]}</b>",
                    ParagraphStyle("SubHdr", fontName="Helvetica-Bold", fontSize=7.5,
                                   textColor=NAVY, leading=10)),
                Paragraph("", BODY_STYLE)
            ])
        else:
            styled_rows.append([
                Paragraph(f"<b>{r[0]}</b>", BOLD_STYLE),
                Paragraph(r[1], BODY_STYLE)
            ])
    cw = col_widths or [45*mm, None]
    t = Table(styled_rows, colWidths=cw, hAlign="LEFT")
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0), (-1,-1), LIGHT_GRAY),
        ("BACKGROUND",   (0,0), (-1,0),  LIGHT_BLUE),
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_GRAY]),
        ("GRID",         (0,0), (-1,-1), 0.3, MID_GRAY),
        ("VALIGN",       (0,0), (-1,-1), "TOP"),
        ("TOPPADDING",   (0,0), (-1,-1), 3),
        ("BOTTOMPADDING",(0,0), (-1,-1), 3),
        ("LEFTPADDING",  (0,0), (-1,-1), 4),
        ("RIGHTPADDING", (0,0), (-1,-1), 4),
    ]))
    return t

def alert_box(text, bg=colors.HexColor("#FFF3CD"), border=GOLD):
    """Yellow alert/note box."""
    p = Paragraph(f"⚠ {text}", ParagraphStyle("Alert", fontName="Helvetica-Bold",
                  fontSize=7, textColor=colors.HexColor("#7D5A00"), leading=10))
    t = Table([[p]], colWidths=[None])
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), bg),
        ("BOX",          (0,0),(-1,-1), 1.0, border),
        ("TOPPADDING",   (0,0),(-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING",  (0,0),(-1,-1), 6),
    ]))
    return t

def critical_box(text):
    """Red critical alert."""
    p = Paragraph(f"🚨 {text}", ParagraphStyle("Crit2", fontName="Helvetica-Bold",
                  fontSize=7, textColor=WHITE, leading=10))
    t = Table([[p]], colWidths=[None])
    t.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), CRIMSON),
        ("TOPPADDING",   (0,0),(-1,-1), 4),
        ("BOTTOMPADDING",(0,0),(-1,-1), 4),
        ("LEFTPADDING",  (0,0),(-1,-1), 6),
    ]))
    return t

# ── Page canvas (header/footer) ────────────────────────────────────────────────
def on_page(canv, doc):
    canv.saveState()
    # top banner
    canv.setFillColor(NAVY)
    canv.rect(0, H - 28, W, 28, fill=1, stroke=0)
    canv.setFillColor(WHITE)
    canv.setFont("Helvetica-Bold", 10)
    canv.drawString(14, H - 18, "ECG REFERENCE CARD")
    canv.setFont("Helvetica", 8)
    canv.setFillColor(colors.HexColor("#A0C4E0"))
    canv.drawRightString(W - 14, H - 18, f"Page {doc.page}  |  Orris Medical Reference")
    # footer
    canv.setFillColor(NAVY)
    canv.rect(0, 0, W, 18, fill=1, stroke=0)
    canv.setFillColor(MID_GRAY)
    canv.setFont("Helvetica-Oblique", 6.5)
    canv.drawString(14, 6, "Sources: Harrison's 22E · Guyton & Hall · Ganong's · Tintinalli's EM")
    canv.setFillColor(colors.HexColor("#A0C4E0"))
    canv.drawRightString(W - 14, 6, "For educational use only — not a substitute for clinical judgment")
    canv.restoreState()

def on_first_page(canv, doc):
    on_page(canv, doc)

# ── Build document ─────────────────────────────────────────────────────────────
def build():
    os.makedirs("/home/daytona/workspace/ecg-reference", exist_ok=True)
    doc = SimpleDocTemplate(
        OUTPUT, pagesize=A4,
        leftMargin=14*mm, rightMargin=14*mm,
        topMargin=34*mm, bottomMargin=24*mm,
        title="ECG Reference Card",
        author="Orris Medical",
        subject="ECG Patterns & Diagnostic Criteria",
    )

    story = []

    # ── Cover block ──────────────────────────────────────────────────────────
    cover_data = [[
        Paragraph("ECG Reference Card", TITLE_STYLE),
    ],[
        Paragraph("Common Heart Diseases · Characteristic ECG Patterns · Diagnostic Criteria", SUB_STYLE),
    ],[
        Paragraph("Harrison's 22E  ·  Guyton & Hall  ·  Ganong's  ·  Tintinalli's EM", SUB_STYLE),
    ]]
    cover = Table(cover_data, colWidths=[None])
    cover.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,-1), NAVY),
        ("TOPPADDING",   (0,0),(-1,-1), 6),
        ("BOTTOMPADDING",(0,0),(-1,-1), 6),
        ("LEFTPADDING",  (0,0),(-1,-1), 10),
        ("RIGHTPADDING", (0,0),(-1,-1), 10),
        ("ROUNDEDCORNERS", [4]),
    ]))
    story.append(cover)
    story.append(Spacer(1, 5*mm))

    # ── Quick-Reference Summary Table ────────────────────────────────────────
    qr_hdr = [
        Paragraph("<b>Condition</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Key Feature</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Rate</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>Rhythm</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>P Wave</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
        Paragraph("<b>QRS</b>", ParagraphStyle("QH", fontName="Helvetica-Bold",
                  fontSize=7.5, textColor=WHITE, alignment=TA_CENTER)),
    ]
    qr_rows = [
        ["STEMI",             "ST elevation + Q waves",          "Variable",     "Regular",              "Normal",             "Normal/Wide"],
        ["NSTEMI",            "ST depression / T inversions",    "Variable",     "Regular",              "Normal",             "Normal"],
        ["Atrial Fibrillation","No P waves; f-waves",            "90–170 bpm",   "Irreg. irregular",     "Absent",             "Normal"],
        ["Atrial Flutter",    "Sawtooth F-waves 250–350/min",    "75–150 bpm",   "Regular (fixed ratio)","Flutter waves",      "Normal"],
        ["Ventricular Fib.",  "Chaotic oscillations",            "Indeterm.",    "No rhythm",            "None",               "None"],
        ["Ventricular Tach.", "Wide regular QRS ≥3 beats",       "100–250 bpm",  "Regular",              "Dissociated/absent","Wide ≥120ms"],
        ["1° AV Block",       "PR > 200 ms",                     "Normal",       "Regular",              "Normal",             "Normal"],
        ["2° AV Block (I)",   "Progressive PR → dropped QRS",    "Slow",         "Irregular",            "Normal",             "Normal"],
        ["2° AV Block (II)",  "Fixed PR → sudden dropped QRS",   "Slow",         "Irregular",            "Normal",             "Wide (usually)"],
        ["3° AV Block",       "P-QRS dissociation",              "Vent: 20–60",  "Indep. regular",       "Normal (indep.)",    "Wide or narrow"],
        ["WPW Syndrome",      "Short PR + delta wave",           "Normal",       "Regular (sinus)",      "Normal",             "Wide (≥120ms)"],
        ["LVH",               "Voltage criteria + strain",       "Normal",       "Regular",              "Bifid (LAE)",        "Tall"],
        ["LBBB",              "QS V1; broad R V6; no Q I,V6",    "Normal",       "Regular",              "Normal",             "≥120ms"],
        ["RBBB",              "rSR' V1; wide S I, V6",           "Normal",       "Regular",              "Normal",             "≥120ms"],
    ]
    cell_style = ParagraphStyle("Cell", fontName="Helvetica", fontSize=6.8,
                                textColor=DARK_GRAY, leading=9)
    bold_cell  = ParagraphStyle("BCell", fontName="Helvetica-Bold", fontSize=6.8,
                                textColor=NAVY, leading=9)

    table_data = [qr_hdr]
    for r in qr_rows:
        table_data.append([
            Paragraph(r[0], bold_cell),
            Paragraph(r[1], cell_style),
            Paragraph(r[2], cell_style),
            Paragraph(r[3], cell_style),
            Paragraph(r[4], cell_style),
            Paragraph(r[5], cell_style),
        ])

    cw = [32*mm, 44*mm, 22*mm, 28*mm, 22*mm, 22*mm]
    qr_table = Table(table_data, colWidths=cw, hAlign="LEFT")
    qr_table.setStyle(TableStyle([
        ("BACKGROUND",   (0,0),(-1,0),  NAVY),
        ("ROWBACKGROUNDS",(0,1),(-1,-1),[WHITE, LIGHT_BLUE]),
        ("GRID",         (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",       (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",   (0,0),(-1,-1), 2.5),
        ("BOTTOMPADDING",(0,0),(-1,-1), 2.5),
        ("LEFTPADDING",  (0,0),(-1,-1), 3),
        ("RIGHTPADDING", (0,0),(-1,-1), 3),
    ]))
    story.append(Paragraph("<b>QUICK-REFERENCE SUMMARY</b>",
                           ParagraphStyle("QRTitle", fontName="Helvetica-Bold",
                           fontSize=9, textColor=NAVY, spaceBefore=2, spaceAfter=3)))
    story.append(qr_table)
    story.append(PageBreak())

    # ════════════════════════════════════════════════════════════════════════════
    # Helper: build one condition block
    # ════════════════════════════════════════════════════════════════════════════
    def condition_block(num, title, drawing_fn, criteria_rows, notes=None,
                        critical=None, alert=None, ecg_caption=""):
        elems = []
        elems.append(KeepTogether([
            section_header(title, num, color=NAVY),
            Spacer(1, 2*mm),
        ]))

        # ECG drawing (left) + criteria (right)
        d = drawing_fn()
        ecg_cell = [ECGDrawing(d), Spacer(1, 1*mm), Paragraph(ecg_caption, CAPTION)]
        crit_cell = [criteria_table(criteria_rows)]
        if notes:
            crit_cell.append(Spacer(1, 1.5*mm))
            crit_cell.append(Paragraph(notes, NOTE_STYLE))

        side_table = Table(
            [[ecg_cell, crit_cell]],
            colWidths=[d.width + 4, None],
        )
        side_table.setStyle(TableStyle([
            ("VALIGN",       (0,0),(-1,-1), "TOP"),
            ("LEFTPADDING",  (0,0),(-1,-1), 0),
            ("RIGHTPADDING", (0,0),(-1,-1), 4),
            ("TOPPADDING",   (0,0),(-1,-1), 0),
            ("BOTTOMPADDING",(0,0),(-1,-1), 0),
        ]))
        elems.append(side_table)

        if critical:
            elems.append(Spacer(1, 1.5*mm))
            elems.append(critical_box(critical))
        if alert:
            elems.append(Spacer(1, 1.5*mm))
            elems.append(alert_box(alert))

        elems.append(Spacer(1, 4*mm))
        elems.append(HRFlowable(width="100%", thickness=0.5, color=MID_GRAY))
        elems.append(Spacer(1, 3*mm))
        return elems

    # ── 1. STEMI ──────────────────────────────────────────────────────────────
    story += condition_block(
        "1", "Acute Myocardial Infarction — STEMI",
        make_stemi_ecg,
        [
            ["Phase / Feature",     "ECG Finding"],
            ["Hyperacute (min)",    "Tall, peaked T waves (hyperacute T)"],
            ["Early acute (hours)", "ST elevation ≥1 mm (≥2 mm V1-V4) in ≥2 contiguous leads"],
            ["Evolving (hours-days)","T-wave inversions; pathological Q waves develop"],
            ["Established",         "Persistent Q waves; ST normalises"],
            ["Reciprocal changes",  "ST depression in opposite leads (mirror image)"],
            ["Localisation", ""],
            ["Anterior (LAD)",      "ST ↑ V1–V6, I, aVL"],
            ["Inferior (RCA)",      "ST ↑ II, III, aVF; reciprocal ↓ I, aVL"],
            ["Posterior (LCx)",     "ST ↓ V1–V3 + dominant R (ST ↑ equivalent)"],
            ["RV infarct",          "ST ↑ V3R, V4R; seen with inferior MI"],
        ],
        notes="Pathological Q wave: duration ≥40 ms or depth ≥25% of R-wave height.",
        critical="STEMI is a time-critical emergency. Target door-to-balloon ≤90 min.",
        ecg_caption="STEMI: ST elevation with early T-wave changes (V4)",
        col_widths=[38*mm, None]
    )

    # ── 2. NSTEMI ─────────────────────────────────────────────────────────────
    story += condition_block(
        "2", "Acute Coronary Syndrome — NSTEMI / UA",
        make_normal_ecg,   # shows baseline for comparison
        [
            ["Feature",             "ECG Finding"],
            ["ST depression",       "Horizontal or downsloping ≥0.5 mm in ≥2 contiguous leads"],
            ["T-wave inversion",    "Symmetric, ≥1 mm, in leads with prominent R waves"],
            ["Transient ST changes","Changing with symptoms (dynamic ischemia)"],
            ["Wellens' syndrome",   "Biphasic or deep symmetric T inversions V2–V3 (LAD stenosis)"],
            ["De Winter T-waves",   "ST depression + tall peaked T (LAD occlusion equivalent)"],
            ["No ST elevation",     "No pathological Q waves required for NSTEMI"],
            ["Troponin",            "Elevated (distinguishes NSTEMI from unstable angina)"],
        ],
        notes="10–15% of NSTEMI present with a normal ECG — serial ECGs and troponins are essential.",
        alert="Normal ECG does not exclude ACS — obtain serial ECGs and troponins.",
        ecg_caption="NSTEMI: changes may be subtle; compare with baseline ECG",
    )

    # ── 3. Atrial Fibrillation ────────────────────────────────────────────────
    story += condition_block(
        "3", "Atrial Fibrillation",
        make_afib_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["P waves",             "Absent — replaced by fibrillatory f-waves (350–600/min)"],
            ["RR intervals",        "Irregularly irregular — the hallmark"],
            ["f-wave rate",         "350–600 atrial impulses per minute"],
            ["QRS duration",        "Normal (unless aberrant conduction or pre-excitation)"],
            ["Ventricular rate",    "90–170 bpm (uncontrolled); 60–100 bpm (rate-controlled)"],
            ["Baseline",            "Fine, irregular low-voltage oscillations"],
        ],
        notes="Causes: hypertension, valvular disease, thyrotoxicosis, alcohol, HF, lone AF.",
        alert="Assess CHA₂DS₂-VASc score for anticoagulation in all patients with AF.",
        ecg_caption="AF: irregular QRS, absent P waves, fibrillatory baseline",
    )

    # ── 4. Atrial Flutter ─────────────────────────────────────────────────────
    story += condition_block(
        "4", "Atrial Flutter",
        make_aflutter_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["Flutter (F) waves",   "Regular sawtooth waves, 250–350/min; no isoelectric baseline"],
            ["Best seen in",        "Leads II, III, aVF, V1"],
            ["AV conduction ratio", "Typically 2:1 → ventricular rate ~150 bpm"],
            ["Variable ratios",     "3:1, 4:1, or variable block also possible"],
            ["QRS complex",         "Normal morphology (unless aberrant conduction)"],
            ["P waves",             "Flutter waves replace P waves"],
            ["Regularity",          "Ventricular rhythm regular or regularly irregular"],
        ],
        notes="Ventricular rate of exactly 150 bpm should always raise suspicion for atrial flutter with 2:1 block.",
        ecg_caption="Atrial flutter: sawtooth F-waves with 2:1 AV block",
    )

    story.append(PageBreak())

    # ── 5. Ventricular Fibrillation ───────────────────────────────────────────
    story += condition_block(
        "5", "Ventricular Fibrillation",
        make_vfib_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["Waveform",            "Completely chaotic, irregular oscillations — no identifiable complexes"],
            ["Coarse VF (early)",   "High-amplitude irregular waves ~0.5 mV; first 30 sec"],
            ["Fine VF (late)",      "Low-amplitude waves ~0.2 mV after 20–30 sec"],
            ["Rate",                "Indeterminate — 300–500 chaotic ventricular activations/min"],
            ["P waves",             "Absent"],
            ["QRS/T",               "Not identifiable"],
            ["Haemodynamics",       "Cardiac output = zero; immediately fatal"],
        ],
        critical="Initiate CPR immediately. Defibrillate without delay. Epinephrine 1 mg IV q3–5 min.",
        ecg_caption="VF: chaotic oscillations — no organised waveforms",
    )

    # ── 6. Ventricular Tachycardia ────────────────────────────────────────────
    story += condition_block(
        "6", "Ventricular Tachycardia (VT)",
        make_vtach_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥120 ms (wide complex)"],
            ["Rate",                "100–250 bpm (typically 140–200)"],
            ["Rhythm",              "Regular or slightly irregular"],
            ["P waves",             "Often dissociated from QRS (AV dissociation)"],
            ["Fusion beats",        "Pathognomonic — QRS hybrid of sinus + ectopic"],
            ["Capture beats",       "Narrow QRS during VT (sinus captures ventricle)"],
            ["Concordance",         "All precordial QRS same direction (positive or negative)"],
            ["Axis",                "Extreme axis deviation (NW axis) favours VT"],
            ["Duration",            "Sustained: ≥30 sec; Non-sustained: <30 sec"],
        ],
        notes="Brugada criteria, Vereckei algorithm, or aVR lead algorithm help distinguish VT from SVT with aberrancy. When in doubt, treat as VT.",
        critical="Pulseless VT: defibrillate immediately. Haemodynamically unstable VT: synchronised cardioversion.",
        ecg_caption="VT: wide, regular QRS complexes at rapid rate",
    )

    # ── 7. First-Degree AV Block ──────────────────────────────────────────────
    story += condition_block(
        "7", "First-Degree Atrioventricular Block",
        make_first_degree_block_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["PR interval",         "> 200 ms (0.20 s) — defining criterion"],
            ["Conduction",          "Every P wave is conducted to ventricles (1:1)"],
            ["P wave morphology",   "Normal"],
            ["QRS complex",         "Normal (unless co-existing bundle branch block)"],
            ["Rhythm",              "Regular"],
            ["Site of delay",       "Usually AV node; occasionally His-Purkinje"],
        ],
        notes="Causes: vagal tone (athletes), inferior MI, myocarditis, digitalis, hyperkalaemia, beta-blockers, calcium channel blockers.",
        alert="First-degree block alone rarely requires treatment but warrants monitoring for progression.",
        ecg_caption="1° AV block: PR = 0.38 s (normal ≤ 0.20 s)",
    )

    # ── 8. Second-Degree AV Block ─────────────────────────────────────────────
    story += condition_block(
        "8", "Second-Degree Atrioventricular Block",
        make_wenckebach_ecg,
        [
            ["Type",                "Distinguishing Feature"],
            ["Mobitz I (Wenckebach)","Progressive PR lengthening → dropped QRS; AV node level"],
            ["PR change in Mobitz I","Each PR longer than the last; RR shortens before drop"],
            ["Mobitz II",           "Sudden dropped QRS without prior PR change; infranodal"],
            ["QRS in Mobitz II",    "Usually wide (bundle branch block pattern)"],
            ["2:1 block",           "Every other P blocked; cannot classify without other beats"],
            ["Risk of Mobitz II",   "Higher risk of progression to complete block"],
            ["Wenckebach",          "Usually benign; pacemaker rarely needed"],
        ],
        notes="Mobitz II with wide QRS = high risk of complete heart block → pacemaker often needed.",
        alert="Mobitz II and high-degree block require urgent cardiology referral for pacemaker assessment.",
        ecg_caption="Wenckebach: PR progressively lengthens until 4th P wave is blocked",
    )

    story.append(PageBreak())

    # ── 9. Complete (Third-Degree) AV Block ──────────────────────────────────
    story += condition_block(
        "9", "Third-Degree (Complete) AV Block",
        make_complete_block_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["P waves",             "Present, regular, at normal atrial rate (60–100 bpm)"],
            ["QRS complexes",       "Present, regular, at escape rate (20–60 bpm)"],
            ["P-QRS relationship",  "Completely dissociated — no fixed PR interval"],
            ["QRS morphology",      "Narrow = junctional escape (40–60 bpm); Wide = ventricular escape (20–40 bpm)"],
            ["P rate > QRS rate",   "More P waves than QRS complexes"],
            ["Symptoms",            "Syncope (Stokes-Adams attacks), presyncope, fatigue"],
            ["Causes",              "Inferior MI, Lyme disease, myocarditis, surgical trauma, drugs"],
        ],
        critical="Complete heart block = medical emergency. Atropine (if narrow QRS). Transcutaneous pacing immediately. Cardiology consult for transvenous pacemaker.",
        ecg_caption="3° block: atrial rate 107/min, ventricular escape 43/min — fully dissociated",
    )

    # ── 10. WPW Syndrome ─────────────────────────────────────────────────────
    story += condition_block(
        "10", "Wolff-Parkinson-White (WPW) Syndrome",
        make_wpw_ecg,
        [
            ["Feature (sinus rhythm)", "ECG Finding"],
            ["PR interval",         "Short: < 120 ms (0.12 s) — AV node bypassed"],
            ["Delta wave",          "Slurred, slow upstroke at start of QRS — pre-excitation"],
            ["QRS duration",        "Wide: ≥ 120 ms (fusion of AP + normal conduction)"],
            ["ST-T changes",        "Discordant (opposite to delta wave/QRS direction)"],
            ["Tachyarrhythmias",    ""],
            ["Orthodromic AVRT",    "Narrow QRS tachycardia (most common, 70%)"],
            ["Antidromic AVRT",     "Wide QRS tachycardia (mimics VT)"],
            ["AF with WPW",         "Irregular wide-complex tachycardia — life-threatening"],
        ],
        notes="Localisation: negative delta in lead III → left posterior AP. Negative delta V1 → left-sided AP.",
        critical="AF + WPW: AVOID adenosine, beta-blockers, CCBs, digoxin — may cause VF. Use procainamide or cardioversion.",
        ecg_caption="WPW: short PR, delta (δ) wave slur, wide QRS — dual pathway conduction",
    )

    # ── 11. LVH ───────────────────────────────────────────────────────────────
    story += condition_block(
        "11", "Left Ventricular Hypertrophy (LVH)",
        make_lvh_ecg,
        [
            ["Voltage Criteria",    "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"],
            ["RaVL",                "> 20 mm (women) or > 28 mm (men)"],
            ["Associated findings", ""],
            ["Strain pattern",      "ST depression + asymmetric T-inversion in I, aVL, V5–V6"],
            ["Left axis deviation", "QRS axis < −30°"],
            ["LAE",                 "Bifid P wave ≥120ms in II; biphasic P in V1"],
            ["QRS widening",        "May progress to LBBB"],
        ],
        notes="Sensitivity of voltage criteria is low (~50%) in older adults, obese patients, and those with RBBB. Echocardiography provides definitive diagnosis.",
        alert="Prominent voltage in young athletes may be a normal variant — clinical context is essential.",
        ecg_caption="LVH: deep S in V1, very tall R in V5, lateral strain pattern",
    )

    story.append(PageBreak())

    # ── 12. LBBB ─────────────────────────────────────────────────────────────
    story += condition_block(
        "12", "Left Bundle Branch Block (LBBB)",
        make_lbbb_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥ 120 ms (complete LBBB)"],
            ["Lead V1",             "Broad, deep QS (or rS) complex"],
            ["Leads I, aVL, V5–V6", "Broad, monophasic R wave (no septal Q)"],
            ["Septal Q waves",      "Absent in I, aVL, V5, V6 (septal activation reversed)"],
            ["ST-T changes",        "Discordant to QRS (ST ↑ where QRS negative; ST ↓ where QRS positive)"],
            ["Sgarbossa criteria",  "Concordant ST ≥1 mm (score 5) or ST ↑ ≥5 mm (score 2) in new LBBB suggest MI"],
            ["Axis",                "Left axis deviation common"],
            ["Causes",              "Hypertension, CAD, dilated cardiomyopathy, aortic stenosis"],
        ],
        critical="New LBBB + chest pain = STEMI equivalent until proven otherwise. Activate cath lab.",
        ecg_caption="LBBB: QS in V1, broad notched R in V6, QRS ≥120ms",
    )

    # ── 13. RBBB ─────────────────────────────────────────────────────────────
    story += condition_block(
        "13", "Right Bundle Branch Block (RBBB)",
        make_rbbb_ecg,
        [
            ["Feature",             "ECG Finding"],
            ["QRS duration",        "≥ 120 ms (complete RBBB)"],
            ["Lead V1",             "rSR' pattern ('M' shape or rabbit ears); terminal R' broad"],
            ["Leads I, V5–V6",      "Wide, slurred S wave (terminal activation rightward)"],
            ["ST-T changes",        "T-wave inversion V1–V3 (secondary to block; not ischemic)"],
            ["Axis",                "Right axis deviation common"],
            ["Causes (new onset)",  "PE, right heart strain, anterior MI, myocarditis"],
            ["Isolated RBBB",       "Often incidental; may be normal variant"],
            ["S₁Q₃T₃ + RBBB",       "Suggests acute right heart strain / PE"],
        ],
        notes="Right bundle branch block is more commonly a benign finding than LBBB. New RBBB warrants investigation for PE or anterior MI.",
        ecg_caption="RBBB: rSR' in V1, wide terminal S in V6, QRS ≥120ms",
    )

    # ── Electrolyte Changes reference ─────────────────────────────────────────
    story.append(Spacer(1, 2*mm))
    story.append(section_header("⚡ ELECTROLYTE & ION EFFECTS ON ECG", "REF", color=TEAL))
    story.append(Spacer(1, 2*mm))

    elec_data = [
        [
            Paragraph("<b>Hyperkalaemia</b>", BOLD_STYLE),
            Paragraph("Tall peaked T waves (early) → PR prolongation → wide QRS → sine wave → asystole", BODY_STYLE),
            Paragraph("<b>Hypokalaemia</b>", BOLD_STYLE),
            Paragraph("Flat/inverted T waves, prominent U waves, ST depression, apparent long QT", BODY_STYLE),
        ],
        [
            Paragraph("<b>Hypercalcaemia</b>", BOLD_STYLE),
            Paragraph("Short QT interval, short ST segment, J waves (Osborn waves)", BODY_STYLE),
            Paragraph("<b>Hypocalcaemia</b>", BOLD_STYLE),
            Paragraph("Prolonged QT interval (prolonged ST segment), unchanged T wave", BODY_STYLE),
        ],
        [
            Paragraph("<b>Long QT Syndrome</b>", BOLD_STYLE),
            Paragraph("QTc > 450 ms (men) / 460 ms (women); risk of torsades de pointes", BODY_STYLE),
            Paragraph("<b>Digoxin effect</b>", BOLD_STYLE),
            Paragraph("Scooped ST depression ('reverse tick'), short QT, PR prolongation", BODY_STYLE),
        ],
    ]
    elec_table = Table(elec_data, colWidths=[35*mm, 60*mm, 35*mm, None])
    elec_table.setStyle(TableStyle([
        ("BACKGROUND",    (0,0),(-1,-1), LIGHT_GRAY),
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_BLUE, WHITE]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("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(elec_table)
    story.append(Spacer(1, 4*mm))

    # ── ECG Normal Values ─────────────────────────────────────────────────────
    story.append(section_header("📏 NORMAL ECG INTERVALS & VALUES", "NV", color=colors.HexColor("#2E7D32")))
    story.append(Spacer(1, 2*mm))

    nv_data = [
        ["PR interval",       "120–200 ms (3–5 small squares)"],
        ["QRS duration",      "60–100 ms (<120 ms; 3 small squares)"],
        ["QT interval",       "350–440 ms (rate-dependent)"],
        ["QTc (corrected)",   "Men: ≤ 450 ms | Women: ≤ 460 ms (Bazett formula)"],
        ["P-wave duration",   "< 120 ms; amplitude < 2.5 mm in limb leads"],
        ["P-wave axis",       "0° to +75° (upright in I, II; inverted in aVR)"],
        ["QRS axis",          "−30° to +90° (normal); −30° to −90° = LAD; >+90° = RAD"],
        ["Heart rate",        "60–100 bpm (sinus rhythm)"],
        ["Paper speed",       "25 mm/sec | 1 small square = 40 ms, 1 large = 200 ms"],
        ["Amplitude",         "Standard: 10 mm/mV | 1 small square = 0.1 mV"],
    ]
    nv_table_data = []
    for r in nv_data:
        nv_table_data.append([
            Paragraph(f"<b>{r[0]}</b>", BOLD_STYLE),
            Paragraph(r[1], BODY_STYLE),
        ])
    nv_table = Table(nv_table_data, colWidths=[48*mm, None])
    nv_table.setStyle(TableStyle([
        ("ROWBACKGROUNDS",(0,0),(-1,-1), [WHITE, LIGHT_BLUE]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",        (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",    (0,0),(-1,-1), 3),
        ("BOTTOMPADDING", (0,0),(-1,-1), 3),
        ("LEFTPADDING",   (0,0),(-1,-1), 4),
    ]))
    story.append(nv_table)
    story.append(Spacer(1, 4*mm))

    # ── STEMI Territory localisation table ────────────────────────────────────
    story.append(section_header("🗺  STEMI TERRITORY LOCALISATION", "LOC",
                                color=colors.HexColor("#6A1515")))
    story.append(Spacer(1, 2*mm))

    loc_data = [
        [Paragraph("<b>Territory</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>ST Elevation Leads</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>Reciprocal ST Depression</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE)),
         Paragraph("<b>Artery (usually)</b>", ParagraphStyle("LH",fontName="Helvetica-Bold",fontSize=7.5,textColor=WHITE))],
        ["Anterior",      "V1–V4",         "II, III, aVF",      "LAD"],
        ["Anterolateral",  "V4–V6, I, aVL", "II, III, aVF",      "LAD (diagonal) or LCx"],
        ["Lateral",       "I, aVL, V5–V6", "II, III, aVF",      "LCx or diagonal"],
        ["Inferior",      "II, III, aVF",  "I, aVL",            "RCA (80%) or LCx"],
        ["Posterior",     "V7–V9 (direct); ST ↓ V1–V3", "None", "RCA or LCx"],
        ["RV",            "V3R, V4R",      "None",              "Proximal RCA"],
        ["Septal",        "V1–V2",         "Variable",          "Septal perforators (LAD)"],
    ]
    loc_styled = []
    for i, r in enumerate(loc_data):
        if i == 0:
            loc_styled.append(r)
        else:
            loc_styled.append([Paragraph(r[j], SMALL_STYLE) for j in range(4)])
    loc_table = Table(loc_styled, colWidths=[30*mm, 46*mm, 46*mm, None])
    loc_table.setStyle(TableStyle([
        ("BACKGROUND",    (0,0),(-1,0),  colors.HexColor("#6A1515")),
        ("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, LIGHT_GRAY]),
        ("GRID",          (0,0),(-1,-1), 0.3, MID_GRAY),
        ("VALIGN",        (0,0),(-1,-1), "MIDDLE"),
        ("TOPPADDING",    (0,0),(-1,-1), 2.5),
        ("BOTTOMPADDING", (0,0),(-1,-1), 2.5),
        ("LEFTPADDING",   (0,0),(-1,-1), 4),
    ]))
    story.append(loc_table)

    # ── Build ──────────────────────────────────────────────────────────────────
    doc.build(story, onFirstPage=on_first_page, onLaterPages=on_page)
    print(f"PDF generated: {OUTPUT}")

if __name__ == "__main__":
    build()
Running Command

cd /home/daytona/workspace/ecg-reference && python build_ecg_pdf.py 2>&1

Editing File

~/ecg-reference/build_ecg_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-reference/build_ecg_pdf.py'
Editing File

~/ecg-reference/build_ecg_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-reference/build_ecg_pdf.py'
Running Command

cd /home/daytona/workspace/ecg-reference && python build_ecg_pdf.py 2>&1

Running Command

ls -lh /home/daytona/workspace/ecg-reference/ECG_Reference_Card.pdf

Your ECG Reference Card PDF is ready:

ECG Reference Card

PDF Document · PDF


Here's what's included across the pages:
Page 1 - Quick-Reference Summary Table All 14 ECG conditions side-by-side: key feature, rate, rhythm, P wave, and QRS characteristics at a glance.
Pages 2-4 - Individual Condition Cards (13 conditions) Each card includes a custom-drawn ECG waveform strip and a criteria table:
  1. STEMI - ST evolution stages + territory localisation
  2. NSTEMI/UA - Wellens' syndrome, De Winter T-waves
  3. Atrial Fibrillation - f-waves, irregular RR
  4. Atrial Flutter - sawtooth pattern, 2:1 block
  5. Ventricular Fibrillation - coarse vs fine VF
  6. Ventricular Tachycardia - Brugada/Vereckei criteria mentioned
  7. First-Degree AV Block - PR > 200 ms
  8. Second-Degree AV Block - Wenckebach vs Mobitz II
  9. Third-Degree (Complete) AV Block - P-QRS dissociation
  10. WPW Syndrome - delta wave, pathway localisation
  11. LVH - Sokolow-Lyon + Cornell voltage criteria
  12. LBBB - Sgarbossa criteria for MI in LBBB
  13. RBBB - rSR' pattern, S₁Q₃T₃
Bottom of Page 4 - Reference Tables
  • Electrolyte effects (hyperkalaemia, hypokalaemia, hypercalcaemia, long QT, digoxin)
  • Normal ECG intervals (PR, QRS, QTc, axes, paper speed, amplitude)
  • STEMI territory localisation map (artery, ST elevation leads, reciprocal leads)
This is a shared conversation. Sign in to Orris to start your own chat.