Make me pdf explaining all the basics things about ECG with diagram, also include how to identify different types of pathology in ecg( include ecg and identify steps). Use standard medical book as reference
mkdir -p /home/daytona/workspace/ecg-pdf && ls /home/daytona/workspace/ecg-pdf/
~/ecg-pdf/generate_ecg_pdf.py
"""
ECG Basics & Pathology PDF Generator
References: Harrison's Principles of Internal Medicine 22E (2025),
Guyton and Hall Textbook of Medical Physiology
"""
import math, io, urllib.request
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, Image, PageBreak, KeepTogether
)
from reportlab.graphics.shapes import Drawing, Path, Line, Rect, String, Circle, Polygon
from reportlab.graphics import renderPDF
from reportlab.graphics.renderPDF import drawToString
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus.flowables import Flowable
import reportlab.graphics.shapes as shapes
# ─── Color Palette ───────────────────────────────────────────────────────────
ECG_RED = colors.HexColor("#C0392B")
ECG_BLUE = colors.HexColor("#1A5276")
ECG_LIGHT = colors.HexColor("#D6EAF8")
ECG_YELLOW = colors.HexColor("#FEF9E7")
ECG_GREEN = colors.HexColor("#1E8449")
ECG_ORANGE = colors.HexColor("#E67E22")
ECG_PURPLE = colors.HexColor("#7D3C98")
GRID_COLOR = colors.HexColor("#FADBD8")
PAPER_BG = colors.HexColor("#FDFEFE")
DARK_TEXT = colors.HexColor("#1B2631")
BOX_BLUE = colors.HexColor("#EBF5FB")
BOX_GREEN = colors.HexColor("#EAFAF1")
BOX_ORANGE = colors.HexColor("#FEF5E7")
BOX_RED = colors.HexColor("#FDEDEC")
PAGE_W, PAGE_H = A4
MARGIN = 2 * cm
# ─── Styles ──────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
title_style = ParagraphStyle("Title",
fontName="Helvetica-Bold", fontSize=26, textColor=ECG_BLUE,
alignment=TA_CENTER, spaceAfter=6, leading=32)
subtitle_style = ParagraphStyle("Subtitle",
fontName="Helvetica", fontSize=13, textColor=colors.HexColor("#555555"),
alignment=TA_CENTER, spaceAfter=4)
h1_style = ParagraphStyle("H1",
fontName="Helvetica-Bold", fontSize=16, textColor=ECG_BLUE,
spaceBefore=14, spaceAfter=4, leading=20,
borderPad=(0,0,3,0))
h2_style = ParagraphStyle("H2",
fontName="Helvetica-Bold", fontSize=13, textColor=ECG_GREEN,
spaceBefore=10, spaceAfter=3, leading=17)
h3_style = ParagraphStyle("H3",
fontName="Helvetica-Bold", fontSize=11, textColor=ECG_ORANGE,
spaceBefore=6, spaceAfter=2, leading=14)
body_style = ParagraphStyle("Body",
fontName="Helvetica", fontSize=10, textColor=DARK_TEXT,
spaceBefore=2, spaceAfter=4, leading=15, alignment=TA_JUSTIFY)
bullet_style = ParagraphStyle("Bullet",
fontName="Helvetica", fontSize=10, textColor=DARK_TEXT,
spaceBefore=1, spaceAfter=2, leading=14,
leftIndent=14, bulletIndent=4)
caption_style = ParagraphStyle("Caption",
fontName="Helvetica-Oblique", fontSize=8.5, textColor=colors.HexColor("#555555"),
alignment=TA_CENTER, spaceBefore=2, spaceAfter=6)
ref_style = ParagraphStyle("Ref",
fontName="Helvetica-Oblique", fontSize=8, textColor=colors.HexColor("#888888"),
spaceBefore=1, spaceAfter=1, leading=11)
label_style = ParagraphStyle("Label",
fontName="Helvetica-Bold", fontSize=9, textColor=ECG_BLUE,
spaceBefore=0, spaceAfter=0)
# ─── ECG Drawing Helpers ─────────────────────────────────────────────────────
def draw_ecg_grid(d, x0, y0, width, height, small=5, large=25):
"""Draw standard ECG graph paper grid."""
d.add(Rect(x0, y0, width, height, fillColor=colors.HexColor("#FFF8F8"),
strokeColor=colors.HexColor("#CCCCCC"), strokeWidth=0.5))
# small squares
for i in range(0, int(width)+1, small):
d.add(Line(x0+i, y0, x0+i, y0+height,
strokeColor=GRID_COLOR, strokeWidth=0.3))
for j in range(0, int(height)+1, small):
d.add(Line(x0, y0+j, x0+width, y0+j,
strokeColor=GRID_COLOR, strokeWidth=0.3))
# large squares
for i in range(0, int(width)+1, large):
d.add(Line(x0+i, y0, x0+i, y0+height,
strokeColor=colors.HexColor("#F1948A"), strokeWidth=0.7))
for j in range(0, int(height)+1, large):
d.add(Line(x0, y0+j, x0+width, y0+j,
strokeColor=colors.HexColor("#F1948A"), strokeWidth=0.7))
def ecg_path(d, pts, x0, y0, baseline, scale_x=1.0, scale_y=1.0,
color=ECG_RED, width=1.5):
"""Draw ECG trace from list of (t, v) points."""
if len(pts) < 2:
return
p = Path(strokeColor=color, strokeWidth=width, fillColor=None)
px0 = x0 + pts[0][0]*scale_x
py0 = y0 + baseline + pts[0][1]*scale_y
p.moveTo(px0, py0)
for t, v in pts[1:]:
p.lineTo(x0 + t*scale_x, y0 + baseline + v*scale_y)
d.add(p)
def normal_beat(t_offset=0, duration=125, amp=1.0):
"""Return (t,v) pts for a normal sinus beat. t in 'steps', v in mm."""
pts = []
# Isoelectric baseline
for t in range(0, 20):
pts.append((t_offset+t, 0))
# P wave (upright, small)
pw = 12; ph = 3*amp
for t in range(pw):
v = ph * math.sin(math.pi * t / pw)
pts.append((t_offset+20+t, v))
# PR segment
for t in range(12):
pts.append((t_offset+32+t, 0))
# Q wave
pts.append((t_offset+44, 0))
pts.append((t_offset+46, -3*amp))
pts.append((t_offset+48, 0))
# R wave (tall)
pts.append((t_offset+50, 20*amp))
pts.append((t_offset+52, 0))
# S wave
pts.append((t_offset+54, -4*amp))
pts.append((t_offset+56, 0))
# ST segment (isoelectric)
for t in range(14):
pts.append((t_offset+57+t, 0))
# T wave (upright)
tw = 18; th = 6*amp
for t in range(tw):
v = th * math.sin(math.pi * t / tw)
pts.append((t_offset+71+t, v))
# Baseline after
for t in range(duration-(71+tw)):
pts.append((t_offset+71+tw+t, 0))
return pts
# ─── Custom Flowables ─────────────────────────────────────────────────────────
class SectionHeader(Flowable):
def __init__(self, text, color=ECG_BLUE, width=None):
Flowable.__init__(self)
self.text = text
self.color = color
self._width = width or (PAGE_W - 2*MARGIN)
def wrap(self, aw, ah):
self.width = self._width
return self.width, 22
def draw(self):
c = self.canv
c.setFillColor(self.color)
c.roundRect(0, 0, self.width, 20, 4, fill=1, stroke=0)
c.setFillColor(colors.white)
c.setFont("Helvetica-Bold", 13)
c.drawString(10, 5, self.text)
class InfoBox(Flowable):
def __init__(self, title, lines, bg=BOX_BLUE, title_color=ECG_BLUE, width=None):
Flowable.__init__(self)
self.title = title
self.lines = lines
self.bg = bg
self.tc = title_color
self._width = width or (PAGE_W - 2*MARGIN)
self._height = 18 + len(lines)*14 + 6
def wrap(self, aw, ah):
return self._width, self._height
def draw(self):
c = self.canv
c.setFillColor(self.bg)
c.roundRect(0, 0, self._width, self._height, 5, fill=1, stroke=0)
c.setFillColor(self.tc)
c.setFont("Helvetica-Bold", 9.5)
c.drawString(8, self._height-13, self.title)
c.setFillColor(DARK_TEXT)
c.setFont("Helvetica", 9)
for i, ln in enumerate(self.lines):
c.drawString(14, self._height-26-(i*14), ln)
# ─── Normal Sinus ECG Drawing ─────────────────────────────────────────────────
def make_normal_ecg(w=480, h=100, n_beats=3, title="Normal Sinus Rhythm"):
d = Drawing(w, h+30)
# Title
d.add(String(w/2, h+20, title, textAnchor='middle',
fontName='Helvetica-Bold', fontSize=9, fillColor=ECG_BLUE))
draw_ecg_grid(d, 0, 0, w, h)
# baseline
bl = h//2
all_pts = []
beat_len = 125
for b in range(n_beats):
all_pts += normal_beat(t_offset=b*beat_len, duration=beat_len)
# add trailing baseline
for t in range(n_beats*beat_len, int(w)+5):
all_pts.append((t, 0))
scale_x = w / (n_beats * beat_len + 10)
ecg_path(d, all_pts, 0, 0, bl, scale_x=scale_x, scale_y=1.8, color=ECG_RED)
return d
# ─── Pathology ECG Drawings ───────────────────────────────────────────────────
def make_stemi_ecg(w=480, h=100):
"""Anterior STEMI: ST elevation in V1-V4, pathological Q waves."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "STEMI - ST Elevation Myocardial Infarction",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_RED))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 150
pts = []
for b in range(3):
t0 = b * beat_len
# Baseline
for t in range(15): pts.append((t0+t, 0))
# P wave
for t in range(10):
pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
# PR
for t in range(10): pts.append((t0+25+t, 0))
# Pathological Q
pts += [(t0+35, 0),(t0+37, -7),(t0+39, 0)]
# R (smaller than normal — infarction)
pts += [(t0+41, 8),(t0+43, 0)]
# ST Elevation — tombstone pattern
pts += [(t0+44, 6),(t0+50, 8),(t0+58, 6),(t0+64, 4)]
# T wave inverted merged with elevated ST
for t in range(14):
pts.append((t0+64+t, 4 - 6*math.sin(math.pi*t/14)))
# baseline
for t in range(beat_len-(64+14)):
pts.append((t0+78+t, 0))
# extend
for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(3*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.6, color=ECG_RED)
# annotation
d.add(String(w*0.5, bl+28, "ST Elevation", textAnchor='middle',
fontName='Helvetica-Bold', fontSize=7.5, fillColor=ECG_RED))
d.add(String(w*0.15, bl-28, "Pathological Q", textAnchor='middle',
fontName='Helvetica-Bold', fontSize=7.5, fillColor=ECG_PURPLE))
return d
def make_afib_ecg(w=480, h=100):
"""Atrial Fibrillation: no P waves, irregular RR, fibrillatory baseline."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Atrial Fibrillation (AF)",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_BLUE))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
import random; random.seed(42)
pts = []
t = 0
# fibrillatory baseline
while t < w/1.0:
pts.append((t, 1.2*math.sin(2*math.pi*t/4.5 + random.uniform(0,1))))
t += 0.5
# overlay irregular QRS complexes
qrs_times = [20, 62, 97, 138, 167, 210, 255, 290, 330, 370, 410, 445]
qrs_pts = []
for qt in qrs_times:
scale_x = 0.95
qrs_pts += [(qt-2, 0),(qt, -3),(qt+2, 0),(qt+3, 18),(qt+5, -4),(qt+7, 0)]
# merge
all_pts = pts + qrs_pts
all_pts.sort(key=lambda x: x[0])
sx = w/(w/1.0)
ecg_path(d, all_pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_BLUE)
d.add(String(w*0.45, bl+30, "No P waves; irregular RR intervals",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_BLUE))
d.add(String(w*0.45, bl-30, "Fibrillatory baseline",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_BLUE))
return d
def make_heart_block_ecg(w=480, h=100, block_type="complete"):
"""Complete (3rd degree) heart block: P waves independent of QRS."""
d = Drawing(w, h+30)
if block_type == "complete":
ttl = "Complete (3rd Degree) Heart Block"
clr = ECG_PURPLE
else:
ttl = "2nd Degree Heart Block (Mobitz II)"
clr = ECG_ORANGE
d.add(String(w/2, h+20, ttl, textAnchor='middle',
fontName='Helvetica-Bold', fontSize=9, fillColor=clr))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
pts = []
# P waves at ~70 bpm (atrial rate)
p_times = list(range(10, int(w/0.9), 22))
for pt in p_times:
for t in range(10):
pts.append((pt+t, 2.5*math.sin(math.pi*t/10)))
pts.sort(key=lambda x: x[0])
# Add baseline
baseline = [(t, 0) for t in range(0, int(w/0.9)+5, 1)]
merged = baseline + pts
merged.sort(key=lambda x: x[0])
ecg_path(d, merged, 0, 0, bl, scale_x=0.9, scale_y=1.4, color=ECG_ORANGE)
# QRS at slow escape rate ~35 bpm (wide, ventricular)
qrs_times = [40, 98, 156, 214, 272, 330, 388, 446]
qrs_pts = []
for qt in qrs_times:
qrs_pts += [(qt, 0),(qt+2, -3),(qt+4, 0),(qt+6, 15),(qt+10, -5),(qt+14, 0)]
qrs_pts.sort(key=lambda x: x[0])
ecg_path(d, qrs_pts, 0, 0, bl, scale_x=0.9, scale_y=1.4, color=ECG_PURPLE)
d.add(String(w*0.35, bl+28, "P waves (independent)",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_ORANGE))
d.add(String(w*0.65, bl-30, "Wide QRS (escape rhythm)",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_PURPLE))
return d
def make_lbbb_ecg(w=480, h=100):
"""Left Bundle Branch Block: wide QRS >120ms, broad notched R in V5/V6."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Left Bundle Branch Block (LBBB)",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_GREEN))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 150
pts = []
for b in range(3):
t0 = b * beat_len
# Isoelectric
for t in range(15): pts.append((t0+t, 0))
# P wave
for t in range(10):
pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
# PR
for t in range(10): pts.append((t0+25+t, 0))
# Wide QRS: broad notched R (no Q, dominant R, no S)
pts += [(t0+35, 0),(t0+38, 5),(t0+41, 8),(t0+43, 12),(t0+46, 14),
(t0+49, 12),(t0+52, 9),(t0+55, 12),(t0+58, 14),(t0+61, 12),
(t0+64, 8),(t0+67, 4),(t0+70, 0)]
# ST depression + T wave inversion
pts += [(t0+71, -3),(t0+74, -4),(t0+78, -4)]
for t in range(14):
pts.append((t0+78+t, -3.5*math.sin(math.pi*t/14)))
for t in range(beat_len-(78+14)):
pts.append((t0+92+t, 0))
for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(3*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_GREEN)
d.add(String(w*0.5, bl+28, "Broad notched R (William's pattern)",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_GREEN))
d.add(String(w*0.5, bl-28, "T inversion; QRS >120 ms",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_GREEN))
return d
def make_vt_ecg(w=480, h=100):
"""Ventricular Tachycardia: fast, wide, regular QRS, no P waves."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Ventricular Tachycardia (VT)",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_RED))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 55
pts = []
for b in range(8):
t0 = b * beat_len
pts += [(t0, 0),(t0+3, -4),(t0+5, 0),(t0+7, 16),(t0+11, -5),(t0+15, -6),
(t0+20, -2),(t0+25, 1),(t0+30, 0)]
for t in range(30, beat_len): pts.append((t0+t, 0))
for t in range(8*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(8*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_RED)
d.add(String(w*0.5, bl+28, "Fast rate (~150-200 bpm), wide QRS, no P waves",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_RED))
return d
def make_hyperkalemia_ecg(w=480, h=100):
"""Hyperkalemia: tall peaked T waves, wide QRS, loss of P."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Hyperkalemia ECG Changes",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_ORANGE))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 140
pts = []
for b in range(3):
t0 = b * beat_len
for t in range(18): pts.append((t0+t, 0))
# Tiny P wave (flat/absent in severe)
for t in range(8):
pts.append((t0+18+t, 1.0*math.sin(math.pi*t/8)))
for t in range(10): pts.append((t0+26+t, 0))
# Wide QRS
pts += [(t0+36, 0),(t0+38,-4),(t0+40,0),(t0+42,16),(t0+46,-6),(t0+52,0)]
# Tall peaked T wave (tented)
for t in range(18):
v = 12 * math.sin(math.pi*t/18)
pts.append((t0+54+t, v))
# baseline
for t in range(beat_len-(54+18)):
pts.append((t0+72+t, 0))
for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(3*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_ORANGE)
d.add(String(w*0.5, bl+32, "Tall peaked (tented) T waves",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_ORANGE))
return d
def make_wpw_ecg(w=480, h=100):
"""Wolff-Parkinson-White: short PR, delta wave, wide QRS."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Wolff-Parkinson-White (WPW) Syndrome",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_PURPLE))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 130
pts = []
for b in range(3):
t0 = b * beat_len
for t in range(12): pts.append((t0+t, 0))
# P wave
for t in range(10):
pts.append((t0+12+t, 2.5*math.sin(math.pi*t/10)))
# SHORT PR (<120ms)
for t in range(5): pts.append((t0+22+t, 0))
# Delta wave (slurred upstroke)
pts += [(t0+27, 0),(t0+30, 3),(t0+33, 7),(t0+36, 12),(t0+38,15),
(t0+40,18),(t0+42, 16),(t0+44,12),(t0+47, 6),(t0+50, 0)]
# S wave absent, broad QRS
pts += [(t0+51, -3),(t0+54, 0)]
# ST/T
for t in range(15):
pts.append((t0+55+t, -4*math.sin(math.pi*t/15)))
for t in range(beat_len-(55+15)):
pts.append((t0+70+t, 0))
for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(3*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.4, color=ECG_PURPLE)
d.add(String(w*0.5, bl+28, "Short PR + Delta wave (slurred QRS onset)",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_PURPLE))
return d
def make_long_qt_ecg(w=480, h=100):
"""Long QT syndrome: prolonged QT interval."""
d = Drawing(w, h+30)
d.add(String(w/2, h+20, "Long QT Syndrome",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_PURPLE))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2
beat_len = 160
pts = []
for b in range(3):
t0 = b * beat_len
for t in range(15): pts.append((t0+t, 0))
for t in range(10):
pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
for t in range(10): pts.append((t0+25+t, 0))
pts += [(t0+35, 0),(t0+37,-3),(t0+39,0),(t0+41,18),(t0+43,0)]
pts += [(t0+44,-4),(t0+46,0)]
# Prolonged ST segment before T
for t in range(28): pts.append((t0+47+t, 0))
# T wave (late, broad)
for t in range(22):
pts.append((t0+75+t, 5*math.sin(math.pi*t/22)))
for t in range(beat_len-(75+22)):
pts.append((t0+97+t, 0))
for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
sx = w/(3*beat_len+5)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_PURPLE)
# Draw QT interval arrow
# annotation
d.add(String(w*0.5, bl-30, "Prolonged QT interval (> 450 ms men / 460 ms women)",
textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
fillColor=ECG_PURPLE))
return d
# ─── Annotated Normal Beat Drawing ───────────────────────────────────────────
def make_labeled_beat(w=420, h=130):
d = Drawing(w, h+40)
d.add(String(w/2, h+30, "ECG Waveform Components (Labeled)",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=10, fillColor=ECG_BLUE))
draw_ecg_grid(d, 0, 0, w, h)
bl = h//2 - 10
beat_len = 200
pts = normal_beat(t_offset=20, duration=beat_len)
pts = [(t, v) for t, v in pts if t < w]
pts = [(t, 0) for t in range(20)] + pts
sx = w / (beat_len + 40)
ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=2.2, color=ECG_RED, width=2)
# Scale factor for annotation positions
def tx(t): return t * sx
sy = 2.2
# Label P wave
p_peak_x = tx(26)
p_peak_y = bl + 3 * sy
d.add(Line(p_peak_x, p_peak_y+3, p_peak_x, p_peak_y+20,
strokeColor=ECG_BLUE, strokeWidth=0.8))
d.add(String(p_peak_x, p_peak_y+22, "P wave", textAnchor='middle',
fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_BLUE))
# Label QRS
qrs_x = tx(72)
qrs_y = bl + 20*sy
d.add(Line(qrs_x, qrs_y+3, qrs_x, qrs_y+18,
strokeColor=ECG_RED, strokeWidth=0.8))
d.add(String(qrs_x, qrs_y+20, "QRS", textAnchor='middle',
fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_RED))
# Label T wave
t_peak_x = tx(100)
t_peak_y = bl + 6*sy
d.add(Line(t_peak_x, t_peak_y+3, t_peak_x, t_peak_y+16,
strokeColor=ECG_GREEN, strokeWidth=0.8))
d.add(String(t_peak_x, t_peak_y+18, "T wave", textAnchor='middle',
fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_GREEN))
# Label intervals below baseline
# PR interval
pr_start = tx(20); pr_end = tx(69)
d.add(Line(pr_start, bl-30, pr_end, bl-30,
strokeColor=ECG_BLUE, strokeWidth=1))
d.add(Line(pr_start, bl-30, pr_start, bl-26, strokeColor=ECG_BLUE, strokeWidth=1))
d.add(Line(pr_end, bl-30, pr_end, bl-26, strokeColor=ECG_BLUE, strokeWidth=1))
d.add(String((pr_start+pr_end)/2, bl-42, "PR interval (120-200 ms)",
textAnchor='middle', fontName='Helvetica', fontSize=7,
fillColor=ECG_BLUE))
# QRS interval
qrs_start = tx(66); qrs_end = tx(79)
d.add(Line(qrs_start, bl-18, qrs_end, bl-18,
strokeColor=ECG_RED, strokeWidth=1))
d.add(Line(qrs_start, bl-18, qrs_start, bl-14, strokeColor=ECG_RED, strokeWidth=1))
d.add(Line(qrs_end, bl-18, qrs_end, bl-14, strokeColor=ECG_RED, strokeWidth=1))
d.add(String((qrs_start+qrs_end)/2, bl-14, "QRS (≤110 ms)",
textAnchor='middle', fontName='Helvetica', fontSize=7,
fillColor=ECG_RED))
# QT interval
qt_start = tx(66); qt_end = tx(125)
d.add(Line(qt_start, bl-50, qt_end, bl-50,
strokeColor=ECG_PURPLE, strokeWidth=1))
d.add(Line(qt_start, bl-50, qt_start, bl-46, strokeColor=ECG_PURPLE, strokeWidth=1))
d.add(Line(qt_end, bl-50, qt_end, bl-46, strokeColor=ECG_PURPLE, strokeWidth=1))
d.add(String((qt_start+qt_end)/2, bl-62, "QT interval",
textAnchor='middle', fontName='Helvetica', fontSize=7,
fillColor=ECG_PURPLE))
return d
# ─── Leads Diagram ───────────────────────────────────────────────────────────
def make_leads_diagram(w=420, h=140):
d = Drawing(w, h)
cx, cy = w//2, h//2 + 10
# Body outline
d.add(Rect(w*0.3, h*0.1, w*0.4, h*0.85,
strokeColor=colors.HexColor("#AAAAAA"), strokeWidth=0.5,
fillColor=colors.HexColor("#FFF0F0"), rx=20, ry=20))
# Electrode positions
electrodes = {
"RA": (w*0.2, h*0.75), "LA": (w*0.8, h*0.75),
"RL": (w*0.3, h*0.15), "LL": (w*0.7, h*0.15),
"V1": (w*0.48, h*0.62), "V2": (w*0.52, h*0.58),
"V3": (w*0.56, h*0.52), "V4": (w*0.60, h*0.47),
"V5": (w*0.65, h*0.42), "V6": (w*0.70, h*0.38),
}
colors_map = {"RA":"#E74C3C","LA":"#2980B9","RL":"#27AE60","LL":"#F39C12",
"V1":"#8E44AD","V2":"#8E44AD","V3":"#8E44AD",
"V4":"#8E44AD","V5":"#8E44AD","V6":"#8E44AD"}
for name, (ex, ey) in electrodes.items():
c = colors.HexColor(colors_map[name])
d.add(Circle(ex, ey, 7, fillColor=c, strokeColor=colors.white, strokeWidth=1))
d.add(String(ex, ey-3, name, textAnchor='middle',
fontName='Helvetica-Bold', fontSize=6, fillColor=colors.white))
# Lead I line
d.add(Line(electrodes["RA"][0]+7, electrodes["RA"][1],
electrodes["LA"][0]-7, electrodes["LA"][1],
strokeColor=colors.HexColor("#E74C3C"), strokeWidth=0.8, strokeDashArray=[3,2]))
d.add(String(w/2, h*0.85, "Lead I", textAnchor='middle',
fontName='Helvetica', fontSize=7, fillColor=ECG_BLUE))
d.add(String(w/2, h+5, "Standard 12-Lead Electrode Placement",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_BLUE))
return d
# ─── Axis Diagram ─────────────────────────────────────────────────────────────
def make_axis_diagram(w=220, h=220):
d = Drawing(w, h+20)
d.add(String(w/2, h+10, "QRS Axis (Frontal Plane)",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=9, fillColor=ECG_BLUE))
cx, cy = w//2, h//2
# Background sectors
sectors = [
((-30, 90), colors.HexColor("#EAFAF1"), "Normal"),
((-90, -30), colors.HexColor("#FEF9E7"), "LAD"),
((90, 180), colors.HexColor("#FEF5E7"), "RAD"),
((180, 270), colors.HexColor("#FDEDEC"), "Extreme"),
]
r = 90
for (a1, a2), col, label in sectors:
pts = [cx, cy]
steps = 20
for i in range(steps+1):
angle = math.radians(a1 + (a2-a1)*i/steps)
pts.extend([cx + r*math.cos(angle), cy + r*math.sin(angle)])
pts.extend([cx, cy])
poly_pts = [(pts[i*2], pts[i*2+1]) for i in range(len(pts)//2)]
d.add(Polygon(pts[2:], fillColor=col, strokeColor=None))
# Circle
d.add(Circle(cx, cy, r, fillColor=None, strokeColor=colors.HexColor("#BBBBBB"),
strokeWidth=1))
# Axis lines (lead directions)
leads = [
(0, "I", ECG_BLUE), (60, "aVF", ECG_BLUE), (-60, "aVL", ECG_BLUE),
(90, "aVF", None), (120, "III", ECG_BLUE), (-120, "aVR", ECG_BLUE),
]
for angle_deg, lbl, col in leads:
angle = math.radians(angle_deg)
x1 = cx - r*math.cos(angle)
y1 = cy - r*math.sin(angle)
x2 = cx + r*math.cos(angle)
y2 = cy + r*math.sin(angle)
d.add(Line(x1, y1, x2, y2, strokeColor=colors.HexColor("#CCCCCC"),
strokeWidth=0.7))
# Normal axis arrow
mean_angle = math.radians(60) # typical normal ~60°
d.add(Line(cx, cy, cx + 70*math.cos(mean_angle), cy + 70*math.sin(mean_angle),
strokeColor=ECG_GREEN, strokeWidth=2.5))
# Labels
d.add(String(cx+r+5, cy, "0°", fontName='Helvetica-Bold', fontSize=7,
fillColor=ECG_BLUE))
d.add(String(cx-r-18, cy, "±180°", fontName='Helvetica-Bold', fontSize=7,
fillColor=ECG_BLUE))
d.add(String(cx-5, cy+r+5, "+90°", fontName='Helvetica-Bold', fontSize=7,
fillColor=ECG_BLUE))
d.add(String(cx-5, cy-r-12, "-90°", fontName='Helvetica-Bold', fontSize=7,
fillColor=ECG_BLUE))
# Zone labels
d.add(String(cx+15, cy+35, "NORMAL\n-30°to+90°",
fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_GREEN))
d.add(String(cx-75, cy+40, "LAD",
fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_ORANGE))
d.add(String(cx+35, cy-40, "RAD",
fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_RED))
return d
# ─── Build the PDF ────────────────────────────────────────────────────────────
def build_pdf(out_path):
doc = SimpleDocTemplate(out_path, pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=MARGIN,
title="ECG Basics & Pathology Reference",
author="Medical Reference - Harrison's & Guyton")
story = []
W = PAGE_W - 2*MARGIN
# ── Cover ──────────────────────────────────────────────────────────────
story.append(Spacer(1, 1.5*cm))
# Title block
title_drawing = Drawing(W, 80)
title_drawing.add(Rect(0, 0, W, 80, fillColor=ECG_BLUE, strokeColor=None, rx=8, ry=8))
title_drawing.add(String(W/2, 52, "ECG Basics & Clinical Pathology",
textAnchor='middle', fontName='Helvetica-Bold',
fontSize=22, fillColor=colors.white))
title_drawing.add(String(W/2, 30, "A Comprehensive Reference Guide",
textAnchor='middle', fontName='Helvetica',
fontSize=13, fillColor=colors.HexColor("#AED6F1")))
title_drawing.add(String(W/2, 12, "Based on Harrison's Principles of Internal Medicine 22E (2025) & Guyton and Hall Textbook of Medical Physiology",
textAnchor='middle', fontName='Helvetica-Oblique',
fontSize=7.5, fillColor=colors.HexColor("#85C1E9")))
story.append(title_drawing)
# Cover ECG trace
story.append(Spacer(1, 0.4*cm))
cover_ecg = make_normal_ecg(w=int(W), h=80, n_beats=5,
title="")
story.append(cover_ecg)
story.append(Spacer(1, 0.3*cm))
# Table of contents summary
toc_data = [
["Section", "Topic"],
["1", "Fundamentals of Electrocardiography"],
["2", "ECG Waveforms and Intervals"],
["3", "The 12-Lead System"],
["4", "Cardiac Axis"],
["5", "Systematic ECG Interpretation"],
["6", "Pathology: STEMI & Ischaemia"],
["7", "Pathology: Arrhythmias (AF, VT)"],
["8", "Pathology: Conduction Blocks"],
["9", "Pathology: WPW & Long QT"],
["10", "Pathology: Electrolyte Disturbances"],
]
toc_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,0), 9),
('FONTNAME', (0,1), (-1,-1), 'Helvetica'),
('FONTSIZE', (0,1), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 8),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
])
toc_table = Table(toc_data, colWidths=[1*cm, W-1*cm])
toc_table.setStyle(toc_style)
story.append(toc_table)
story.append(PageBreak())
# ── Section 1: Fundamentals ───────────────────────────────────────────
story.append(SectionHeader("SECTION 1 — Fundamentals of Electrocardiography"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("What is an ECG?", h2_style))
story.append(Paragraph(
"When a cardiac impulse passes through the heart, electrical current spreads from the heart "
"into adjacent tissues and to the body surface. If electrodes are placed on the skin on "
"opposite sides of the heart, the electrical potentials generated can be recorded as an "
"<b>electrocardiogram (ECG)</b>. The ECG provides a graphic representation of the heart's "
"electrical activity over time.", body_style))
story.append(Paragraph("Cardiac Conduction System", h2_style))
story.append(Paragraph(
"The stimulus initiating the normal heartbeat originates in the <b>sinoatrial (SA) node</b>, "
"which possesses spontaneous automaticity. Depolarization spreads through the atria, then passes "
"through the <b>AV node</b> (where it is delayed to allow ventricular filling), then into the "
"<b>Bundle of His</b>, which splits into the <b>right and left bundle branches</b>, and finally "
"to the ventricular myocardium via <b>Purkinje fibers</b>.", body_style))
# Conduction table
cond_data = [
["Structure", "Function", "Normal Velocity"],
["SA Node", "Pacemaker — initiates impulse (60-100 bpm)", "~1 m/s"],
["AV Node", "Gatekeeper — delays impulse (PR interval)", "~0.05 m/s"],
["Bundle of His", "Transmits impulse to ventricles", "~1 m/s"],
["Bundle Branches (R&L)", "Rapid spread to respective ventricles", "~2 m/s"],
["Purkinje Fibers", "Final fast delivery to myocardium", "~4 m/s"],
]
ct_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
])
ct = Table(cond_data, colWidths=[W*0.28, W*0.52, W*0.20])
ct.setStyle(ct_style)
story.append(ct)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<i>Reference: Harrison's Principles of Internal Medicine 22E (2025), Chapter 247; "
"Guyton and Hall Textbook of Medical Physiology, Chapter 11</i>", ref_style))
# ── Section 2: Waveforms & Intervals ─────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 2 — ECG Waveforms and Intervals", color=ECG_GREEN))
story.append(Spacer(1, 0.3*cm))
labeled_beat = make_labeled_beat(w=int(W), h=130)
story.append(labeled_beat)
story.append(Paragraph("Figure 1. Labeled ECG waveform components with key intervals. "
"(Adapted from Harrison's 22E, Figure 247-2; Guyton & Hall, Ch.11)", caption_style))
story.append(Spacer(1, 0.3*cm))
# Waveform table
wave_data = [
["Wave/Segment", "Origin", "Normal Duration", "Normal Amplitude"],
["P wave", "Atrial depolarization", "80-100 ms", "<2.5 mm (height)\n<120 ms (width)"],
["PR interval", "Atrial depol. + AV node delay", "120-200 ms", "Isoelectric"],
["QRS complex", "Ventricular depolarization", "≤110 ms", "Variable"],
["ST segment", "Ventricular plateau (phase 2)", "Isoelectric", "±1 mm from baseline"],
["T wave", "Ventricular repolarization", "150-250 ms", "Positive in most leads"],
["QT interval", "Ventricular depol. + repol.", "QTc ≤450 ms (M)\n≤460 ms (F)", "N/A"],
["U wave", "Purkinje repolarization", "~200 ms", "<1 mm (small, positive)"],
]
wt_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
])
wt = Table(wave_data, colWidths=[W*0.22, W*0.30, W*0.22, W*0.26])
wt.setStyle(wt_style)
story.append(wt)
story.append(Spacer(1, 0.2*cm))
# Info boxes for key facts
story.append(InfoBox("Key Fact: QRS Formation",
["• First phase: Septal depolarization (left → right, anteriorly)",
"• Second phase: Ventricular depolarization (left ventricle dominates, points left & posteriorly)",
"• V1: Small r wave then deep S wave (rS pattern)",
"• V6: Small q wave then tall R wave (qR pattern)",
"• R-wave progression: R grows from V1→V6 (transition at V3 or V4)"],
bg=BOX_BLUE, title_color=ECG_BLUE))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("<i>Reference: Harrison's 22E, p.1912-1913</i>", ref_style))
# ── Section 3: 12-Lead System ─────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 3 — The 12-Lead ECG System", color=ECG_ORANGE))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Lead Groups", h2_style))
story.append(Paragraph(
"The 12 conventional ECG leads are divided into two groups: <b>six limb (extremity) leads</b> "
"that record potentials in the frontal plane, and <b>six chest (precordial) leads</b> that record "
"potentials in the horizontal plane. Each lead acts like a different 'camera angle' looking at the "
"same cardiac electrical events from a different spatial orientation.", body_style))
leads_data = [
["Lead Group", "Leads", "View / Territory"],
["Inferior", "II, III, aVF", "Inferior wall (right coronary artery territory)"],
["Lateral", "I, aVL, V5, V6", "Lateral wall (left circumflex territory)"],
["Anterior/Septal", "V1, V2", "Septal/anterior wall (LAD territory)"],
["Anterior", "V3, V4", "Anterior wall (LAD territory)"],
["Right heart", "aVR", "Right atrium / cavity; reciprocal of lateral leads"],
["Extended", "V3R, V4R", "Right ventricle (add-on leads for RV infarction)"],
]
lt_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_ORANGE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
])
lt = Table(leads_data, colWidths=[W*0.20, W*0.25, W*0.55])
lt.setStyle(lt_style)
story.append(lt)
story.append(Spacer(1, 0.4*cm))
# Normal sinus ECG
norm_ecg = make_normal_ecg(w=int(W), h=90, n_beats=4, title="Normal Sinus Rhythm (Rate 60-100 bpm)")
story.append(norm_ecg)
story.append(Paragraph("Figure 2. Normal sinus rhythm: upright P wave before each QRS, regular rhythm, "
"normal intervals.", caption_style))
# ── Section 4: Cardiac Axis ────────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 4 — Cardiac Axis", color=ECG_PURPLE))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"The <b>QRS axis</b> describes the mean direction of ventricular depolarization in the frontal plane. "
"Normally the QRS axis ranges from <b>-30° to +100°</b>. Since the cardiac depolarization and "
"repolarization wavefronts have direction and magnitude, they can be represented by vectors. "
"The axis is determined by examining which limb leads have the tallest positive and negative deflections.",
body_style))
# Axis table + diagram side by side
axis_diag = make_axis_diagram(w=200, h=200)
axis_data = [
["Axis", "Degrees", "Common Causes"],
["Normal", "-30° to +90°", "Normal variant"],
["Left axis\ndeviation (LAD)", "-30° to -90°", "LBBB, inferior MI,\nleft anterior\nfascicular block"],
["Right axis\ndeviation (RAD)", "+90° to +180°", "RVH, RBBB,\nLateral MI,\npulmonary embolism"],
["Extreme\n(NW axis)", "±180° to -90°", "VT, emphysema,\nelectrode reversal"],
]
at_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
])
at = Table(axis_data, colWidths=[W*0.22, W*0.18, W*0.28])
at.setStyle(at_style)
combo_table = Table([[axis_diag, at]], colWidths=[W*0.40, W*0.60])
combo_table.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 4),
]))
story.append(combo_table)
story.append(Spacer(1, 0.2*cm))
story.append(InfoBox("How to Determine Axis Quickly",
["1. Find the lead where QRS is most isoelectric (equal + and - deflections) — axis is perpendicular to it",
"2. Lead I positive + aVF positive → Normal axis",
"3. Lead I positive + aVF negative → Left axis deviation",
"4. Lead I negative + aVF positive → Right axis deviation",
"5. Lead I negative + aVF negative → Extreme (northwest) axis"],
bg=colors.HexColor("#F5EEF8"), title_color=ECG_PURPLE))
# ── Section 5: Systematic Interpretation ─────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 5 — Systematic ECG Interpretation", color=ECG_BLUE))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Use a systematic approach every time to avoid missing findings. "
"The following 8-step method ensures comprehensive analysis.", body_style))
steps_data = [
["Step", "Parameter", "What to Check", "Normal Values"],
["1", "Rate", "Count R-R intervals; divide 300 by # large squares", "60-100 bpm"],
["2", "Rhythm", "Regular? P before every QRS? QRS after every P?", "Regular sinus"],
["3", "P wave", "Present? Upright in I, II? Same morphology?", "Present, ≤2.5mm, ≤110ms"],
["4", "PR interval", "Measure P onset → QRS onset", "120-200 ms"],
["5", "QRS complex", "Width? Morphology? Q waves? R progression?", "≤110 ms, narrow"],
["6", "Axis", "Lead I & aVF positivity (see Section 4)", "-30° to +90°"],
["7", "ST segment", "Elevation or depression vs. baseline?", "Isoelectric ±1mm"],
["8", "T wave & QT", "Upright or inverted? QTc prolonged?", "Upright; QTc <450ms"],
]
st_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('ALIGN', (0,0), (1,-1), 'CENTER'),
])
st = Table(steps_data, colWidths=[W*0.07, W*0.16, W*0.47, W*0.30])
st.setStyle(st_style)
story.append(st)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Heart Rate Calculation Methods", h2_style))
rate_data = [
["Method", "How", "Best Used For"],
["300 ÷ large squares", "Count large squares between R-R; divide 300", "Regular rhythms (quick)"],
["1500 ÷ small squares", "Count small squares between R-R; divide 1500", "Regular rhythms (precise)"],
["6-second strip", "Count QRS complexes in 6-sec strip × 10", "Irregular rhythms (AF)"],
]
rr_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
])
rr = Table(rate_data, colWidths=[W*0.30, W*0.40, W*0.30])
rr.setStyle(rr_style)
story.append(rr)
# ── Section 6: STEMI ──────────────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 6 — Pathology: ST-Elevation MI (STEMI)", color=ECG_RED))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("STEMI: ECG Findings", h2_style))
story.append(Paragraph(
"Acute STEMI produces a characteristic sequence of ECG changes. "
"The diagnosis of STEMI requires <b>ST-segment elevation ≥1mm in ≥2 contiguous leads</b> "
"(or ≥2mm in V1-V3) in the context of symptoms. "
"Reciprocal ST depression appears in leads opposite the infarct territory. "
"<b>Pathological Q waves</b> (>40 ms wide or >25% of R wave height) develop over hours-days, "
"indicating irreversible myocardial necrosis.", body_style))
stemi_ecg = make_stemi_ecg(w=int(W), h=90)
story.append(stemi_ecg)
story.append(Paragraph("Figure 3. STEMI: Tombstone ST elevation, pathological Q waves, T-wave changes.",
caption_style))
stemi_steps = [
["Step", "Finding", "Significance"],
["1", "ST elevation ≥1mm in ≥2 contiguous leads", "Active transmural ischemia/infarction"],
["2", "Hyperacute T waves (tall, peaked)", "Earliest sign — often missed (minutes)"],
["3", "Pathological Q waves (>40ms or >25% R)", "Necrosis — develops over hours"],
["4", "T-wave inversion", "Evolving phase (hours to days after onset)"],
["5", "Reciprocal ST depression", "Confirms true ST elevation (not spurious)"],
["6", "Loss of R-wave progression", "Prior anterior infarction"],
]
ss_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_RED),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
])
ss = Table(stemi_steps, colWidths=[W*0.07, W*0.48, W*0.45])
ss.setStyle(ss_style)
story.append(ss)
story.append(Spacer(1, 0.2*cm))
# Territory table
story.append(Paragraph("STEMI Territory Localization", h3_style))
terr_data = [
["Territory", "Leads with ST Elevation", "Culprit Artery"],
["Anterior", "V1-V4", "Left Anterior Descending (LAD)"],
["Lateral", "I, aVL, V5-V6", "Left Circumflex (LCx)"],
["Inferior", "II, III, aVF", "Right Coronary Artery (RCA) 80%"],
["Posterior", "Tall R in V1-V2, ST depression V1-V3", "RCA or LCx"],
["Right Ventricle", "ST elevation V3R-V4R + inferior changes", "Proximal RCA"],
["Anterolateral", "V1-V6, I, aVL", "Large LAD or left main"],
]
tt_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#922B21")),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
])
tt = Table(terr_data, colWidths=[W*0.22, W*0.40, W*0.38])
tt.setStyle(tt_style)
story.append(tt)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<i>Reference: Harrison's 22E pp.1915-1917, Goldberger's Clinical Electrocardiography 10e; "
"Tintinalli's Emergency Medicine</i>", ref_style))
# ── Section 7: Arrhythmias ─────────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 7 — Pathology: Arrhythmias", color=ECG_BLUE))
story.append(Spacer(1, 0.3*cm))
# AF
story.append(Paragraph("Atrial Fibrillation (AF)", h2_style))
story.append(Paragraph(
"AF is the most common sustained cardiac arrhythmia, affecting 1-2% of the population. "
"It results from chaotic, rapid atrial depolarizations (350-600/min) causing an <b>irregularly "
"irregular ventricular response</b>. The atria quiver rather than contract.", body_style))
afib_ecg = make_afib_ecg(w=int(W), h=90)
story.append(afib_ecg)
story.append(Paragraph("Figure 4. Atrial fibrillation: absent P waves, irregular baseline, "
"irregularly irregular QRS complexes.", caption_style))
af_data = [
["Feature", "Finding"],
["P waves", "Absent — replaced by irregular fibrillatory baseline ('f' waves, 350-600/min)"],
["RR intervals", "Irregularly irregular (no two identical)"],
["QRS complex", "Usually narrow (<110 ms) unless aberrant conduction"],
["Ventricular rate", "Uncontrolled: 100-180 bpm; controlled: 60-100 bpm"],
["Baseline", "Undulating, coarse or fine fibrillatory activity"],
]
af_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
])
af = Table(af_data, colWidths=[W*0.25, W*0.75])
af.setStyle(af_style)
story.append(af)
story.append(Spacer(1, 0.3*cm))
# VT
story.append(Paragraph("Ventricular Tachycardia (VT)", h2_style))
story.append(Paragraph(
"VT is a potentially life-threatening arrhythmia defined as ≥3 consecutive ventricular beats "
"at a rate ≥100 bpm. Because it originates below the bundle of His, conduction is "
"abnormal producing <b>wide, bizarre QRS complexes</b>. Sustained VT (>30 seconds) requires "
"immediate treatment.", body_style))
vt_ecg = make_vt_ecg(w=int(W), h=90)
story.append(vt_ecg)
story.append(Paragraph("Figure 5. Ventricular tachycardia: fast rate, wide bizarre QRS, no P waves.",
caption_style))
vt_data = [
["Feature", "Finding"],
["Rate", "100-250 bpm (often 150-200 bpm)"],
["QRS complex", "Wide (>120 ms), bizarre morphology"],
["P waves", "Absent or dissociated (AV dissociation)"],
["Regularity", "Usually regular"],
["Fusion beats", "Pathognomonic of VT (capture/fusion beats)"],
["Concordance", "All precordial leads point same direction (positive or negative)"],
]
vt = Table(vt_data, colWidths=[W*0.25, W*0.75])
vt.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_RED),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(vt)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<i>Reference: Harrison's 22E, Chapter 247; Fuster & Hurst's The Heart 15e</i>", ref_style))
# ── Section 8: Conduction Blocks ──────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 8 — Pathology: Conduction Blocks", color=ECG_PURPLE))
story.append(Spacer(1, 0.3*cm))
# Complete heart block
story.append(Paragraph("Complete (3rd Degree) Heart Block", h2_style))
story.append(Paragraph(
"In complete heart block, no atrial impulses are conducted to the ventricles. "
"The atria and ventricles beat <b>independently</b>: the atrial rate is faster "
"(60-100 bpm) while the ventricles are driven by a slow escape rhythm (30-60 bpm "
"if junctional, 20-40 bpm if ventricular — wide QRS).", body_style))
hb_ecg = make_heart_block_ecg(w=int(W), h=90)
story.append(hb_ecg)
story.append(Paragraph("Figure 6. Complete heart block: P waves (orange) march independently of "
"wide QRS escape beats (purple). AV dissociation is present.", caption_style))
hb_data = [
["Feature", "Finding"],
["P waves", "Present, regular — but have NO relationship to QRS"],
["QRS complex", "Wide, slow escape rhythm (if ventricular origin)"],
["AV relationship", "Complete AV dissociation — PP ≠ RR"],
["Rate", "Ventricular: 20-40 bpm; Atrial: 60-100 bpm"],
["PR interval", "Variable — no fixed PR interval"],
]
hb = Table(hb_data, colWidths=[W*0.25, W*0.75])
hb.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(hb)
story.append(Spacer(1, 0.3*cm))
# Heart block classification
story.append(Paragraph("AV Block Classification", h3_style))
av_data = [
["Type", "PR Interval", "Dropped Beats?", "Typical Cause"],
["1st Degree AV block", "Prolonged >200ms", "No — all P's conduct", "Vagal tone, inferior MI, drugs"],
["2nd Degree Mobitz I\n(Wenckebach)", "Progressive ↑ then drop", "Yes — after longest PR", "AV node ischemia, inferior MI"],
["2nd Degree Mobitz II", "Fixed PR, sudden drop", "Yes — without warning", "His-Purkinje disease, anterior MI"],
["3rd Degree (Complete)", "No relationship", "All P's fail to conduct", "Ischemia, Lyme disease, surgery"],
]
av = Table(av_data, colWidths=[W*0.25, W*0.22, W*0.22, W*0.31])
av.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6C3483")),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
]))
story.append(av)
story.append(Spacer(1, 0.3*cm))
# LBBB
story.append(Paragraph("Left Bundle Branch Block (LBBB)", h2_style))
story.append(Paragraph(
"LBBB occurs when conduction through the left bundle branch fails. "
"Depolarization must spread slowly through myocardium rather than rapidly via Purkinje fibers, "
"producing a <b>wide QRS >120 ms</b> with characteristic morphology. "
"New LBBB in the context of chest pain should be treated as STEMI equivalent.", body_style))
lbbb_ecg = make_lbbb_ecg(w=int(W), h=90)
story.append(lbbb_ecg)
story.append(Paragraph("Figure 7. LBBB: broad notched R waves (William's pattern), "
"T-wave inversion, QRS >120 ms.", caption_style))
lbbb_data = [
["Lead", "LBBB Pattern"],
["V1, V2", "Deep, broad rS or QS pattern (dominant negativity)"],
["V5, V6, I, aVL", "Broad notched R wave (M pattern — 'William's' pattern)"],
["ST-T changes", "ST depression + T inversion in lateral leads (discordant)"],
["QRS duration", ">120 ms (often >140 ms)"],
["No Q waves", "Absence of septal q in I, V5, V6 (first sign to look for)"],
]
lbbb = Table(lbbb_data, colWidths=[W*0.25, W*0.75])
lbbb.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(lbbb)
# ── Section 9: WPW & Long QT ──────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 9 — Pathology: WPW & Long QT Syndrome", color=ECG_PURPLE))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Wolff-Parkinson-White (WPW) Syndrome", h2_style))
story.append(Paragraph(
"WPW results from an <b>accessory pathway</b> (Bundle of Kent) that bypasses the AV node, "
"allowing pre-excitation of the ventricle. The hallmarks are a <b>short PR interval</b> "
"(<120 ms) and a <b>delta wave</b> (slurred upstroke of the QRS), causing a wide QRS. "
"It predisposes to re-entrant supraventricular tachycardia and, if associated with AF, "
"can cause dangerously fast ventricular rates.", body_style))
wpw_ecg = make_wpw_ecg(w=int(W), h=90)
story.append(wpw_ecg)
story.append(Paragraph("Figure 8. WPW: short PR interval, delta wave (slurred QRS upstroke), "
"broad QRS complex.", caption_style))
wpw_data = [
["Feature", "Finding"],
["PR interval", "Short (<120 ms) — pre-excitation bypasses AV node delay"],
["Delta wave", "Slurred initial upstroke of QRS (pre-excitation of ventricle)"],
["QRS duration", "Wide (>120 ms) due to abnormal ventricular activation"],
["ST/T changes", "Discordant — secondary to abnormal depolarization"],
["Risk", "AVRT (re-entry); if AF present → rapid ventricular response → VF"],
]
wpw = Table(wpw_data, colWidths=[W*0.25, W*0.75])
wpw.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(wpw)
story.append(Spacer(1, 0.3*cm))
# Long QT
story.append(Paragraph("Long QT Syndrome", h2_style))
story.append(Paragraph(
"Prolongation of the QT interval reflects delayed ventricular repolarization, "
"predisposing to <b>Torsades de Pointes (TdP)</b> — a polymorphic VT that can degenerate "
"into ventricular fibrillation. QTc >450 ms in men and >460 ms in women is abnormal. "
"Causes include drugs (amiodarone, antipsychotics), electrolyte abnormalities, "
"and congenital channelopathies (Romano-Ward, Jervell-Lange-Nielsen).", body_style))
lqt_ecg = make_long_qt_ecg(w=int(W), h=90)
story.append(lqt_ecg)
story.append(Paragraph("Figure 9. Long QT: prolonged QT interval before T wave; predisposes to Torsades de Pointes.",
caption_style))
lqt_data = [
["Feature", "Finding"],
["QTc", ">450 ms (men) / >460 ms (women) — corrected for heart rate"],
["T wave", "May be broad, notched, biphasic, or late-appearing"],
["Risk", "Torsades de Pointes → VF → sudden cardiac death"],
["Drugs causing QT↑", "Amiodarone, sotalol, haloperidol, erythromycin, fluconazole"],
["Electrolytes", "Hypokalemia, hypomagnesaemia, hypocalcaemia all prolong QT"],
]
lqt = Table(lqt_data, colWidths=[W*0.25, W*0.75])
lqt.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6C3483")),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(lqt)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<i>Reference: Harrison's 22E pp.1918-1920; Fuster & Hurst's The Heart 15e</i>", ref_style))
# ── Section 10: Electrolyte Disturbances ──────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("SECTION 10 — Pathology: Electrolyte Disturbances", color=ECG_ORANGE))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Hyperkalemia ECG Changes", h2_style))
story.append(Paragraph(
"Hyperkalemia produces a characteristic sequence of ECG changes that correlate "
"with the serum potassium level. These changes reflect progressive depolarization "
"of cardiac muscle membranes and are <b>potentially life-threatening</b>.", body_style))
hk_ecg = make_hyperkalemia_ecg(w=int(W), h=90)
story.append(hk_ecg)
story.append(Paragraph("Figure 10. Hyperkalemia: tall peaked (tented) T waves, widened QRS, "
"flattened P waves.", caption_style))
hk_data = [
["Serum K+", "ECG Change", "Mechanism"],
["5.5-6.5 mEq/L", "Tall, peaked (tented) T waves; shortened QT", "Accelerated repolarization"],
["6.5-7.5 mEq/L", "Prolonged PR interval; flattened P waves", "Impaired atrial conduction"],
["7.0-8.0 mEq/L", "Widened QRS complex; bundle branch patterns", "Impaired ventricular conduction"],
[">8.0 mEq/L", "'Sine wave' pattern; loss of P wave", "SA arrest; ventricular standstill"],
[">10 mEq/L", "Ventricular fibrillation → asystole", "Complete electrical failure"],
]
hk = Table(hk_data, colWidths=[W*0.20, W*0.45, W*0.35])
hk.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_ORANGE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(hk)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Other Electrolyte ECG Changes", h3_style))
elec_data = [
["Condition", "ECG Findings"],
["Hypokalemia", "Prominent U waves (>T wave height), ST depression, prolonged QU interval, T flattening"],
["Hypercalcemia", "Shortened QT interval, short ST segment; may cause bradycardia, AV block"],
["Hypocalcemia", "Prolonged QT (lengthened ST segment); predisposes to TdP"],
["Hypomagnesemia", "Prolonged QT, U waves, T wave changes; synergistic with hypokalemia"],
["Hypothermia", "Osborn (J) waves at QRS-ST junction, bradycardia, prolonged intervals"],
]
elec = Table(elec_data, colWidths=[W*0.25, W*0.75])
elec.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor("#784212")),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 9),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(elec)
story.append(Spacer(1, 0.3*cm))
# ── Quick Reference Summary ───────────────────────────────────────────
story.append(PageBreak())
story.append(SectionHeader("QUICK REFERENCE — Pathology Summary", color=ECG_BLUE))
story.append(Spacer(1, 0.3*cm))
qr_data = [
["Pathology", "Key ECG Features", "Emergency?"],
["STEMI", "ST elevation ≥1mm ≥2 contiguous leads, Q waves, reciprocal depression",
"YES — PCI within 90 min"],
["NSTEMI/UA", "ST depression, T inversion — no ST elevation, troponin elevated",
"YES — urgent ACS pathway"],
["Atrial Fibrillation", "No P waves, irregularly irregular RR, fibrillatory baseline",
"If rapid or haemodynamic instability"],
["Ventricular Tachycardia", "Wide QRS (>120ms), rate >100, AV dissociation",
"YES — urgent cardioversion if unstable"],
["VF", "Chaotic, irregular, no organised QRS complexes",
"YES — immediate defibrillation"],
["Complete Heart Block", "P waves unrelated to QRS, slow wide escape, AV dissociation",
"YES — transcutaneous/transvenous pacing"],
["1st Degree AV Block", "PR >200ms, every P conducts",
"No — monitor only"],
["LBBB (new)", "QRS >120ms, broad R in V5-V6, no septal q, new with chest pain",
"Treat as STEMI if new"],
["RBBB", "rSR' in V1 ('M' pattern), wide S in I, V5-V6 ('W' pattern)",
"No — unless new with symptoms"],
["WPW", "Short PR, delta wave, wide QRS",
"Yes if tachycardia — avoid AV nodal drugs"],
["Long QT", "QTc >450ms (M) / >460ms (F)",
"Yes if Torsades present"],
["Hyperkalemia", "Peaked T, wide QRS, flat P, sine wave",
"YES >6.5 mEq/L — give calcium"],
["Pericarditis", "Diffuse saddle-shaped ST elevation, PR depression (all leads)",
"Usually not — but exclude STEMI"],
]
qr_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
('LEFTPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('TEXTCOLOR', (2,1), (2,-1), ECG_RED),
('FONTNAME', (2,1), (2,-1), 'Helvetica-Bold'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
])
qr = Table(qr_data, colWidths=[W*0.22, W*0.56, W*0.22])
qr.setStyle(qr_style)
story.append(qr)
story.append(Spacer(1, 0.3*cm))
# References
story.append(HRFlowable(width=W, thickness=0.5, color=ECG_BLUE))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("<b>References</b>", h3_style))
refs = [
"1. Longo DL, et al. <i>Harrison's Principles of Internal Medicine, 22nd Edition</i>. McGraw-Hill, 2025. Chapter 247: Electrocardiography.",
"2. Hall JE. <i>Guyton and Hall Textbook of Medical Physiology, 14th Edition</i>. Elsevier, 2021. Chapter 11: Fundamentals of Electrocardiography.",
"3. Goldberger AL, et al. <i>Goldberger's Clinical Electrocardiography: A Simplified Approach, 10th ed.</i> Elsevier, 2024.",
"4. Fuster V, et al. <i>Fuster and Hurst's The Heart, 15th Edition</i>. McGraw-Hill, 2022.",
"5. Tintinalli JE, et al. <i>Tintinalli's Emergency Medicine: A Comprehensive Study Guide, 9th ed.</i> McGraw-Hill, 2020.",
"6. Goldman L, Schafer AI. <i>Goldman-Cecil Medicine, 27th Edition</i>. Elsevier, 2024.",
]
for r in refs:
story.append(Paragraph(r, ref_style))
doc.build(story)
print(f"PDF created: {out_path}")
if __name__ == "__main__":
out = "/home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf"
build_pdf(out)
cd /home/daytona/workspace/ecg-pdf && python generate_ecg_pdf.py 2>&1
ls -lh /home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf
python3 -c "from pypdf import PdfReader; r = PdfReader('/home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf'); print(f'Pages: {len(r.pages)}')"
ECG Basics and Pathology
PDF Document · PDF
| Section | Content |
|---|---|
| Cover | Title banner + live ECG trace + table of contents |
| Section 1 | Fundamentals of ECG — what it measures, the cardiac conduction system (SA node → AV node → His-Purkinje), conduction velocities table |
| Section 2 | ECG Waveforms & Intervals — fully labeled diagram (P, QRS, T, U, PR/QRS/QT intervals) with normal values table |
| Section 3 | 12-Lead System — lead groups, territories, normal sinus rhythm tracing |
| Section 4 | Cardiac Axis — color-coded frontal plane diagram, LAD/RAD/normal zones, quick identification method |
| Section 5 | Systematic 8-Step ECG Interpretation method + heart rate calculation table |
| Section 6 | STEMI — tombstone ST elevation ECG, identification steps, territory localization table (Anterior/Inferior/Lateral/Posterior/RV) |
| Section 7 | Arrhythmias — AF (fibrillatory baseline, no P waves) + VT (wide fast bizarre QRS) with labeled diagrams |
| Section 8 | Conduction Blocks — Complete heart block (AV dissociation), AV block classification table, LBBB (William's pattern) |
| Section 9 | WPW (delta wave, short PR) + Long QT (TdP risk, drug causes) |
| Section 10 | Electrolyte disturbances — hyperkalemia sequence, hypo/hypercalcemia, hypothermia J waves |
| Quick Ref | Full pathology summary table with 13 diagnoses, key ECG features, and emergency flags |