~/ecg-reference/generate_ecg_pdf.py
#!/usr/bin/env python3
"""
ICU ECG Pattern Reference PDF Generator
Visual quick-reference for bedside use
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.graphics.shapes import (
Drawing, PolyLine, Rect, String, Line, Circle, Group
)
from reportlab.graphics import renderPDF
from reportlab.platypus import Flowable
import math
# ββ Color Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RED = colors.HexColor('#C0392B')
DARK_RED = colors.HexColor('#7B241C')
GREEN = colors.HexColor('#1E8449')
DARK_GREEN= colors.HexColor('#145A32')
BLUE = colors.HexColor('#1A5276')
LIGHT_BLUE= colors.HexColor('#2E86C1')
ORANGE = colors.HexColor('#D35400')
PURPLE = colors.HexColor('#6C3483')
DARK_GRAY = colors.HexColor('#2C3E50')
MID_GRAY = colors.HexColor('#7F8C8D')
LIGHT_GRAY= colors.HexColor('#F2F3F4')
ECG_BG = colors.HexColor('#FFF9F9')
ECG_GRID = colors.HexColor('#FFCCCC')
ECG_LINE = colors.HexColor('#C0392B')
WHITE = colors.white
BLACK = colors.black
YELLOW_BG = colors.HexColor('#FDFEFE')
HEADER_BG = colors.HexColor('#1A5276')
ALERT_BG = colors.HexColor('#FDEDEC')
INFO_BG = colors.HexColor('#EBF5FB')
WARN_BG = colors.HexColor('#FEF9E7')
GOOD_BG = colors.HexColor('#EAFAF1')
# ββ ECG Waveform Drawing Flowable ββββββββββββββββββββββββββββββββββββββββββββ
class ECGWaveform(Flowable):
"""Draw an ECG waveform with grid background and labeling."""
def __init__(self, width, height, points, label="", color=None,
show_grid=True, label_points=None, bg_color=None):
Flowable.__init__(self)
self.width = width
self.height = height
self.points = points # list of (x, y) in 0-1 space
self.label = label
self.color = color or ECG_LINE
self.show_grid = show_grid
self.label_points = label_points or [] # [(x, y, text), ...]
self.bg_color = bg_color or ECG_BG
def draw(self):
c = self.canv
w, h = self.width, self.height
pad = 4
# Background
c.setFillColor(self.bg_color)
c.roundRect(0, 0, w, h, 3, fill=1, stroke=0)
# Grid lines
if self.show_grid:
c.setStrokeColor(ECG_GRID)
c.setLineWidth(0.3)
# Vertical grid every ~5mm
step = w / 20
for i in range(1, 20):
c.line(i * step, pad, i * step, h - pad)
# Horizontal grid every 25% height
for frac in [0.25, 0.5, 0.75]:
y = pad + frac * (h - 2 * pad)
c.line(pad, y, w - pad, y)
# Border
c.setStrokeColor(colors.HexColor('#DDDDDD'))
c.setLineWidth(0.5)
c.roundRect(0, 0, w, h, 3, fill=0, stroke=1)
# Waveform
if len(self.points) >= 2:
c.setStrokeColor(self.color)
c.setLineWidth(1.8)
path = c.beginPath()
x0 = pad + self.points[0][0] * (w - 2 * pad)
y0 = pad + self.points[0][1] * (h - 2 * pad)
path.moveTo(x0, y0)
for px, py in self.points[1:]:
x = pad + px * (w - 2 * pad)
y = pad + py * (h - 2 * pad)
path.lineTo(x, y)
c.drawPath(path, stroke=1, fill=0)
# Baseline reference (dotted)
c.setStrokeColor(MID_GRAY)
c.setLineWidth(0.4)
c.setDash([2, 3])
mid_y = pad + 0.35 * (h - 2 * pad)
c.line(pad, mid_y, w - pad, mid_y)
c.setDash([])
# Annotation labels on waveform
c.setFont("Helvetica-Bold", 6.5)
for lx, ly, lt in self.label_points:
x = pad + lx * (w - 2 * pad)
y = pad + ly * (h - 2 * pad)
c.setFillColor(DARK_GRAY)
c.drawCentredString(x, y, lt)
# Label bottom-left
if self.label:
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(DARK_GRAY)
c.drawString(5, 4, self.label)
def make_ecg(width, height, pattern_type, **kwargs):
"""Factory β returns an ECGWaveform for the named pattern."""
baseline = 0.35 # y-fraction for isoelectric line
top = 0.92
peak_qrs = 0.95
trough_qrs = 0.02
mid = 0.5
# ββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def segment(x0, y0, x1, y1, n=2):
"""Straight segment as list of points."""
return [(x0 + (x1 - x0) * i / (n - 1),
y0 + (y1 - y0) * i / (n - 1)) for i in range(n)]
def p_wave(cx, amp=0.12, w=0.06):
"""Smooth P wave (half-sine)."""
pts = []
for i in range(12):
t = i / 11
pts.append((cx - w / 2 + t * w, baseline + amp * math.sin(math.pi * t)))
return pts
def normal_beat(offset=0.0, scale=1.0):
"""One normal PQRST beat."""
b = baseline
pts = []
# Baseline approach
pts += segment(offset + 0.00, b, offset + 0.05, b)
# P wave
pts += p_wave(offset + 0.09, amp=0.10 * scale)
# PR segment
pts += segment(offset + 0.12, b, offset + 0.17, b)
# Q
pts += segment(offset + 0.17, b, offset + 0.19, b - 0.06 * scale)
# R
pts += segment(offset + 0.19, b - 0.06 * scale, offset + 0.21, b + 0.55 * scale)
# S
pts += segment(offset + 0.21, b + 0.55 * scale, offset + 0.23, b - 0.08 * scale)
# Return to baseline
pts += segment(offset + 0.23, b - 0.08 * scale, offset + 0.26, b)
# ST segment (flat at baseline)
pts += segment(offset + 0.26, b, offset + 0.32, b)
# T wave
pts += p_wave(offset + 0.37, amp=0.13 * scale, w=0.10)
# Return to baseline
pts += segment(offset + 0.43, b, offset + 0.50, b)
return pts
def st_elevation_beat(offset=0.0, elev=0.15):
"""Beat with ST elevation."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
pts += p_wave(offset + 0.09, amp=0.10)
pts += segment(offset + 0.12, b, offset + 0.17, b)
pts += segment(offset + 0.17, b, offset + 0.19, b - 0.06)
pts += segment(offset + 0.19, b - 0.06, offset + 0.21, b + 0.55)
pts += segment(offset + 0.21, b + 0.55, offset + 0.24, b - 0.04)
# ST elevated & straight/oblique
pts += segment(offset + 0.24, b - 0.04, offset + 0.26, b + elev)
pts += segment(offset + 0.26, b + elev, offset + 0.33, b + elev)
# T wave (tall hyperacute-ish)
pts += p_wave(offset + 0.38, amp=0.18, w=0.10)
pts += segment(offset + 0.43, b, offset + 0.50, b)
return pts
def st_depression_beat(offset=0.0, dep=0.12):
"""Beat with horizontal ST depression."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
pts += p_wave(offset + 0.09, amp=0.10)
pts += segment(offset + 0.12, b, offset + 0.17, b)
pts += segment(offset + 0.17, b, offset + 0.19, b - 0.06)
pts += segment(offset + 0.19, b - 0.06, offset + 0.21, b + 0.55)
pts += segment(offset + 0.21, b + 0.55, offset + 0.24, b - 0.04)
pts += segment(offset + 0.24, b - 0.04, offset + 0.26, b - dep)
pts += segment(offset + 0.26, b - dep, offset + 0.33, b - dep)
pts += p_wave(offset + 0.37, amp=0.10, w=0.10)
pts += segment(offset + 0.43, b, offset + 0.50, b)
return pts
def t_inversion_beat(offset=0.0):
"""Beat with inverted T wave."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
pts += p_wave(offset + 0.09, amp=0.10)
pts += segment(offset + 0.12, b, offset + 0.17, b)
pts += segment(offset + 0.17, b, offset + 0.19, b - 0.06)
pts += segment(offset + 0.19, b - 0.06, offset + 0.21, b + 0.55)
pts += segment(offset + 0.21, b + 0.55, offset + 0.24, b - 0.04)
pts += segment(offset + 0.24, b - 0.04, offset + 0.26, b)
pts += segment(offset + 0.26, b, offset + 0.33, b)
# Inverted T
for i in range(12):
t = i / 11
cx = offset + 0.37
w2 = 0.10
x = cx - w2 / 2 + t * w2
y = b - 0.13 * math.sin(math.pi * t)
pts.append((x, y))
pts += segment(offset + 0.43, b, offset + 0.50, b)
return pts
def af_beat(offset=0.0):
"""Irregular AF-like beat (no P, narrow QRS, irregular baseline)."""
b = baseline
pts = []
# Fibrillatory baseline
for i in range(30):
x = offset + i * 0.006
y = b + 0.03 * math.sin(i * 1.8) + 0.02 * math.sin(i * 3.5)
pts.append((x, y))
# QRS at irregular position
qrs_x = offset + 0.18
pts += segment(qrs_x, b, qrs_x + 0.01, b - 0.04)
pts += segment(qrs_x + 0.01, b - 0.04, qrs_x + 0.03, b + 0.55)
pts += segment(qrs_x + 0.03, b + 0.55, qrs_x + 0.05, b - 0.06)
pts += segment(qrs_x + 0.05, b - 0.06, qrs_x + 0.07, b)
# More fibbrillation
for i in range(18):
x = offset + 0.26 + i * 0.006
y = b + 0.025 * math.sin(i * 2.1) + 0.015 * math.sin(i * 4.0)
pts.append((x, y))
# Another QRS at different spacing
qrs2 = offset + 0.38
pts += segment(qrs2, b, qrs2 + 0.01, b - 0.04)
pts += segment(qrs2 + 0.01, b - 0.04, qrs2 + 0.03, b + 0.52)
pts += segment(qrs2 + 0.03, b + 0.52, qrs2 + 0.05, b - 0.05)
pts += segment(qrs2 + 0.05, b - 0.05, qrs2 + 0.07, b)
# Tail
for i in range(10):
x = offset + 0.46 + i * 0.004
y = b + 0.02 * math.sin(i * 2.5)
pts.append((x, y))
return pts
def vt_beat(offset=0.0, n=2):
"""Wide complex VT beats."""
b = baseline
pts = []
for beat in range(n):
o = offset + beat * 0.45
pts += segment(o + 0.00, b, o + 0.03, b)
# Wide bizarre QRS
pts += segment(o + 0.03, b, o + 0.06, b + 0.20)
pts += segment(o + 0.06, b + 0.20, o + 0.10, b + 0.65)
pts += segment(o + 0.10, b + 0.65, o + 0.14, b - 0.05)
pts += segment(o + 0.14, b - 0.05, o + 0.20, b + 0.08)
pts += segment(o + 0.20, b + 0.08, o + 0.24, b - 0.03)
pts += segment(o + 0.24, b - 0.03, o + 0.28, b)
pts += segment(o + 0.28, b, o + 0.32, b)
# Discordant T
for i in range(8):
t = i / 7
cx = o + 0.37
x = cx - 0.06 + t * 0.12
y = b - 0.10 * math.sin(math.pi * t)
pts.append((x, y))
pts += segment(o + 0.44, b, o + 0.45, b)
return pts
def vf_wave(offset=0.0):
"""VF - chaotic undulation."""
b = baseline
pts = []
for i in range(120):
x = offset + i * 0.008
if x > 1.0:
break
amp = 0.25 + 0.10 * math.sin(i * 0.7)
y = b + amp * math.sin(i * 0.9 + 1.2) * math.cos(i * 0.4)
pts.append((x, y))
return pts
def flutter_wave(offset=0.0):
"""Atrial flutter sawtooth."""
b = baseline
pts = []
# Regular sawtooth flutter waves
for wave in range(6):
o = offset + wave * 0.12
pts += segment(o + 0.00, b, o + 0.07, b + 0.22)
pts += segment(o + 0.07, b + 0.22, o + 0.12, b)
# One QRS (2:1)
qrs_x = offset + 0.24
pts += segment(qrs_x, b, qrs_x + 0.01, b - 0.03)
pts += segment(qrs_x + 0.01, b - 0.03, qrs_x + 0.025, b + 0.55)
pts += segment(qrs_x + 0.025, b + 0.55, qrs_x + 0.04, b - 0.05)
pts += segment(qrs_x + 0.04, b - 0.05, qrs_x + 0.06, b)
# More flutter
for wave in range(5):
o = offset + 0.31 + wave * 0.12
pts += segment(o + 0.00, b, o + 0.07, b + 0.22)
pts += segment(o + 0.07, b + 0.22, o + 0.12, b)
# Second QRS
qrs2 = offset + 0.61
pts += segment(qrs2, b, qrs2 + 0.01, b - 0.03)
pts += segment(qrs2 + 0.01, b - 0.03, qrs2 + 0.025, b + 0.55)
pts += segment(qrs2 + 0.025, b + 0.55, qrs2 + 0.04, b - 0.05)
pts += segment(qrs2 + 0.04, b - 0.05, qrs2 + 0.06, b)
for i in range(15):
o2 = offset + 0.68 + i * 0.02
pts.append((o2, b + 0.18 * math.sin(i * 1.6)))
return pts
def heart_block_3rd(offset=0.0):
"""3rd degree heart block - P waves marching through, QRS escape."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.03, b)
# P wave 1
pts += p_wave(offset + 0.06, amp=0.10)
pts += segment(offset + 0.10, b, offset + 0.15, b)
# QRS escape (narrow, slow)
pts += segment(offset + 0.15, b, offset + 0.17, b - 0.04)
pts += segment(offset + 0.17, b - 0.04, offset + 0.19, b + 0.45)
pts += segment(offset + 0.19, b + 0.45, offset + 0.21, b - 0.05)
pts += segment(offset + 0.21, b - 0.05, offset + 0.23, b)
pts += segment(offset + 0.23, b, offset + 0.28, b)
# P wave 2 (different timing from QRS)
pts += p_wave(offset + 0.31, amp=0.10)
pts += segment(offset + 0.35, b, offset + 0.38, b)
# P wave 3
pts += p_wave(offset + 0.44, amp=0.10)
pts += segment(offset + 0.48, b, offset + 0.50, b)
# Another escape QRS
pts += segment(offset + 0.50, b, offset + 0.52, b - 0.04)
pts += segment(offset + 0.52, b - 0.04, offset + 0.54, b + 0.45)
pts += segment(offset + 0.54, b + 0.45, offset + 0.56, b - 0.05)
pts += segment(offset + 0.56, b - 0.05, offset + 0.58, b)
# P wave 4
pts += segment(offset + 0.58, b, offset + 0.64, b)
pts += p_wave(offset + 0.67, amp=0.10)
pts += segment(offset + 0.72, b, offset + 0.80, b)
# P wave 5
pts += p_wave(offset + 0.83, amp=0.10)
pts += segment(offset + 0.87, b, offset + 1.00, b)
return pts
def hyperkalemia_ecg(offset=0.0):
"""Tall tented T waves + wide QRS (moderate hyperK)."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
# Flat/absent P
pts += segment(offset + 0.05, b, offset + 0.11, b + 0.03)
pts += segment(offset + 0.11, b + 0.03, offset + 0.15, b)
pts += segment(offset + 0.15, b, offset + 0.18, b)
# Wide QRS
pts += segment(offset + 0.18, b, offset + 0.20, b - 0.04)
pts += segment(offset + 0.20, b - 0.04, offset + 0.23, b + 0.52)
pts += segment(offset + 0.23, b + 0.52, offset + 0.29, b - 0.06)
pts += segment(offset + 0.29, b - 0.06, offset + 0.33, b)
pts += segment(offset + 0.33, b, offset + 0.36, b)
# Tall symmetric (tented) T wave
for i in range(16):
t = i / 15
cx = offset + 0.43
x = cx - 0.07 + t * 0.14
y = b + 0.42 * math.sin(math.pi * t) # Very tall!
pts.append((x, y))
pts += segment(offset + 0.50, b, offset + 0.55, b)
return pts
def hypokalemia_ecg(offset=0.0):
"""Flat T + prominent U wave."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
pts += p_wave(offset + 0.09, amp=0.10)
pts += segment(offset + 0.12, b, offset + 0.17, b)
pts += segment(offset + 0.17, b, offset + 0.19, b - 0.04)
pts += segment(offset + 0.19, b - 0.04, offset + 0.21, b + 0.50)
pts += segment(offset + 0.21, b + 0.50, offset + 0.23, b - 0.05)
pts += segment(offset + 0.23, b - 0.05, offset + 0.26, b)
pts += segment(offset + 0.26, b, offset + 0.30, b)
# Flat/small T
for i in range(8):
t = i / 7
cx = offset + 0.33
x = cx - 0.04 + t * 0.08
y = b + 0.04 * math.sin(math.pi * t)
pts.append((x, y))
# U wave
pts += segment(offset + 0.38, b, offset + 0.40, b)
for i in range(10):
t = i / 9
cx = offset + 0.44
x = cx - 0.04 + t * 0.08
y = b + 0.11 * math.sin(math.pi * t)
pts.append((x, y))
pts += segment(offset + 0.49, b, offset + 0.55, b)
return pts
def pericarditis_ecg(offset=0.0):
"""Diffuse saddle-shaped ST elevation + PR depression."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.05, b)
pts += p_wave(offset + 0.09, amp=0.10)
# PR depression
pts += segment(offset + 0.12, b, offset + 0.13, b - 0.04)
pts += segment(offset + 0.13, b - 0.04, offset + 0.17, b - 0.04)
pts += segment(offset + 0.17, b - 0.04, offset + 0.19, b - 0.04)
pts += segment(offset + 0.19, b - 0.04, offset + 0.21, b + 0.48)
pts += segment(offset + 0.21, b + 0.48, offset + 0.23, b - 0.02)
# Saddle-shaped ST elevation (curved upward)
for i in range(12):
t = i / 11
x = offset + 0.24 + t * 0.12
# Saddle = concave upward
y = b + 0.12 - 0.04 * math.cos(math.pi * t)
pts.append((x, y))
# T wave (upright, following ST)
pts += p_wave(offset + 0.40, amp=0.16, w=0.10)
pts += segment(offset + 0.46, b, offset + 0.55, b)
return pts
def torsades_ecg(offset=0.0):
"""Torsades de pointes - twisting QRS."""
b = baseline
pts = []
for beat in range(5):
o = offset + beat * 0.19
phase = beat * math.pi * 0.7
amp = 0.55 * math.sin(phase + 0.5)
sign = 1 if amp > 0 else -1
amp = max(0.25, abs(amp)) * sign
# Brief QRS
pts += segment(o, b, o + 0.02, b + amp * 0.3)
pts += segment(o + 0.02, b + amp * 0.3, o + 0.05, b + amp)
pts += segment(o + 0.05, b + amp, o + 0.08, b - amp * 0.2)
pts += segment(o + 0.08, b - amp * 0.2, o + 0.10, b)
# Discordant T
for i in range(6):
t2 = i / 5
x = o + 0.12 + t2 * 0.06
y = b - amp * 0.3 * math.sin(math.pi * t2)
pts.append((x, y))
pts += segment(o + 0.18, b, o + 0.19, b)
return pts
def prolonged_qt(offset=0.0):
"""Normal QRS, prolonged QT interval."""
b = baseline
pts = []
pts += segment(offset + 0.00, b, offset + 0.04, b)
pts += p_wave(offset + 0.08, amp=0.10)
pts += segment(offset + 0.11, b, offset + 0.15, b)
pts += segment(offset + 0.15, b, offset + 0.17, b - 0.04)
pts += segment(offset + 0.17, b - 0.04, offset + 0.19, b + 0.52)
pts += segment(offset + 0.19, b + 0.52, offset + 0.21, b - 0.05)
pts += segment(offset + 0.21, b - 0.05, offset + 0.24, b)
# Very prolonged ST before T wave
pts += segment(offset + 0.24, b, offset + 0.42, b)
# T wave
pts += p_wave(offset + 0.47, amp=0.12, w=0.10)
pts += segment(offset + 0.53, b, offset + 0.60, b)
return pts
def wenckebach_ecg(offset=0.0):
"""Mobitz I - PR progressively lengthens, then dropped beat."""
b = baseline
pts = []
pr_delays = [0.04, 0.06, 0.09] # Increasing PR
beat_starts = [0.00, 0.22, 0.46]
for i, (bs, pr) in enumerate(zip(beat_starts, pr_delays)):
o = offset + bs
pts += segment(o, b, o + 0.02, b)
pts += p_wave(o + 0.03, amp=0.09)
pts += segment(o + 0.06, b, o + 0.06 + pr, b)
qrs_start = o + 0.06 + pr
pts += segment(qrs_start, b, qrs_start + 0.01, b - 0.03)
pts += segment(qrs_start + 0.01, b - 0.03, qrs_start + 0.03, b + 0.45)
pts += segment(qrs_start + 0.03, b + 0.45, qrs_start + 0.05, b - 0.04)
pts += segment(qrs_start + 0.05, b - 0.04, qrs_start + 0.07, b)
pts += segment(qrs_start + 0.07, b, qrs_start + 0.10, b)
pts += p_wave(qrs_start + 0.12, amp=0.09, w=0.05)
pts += segment(qrs_start + 0.16, b, qrs_start + 0.20, b)
# Dropped beat - just P wave, no QRS
o_drop = offset + 0.73
pts += p_wave(o_drop, amp=0.09)
pts += segment(o_drop + 0.04, b, o_drop + 0.27, b)
return pts
# ββ Pattern dispatch ββββββββββββββββββββββββββββββββββββββββββββββββ
patterns = {
'normal': (normal_beat(0.05) + normal_beat(0.55),
[(0.26, 0.96, 'P'), (0.31, 0.98, 'Q'), (0.33, 0.99, 'R'),
(0.35, 0.97, 'S'), (0.44, 0.93, 'T')]),
'st_elevation': (st_elevation_beat(0.05) + st_elevation_beat(0.55),
[(0.36, 0.97, 'STβ')]),
'st_depression': (st_depression_beat(0.05) + st_depression_beat(0.55),
[(0.36, 0.97, 'STβ')]),
't_inversion': (t_inversion_beat(0.05) + t_inversion_beat(0.55),
[(0.44, 0.97, '-T')]),
'af': (af_beat(0.00), [(0.15, 0.97, 'No P')]),
'flutter': (flutter_wave(0.00), [(0.07, 0.96, 'Sawtooth')]),
'vt': (vt_beat(0.02, n=2), [(0.15, 0.97, 'Wide QRS')]),
'vf': (vf_wave(0.00), [(0.50, 0.97, 'Chaotic')]),
'chb': (heart_block_3rd(0.00), [(0.50, 0.97, 'Pβ QRS')]),
'hyperkalemia': (hyperkalemia_ecg(0.05), [(0.43, 0.97, 'Tall T')]),
'hypokalemia': (hypokalemia_ecg(0.05), [(0.44, 0.97, 'U wave')]),
'pericarditis': (pericarditis_ecg(0.05), [(0.30, 0.97, 'Saddle STβ')]),
'torsades': (torsades_ecg(0.00), [(0.50, 0.97, 'Twisting')]),
'prolonged_qt': (prolonged_qt(0.05), [(0.38, 0.97, 'Long QT')]),
'wenckebach': (wenckebach_ecg(0.00), [(0.88, 0.97, 'Drop')]),
}
pts_data, lp = patterns.get(pattern_type, patterns['normal'])
color = kwargs.pop('color', ECG_LINE)
label = kwargs.pop('label', '')
return ECGWaveform(width, height, pts_data, label=label, color=color,
label_points=lp, **kwargs)
# ββ Styled helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_styles():
styles = getSampleStyleSheet()
styles.add(ParagraphStyle('DocTitle',
fontSize=22, fontName='Helvetica-Bold',
textColor=WHITE, alignment=TA_CENTER, spaceAfter=2))
styles.add(ParagraphStyle('DocSubtitle',
fontSize=11, fontName='Helvetica',
textColor=colors.HexColor('#AED6F1'), alignment=TA_CENTER, spaceAfter=0))
styles.add(ParagraphStyle('SectionHeader',
fontSize=12, fontName='Helvetica-Bold',
textColor=WHITE, alignment=TA_LEFT, spaceAfter=3,
leftIndent=4))
styles.add(ParagraphStyle('CardTitle',
fontSize=9.5, fontName='Helvetica-Bold',
textColor=DARK_GRAY, spaceAfter=1, leftIndent=2))
styles.add(ParagraphStyle('CardBody',
fontSize=7.5, fontName='Helvetica',
textColor=DARK_GRAY, spaceAfter=1, leftIndent=2, leading=10))
styles.add(ParagraphStyle('AlertText',
fontSize=7.5, fontName='Helvetica-Bold',
textColor=RED, spaceAfter=1, leftIndent=2))
styles.add(ParagraphStyle('GoodText',
fontSize=7.5, fontName='Helvetica-Bold',
textColor=DARK_GREEN, spaceAfter=1, leftIndent=2))
styles.add(ParagraphStyle('TableHeader',
fontSize=8, fontName='Helvetica-Bold',
textColor=WHITE, alignment=TA_CENTER))
styles.add(ParagraphStyle('TableCell',
fontSize=7.5, fontName='Helvetica',
textColor=DARK_GRAY, alignment=TA_LEFT, leading=10))
styles.add(ParagraphStyle('TableCellBold',
fontSize=7.5, fontName='Helvetica-Bold',
textColor=DARK_GRAY, alignment=TA_LEFT))
styles.add(ParagraphStyle('FooterText',
fontSize=6.5, fontName='Helvetica',
textColor=MID_GRAY, alignment=TA_CENTER))
styles.add(ParagraphStyle('PageTitle',
fontSize=13, fontName='Helvetica-Bold',
textColor=WHITE, alignment=TA_LEFT, spaceAfter=2, leftIndent=6))
return styles
def section_header(title, styles, bg=HEADER_BG):
"""Colored section header bar."""
return Table(
[[Paragraph(title, styles['SectionHeader'])]],
colWidths=[17.5 * cm],
style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), bg),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('ROUNDEDCORNERS', [4, 4, 4, 4]),
])
)
def ecg_card(waveform, title, bullets, styles, bg=ECG_BG, alert=False):
"""A single ECG pattern card: waveform + description."""
title_color = RED if alert else BLUE
title_style = ParagraphStyle('_ct', fontSize=9, fontName='Helvetica-Bold',
textColor=title_color, spaceAfter=2, leftIndent=2)
bullet_style = ParagraphStyle('_cb', fontSize=7.2, fontName='Helvetica',
textColor=DARK_GRAY, leftIndent=4, leading=9.5)
content = [Paragraph(title, title_style)]
for b in bullets:
content.append(Paragraph(f"β’ {b}", bullet_style))
# Build inner table: waveform | text
inner = Table(
[[waveform, [c for c in content]]],
colWidths=[6.8 * cm, 9.5 * cm],
style=TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (0, 0), 3),
('RIGHTPADDING', (0, 0), (0, 0), 4),
('LEFTPADDING', (1, 0), (1, 0), 3),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
])
)
wrapper = Table(
[[inner]],
colWidths=[17.0 * cm],
style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), bg),
('BOX', (0, 0), (-1, -1), 0.6, colors.HexColor('#CCCCCC')),
('ROUNDEDCORNERS', [4, 4, 4, 4]),
('TOPPADDING', (0, 0), (-1, -1), 0),
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
])
)
return wrapper
# ββ Cover Page βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_cover(styles):
elems = []
# Large colored banner
banner = Table(
[[Paragraph("π« ICU ECG REFERENCE", styles['DocTitle']),
Paragraph("Quick Pattern Guide for Bedside Use", styles['DocSubtitle'])]],
colWidths=[17.5 * cm],
style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), HEADER_BG),
('TOPPADDING', (0, 0), (-1, -1), 14),
('BOTTOMPADDING', (0, 0), (-1, -1), 14),
('SPAN', (0, 0), (0, 1)),
])
)
# Re-do as single-column
banner = Table(
[[Paragraph("ICU ECG PATTERN REFERENCE", styles['DocTitle'])],
[Paragraph("Quick Visual Guide for Bedside Use | Hindi-English", styles['DocSubtitle'])]],
colWidths=[17.5 * cm],
style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), HEADER_BG),
('TOPPADDING', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
('LEFTPADDING', (0, 0), (-1, -1), 10),
])
)
elems.append(banner)
elems.append(Spacer(1, 0.3 * cm))
# Contents overview table
contents_data = [
[Paragraph("SECTION", styles['TableHeader']),
Paragraph("TOPICS", styles['TableHeader'])],
["Page 1", "Normal ECG + ECG Basics (PQRST)"],
["Page 2", "ST Elevation - STEMI Patterns & Localization"],
["Page 3", "ST Depression, T-wave Changes, Pericarditis"],
["Page 4", "Supraventricular Arrhythmias (AF, Flutter, SVT)"],
["Page 5", "Ventricular Arrhythmias (VT, VF, Torsades)"],
["Page 6", "Bradyarrhythmias & Heart Blocks"],
["Page 7", "Electrolyte ECG Changes (HyperK, HypoK, Ca, Mg)"],
["Page 8", "Special Patterns (PE, Tamponade, Hypothermia, Digoxin)"],
["Page 9", "ICU Emergency Quick-Action Table"],
]
t_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), HEADER_BG),
('BACKGROUND', (0, 1), (-1, 1), INFO_BG),
('BACKGROUND', (0, 2), (-1, 2), LIGHT_GRAY),
('BACKGROUND', (0, 3), (-1, 3), INFO_BG),
('BACKGROUND', (0, 4), (-1, 4), LIGHT_GRAY),
('BACKGROUND', (0, 5), (-1, 5), INFO_BG),
('BACKGROUND', (0, 6), (-1, 6), LIGHT_GRAY),
('BACKGROUND', (0, 7), (-1, 7), INFO_BG),
('BACKGROUND', (0, 8), (-1, 8), LIGHT_GRAY),
('BACKGROUND', (0, 9), (-1, 9), INFO_BG),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), BLUE),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 8),
])
tbl = Table(contents_data, colWidths=[3 * cm, 14.5 * cm], style=t_style)
elems.append(tbl)
elems.append(Spacer(1, 0.3 * cm))
# How to use box
how_data = [[
Paragraph(
"<b>HOW TO USE THIS REFERENCE / Kaise Use Karein:</b><br/>"
"1. ECG dekho β pattern identify karo β iss guide mein match karo<br/>"
"2. Diagnosis confirm karo β Emergency Action Table (Page 9) dekho<br/>"
"3. Patient stable hai ya nahi - PEHLE yeh decide karo<br/>"
"4. Unstable = SHOCK / DEFIB / CPR pehle, phir diagnosis<br/>"
"<i>Note: Ye guide clinical judgment ki jagah nahi le sakti. Doctor se confirm karo.</i>",
ParagraphStyle('hw', fontSize=8, fontName='Helvetica',
textColor=DARK_GRAY, leading=12))
]]
how_tbl = Table(how_data, colWidths=[17.5 * cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), WARN_BG),
('BOX', (0, 0), (-1, -1), 1, ORANGE),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(how_tbl)
return elems
# ββ Page builders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def page_normal_ecg(styles):
elems = [section_header("PAGE 1 β NORMAL ECG & BASICS", styles), Spacer(1, 0.2 * cm)]
# Normal ECG card (wide)
normal_wf = make_ecg(16 * cm, 3.2 * cm, 'normal', label='Normal Sinus Rhythm')
elems.append(normal_wf)
elems.append(Spacer(1, 0.2 * cm))
# PQRST explanation table
pq_data = [
[Paragraph("WAVE", styles['TableHeader']),
Paragraph("NORMAL VALUE", styles['TableHeader']),
Paragraph("MEANING", styles['TableHeader'])],
["P Wave", "< 0.12 sec, < 2.5 mm", "Atria ka depolarization (SA node se)"],
["PR Interval", "0.12 β 0.20 sec (3-5 boxes)", "AV node delay β agar > 0.20 = 1st degree block"],
["QRS Complex", "< 0.12 sec (< 3 boxes)", "Ventricles ka depolarization β agar wide = BBB ya VT"],
["ST Segment", "Isoelectric (flat, at baseline)", "STEMI = elevation; NSTEMI/Ischemia = depression"],
["T Wave", "Upright in most leads", "Repolarization β inversion = ischemia"],
["QT Interval", "< 440 ms men / < 460 ms women", "Lamba = Torsades risk (drugs, hypoK, hypoMg)"],
["QTc (corrected)", "Bazett: QT / βRR", "Correct for heart rate"],
]
pq_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), HEADER_BG),
('BACKGROUND', (0, 1), (-1, 1), INFO_BG),
('BACKGROUND', (0, 2), (-1, 2), LIGHT_GRAY),
('BACKGROUND', (0, 3), (-1, 3), INFO_BG),
('BACKGROUND', (0, 4), (-1, 4), LIGHT_GRAY),
('BACKGROUND', (0, 5), (-1, 5), INFO_BG),
('BACKGROUND', (0, 6), (-1, 6), LIGHT_GRAY),
('BACKGROUND', (0, 7), (-1, 7), INFO_BG),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), BLUE),
('FONTSIZE', (0, 1), (-1, -1), 8),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 6),
])
pq_tbl = Table(pq_data, colWidths=[3.5 * cm, 5.0 * cm, 9.0 * cm], style=pq_style)
elems.append(pq_tbl)
elems.append(Spacer(1, 0.2 * cm))
# Heart rate calculation box
hr_data = [[
Paragraph(
"<b>HEART RATE CALCULATION:</b> 300 Γ· (number of large boxes between R-R) "
"| 1 large box = 0.20 sec | 1 small box = 0.04 sec<br/>"
"<b>Speed memory:</b> 1 box=300 | 2=150 | 3=100 | 4=75 | 5=60 | 6=50",
ParagraphStyle('hr', fontSize=8, fontName='Helvetica',
textColor=DARK_GRAY, leading=12))
]]
hr_tbl = Table(hr_data, colWidths=[17.5 * cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), GOOD_BG),
('BOX', (0, 0), (-1, -1), 0.8, GREEN),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(hr_tbl)
return elems
def page_stemi(styles):
elems = [section_header("PAGE 2 β ST ELEVATION (STEMI)", styles, RED), Spacer(1, 0.15 * cm)]
# STEMI criteria
crit_data = [[
Paragraph(
"<b>STEMI CRITERIA (Washington Manual):</b> 2+ contiguous leads mein ST elevation needed<br/>"
"Men >40 yr: β₯2mm in V2-V3, β₯1mm others | Men <40 yr: β₯2.5mm in V2-V3 | Women: β₯1.5mm in V2-V3, β₯1mm others<br/>"
"<b>DOOR-TO-BALLOON TARGET: < 90 minutes β Activate Cath Lab IMMEDIATELY</b>",
ParagraphStyle('cr', fontSize=8, fontName='Helvetica',
textColor=DARK_GRAY, leading=12))
]]
crit_tbl = Table(crit_data, colWidths=[17.5 * cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), ALERT_BG),
('BOX', (0, 0), (-1, -1), 1.2, RED),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(crit_tbl)
elems.append(Spacer(1, 0.2 * cm))
# ST elevation waveform
wf = make_ecg(7 * cm, 2.6 * cm, 'st_elevation', color=RED, label='ST Elevation (STEMI)')
normal = make_ecg(7 * cm, 2.6 * cm, 'normal', label='Normal (compare)')
side_tbl = Table([[wf, Spacer(0.3 * cm, 1), normal]],
colWidths=[7.2 * cm, 0.3 * cm, 7.2 * cm],
style=TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP')]))
elems.append(side_tbl)
elems.append(Spacer(1, 0.2 * cm))
# Localization table
loc_data = [
[Paragraph("LOCATION", styles['TableHeader']),
Paragraph("LEADS", styles['TableHeader']),
Paragraph("ARTERY (Rosen's)", styles['TableHeader']),
Paragraph("RECIPROCAL CHANGES", styles['TableHeader'])],
["Anterior STEMI", "V1 β V4", "LAD (Left Anterior Descending)", "STβ in II, III, aVF"],
["Lateral STEMI", "I, aVL, V5, V6", "LCX (Left Circumflex)", "STβ in inferior leads"],
["Anterolateral", "V1βV6, I, aVL", "Proximal LAD / Left Main", "STβ inferior + right"],
["Inferior STEMI", "II, III, aVF", "RCA (Right Coronary Artery)", "STβ in I, aVL"],
["Right Ventricular", "V3R, V4R elevation", "Proximal RCA", "Check if Inferior STEMI"],
["Posterior STEMI", "V7-V9 elevation", "LCX (often missed!)", "STβ + Tall R in V1-V3"],
["aVR elevation", "aVR β + diffuse STβ", "Left Main / Proximal LAD", "Global subendocardial"],
]
loc_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), RED),
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#FDEDEC')),
('BACKGROUND', (0, 2), (-1, 2), LIGHT_GRAY),
('BACKGROUND', (0, 3), (-1, 3), colors.HexColor('#FDEDEC')),
('BACKGROUND', (0, 4), (-1, 4), LIGHT_GRAY),
('BACKGROUND', (0, 5), (-1, 5), colors.HexColor('#FDEDEC')),
('BACKGROUND', (0, 6), (-1, 6), LIGHT_GRAY),
('BACKGROUND', (0, 7), (-1, 7), ALERT_BG),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), RED),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
])
loc_tbl = Table(loc_data,
colWidths=[3.8 * cm, 3.0 * cm, 5.5 * cm, 5.2 * cm],
style=loc_style)
elems.append(loc_tbl)
elems.append(Spacer(1, 0.15 * cm))
# LBBB note
lbbb_data = [[
Paragraph(
"<b>NEW LBBB + Chest Pain = Treat as STEMI (Sgarbossa Criteria)</b><br/>"
"Concordant STβ >1mm | Discordant STβ >5mm | STβ >1mm in V1-V3",
ParagraphStyle('lb', fontSize=8, fontName='Helvetica',
textColor=DARK_GRAY, leading=11))
]]
lbbb_tbl = Table(lbbb_data, colWidths=[17.5 * cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), WARN_BG),
('BOX', (0, 0), (-1, -1), 0.8, ORANGE),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(lbbb_tbl)
return elems
def page_st_depression_t(styles):
elems = [section_header("PAGE 3 β ST DEPRESSION, T-WAVE CHANGES & PERICARDITIS", styles, PURPLE), Spacer(1, 0.15 * cm)]
wf_dep = make_ecg(5.3 * cm, 2.5 * cm, 'st_depression', color=ORANGE, label='ST Depression')
wf_tinv = make_ecg(5.3 * cm, 2.5 * cm, 't_inversion', color=PURPLE, label='T Inversion')
wf_peri = make_ecg(5.3 * cm, 2.5 * cm, 'pericarditis', color=BLUE, label='Pericarditis')
row_tbl = Table([[wf_dep, Spacer(0.2*cm,1), wf_tinv, Spacer(0.2*cm,1), wf_peri]],
colWidths=[5.5*cm, 0.2*cm, 5.5*cm, 0.2*cm, 5.5*cm],
style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP')]))
elems.append(row_tbl)
elems.append(Spacer(1, 0.2 * cm))
changes_data = [
[Paragraph("FINDING", styles['TableHeader']),
Paragraph("PATTERN", styles['TableHeader']),
Paragraph("THINK", styles['TableHeader']),
Paragraph("ACTION", styles['TableHeader'])],
["Horizontal STβ", "Flat depression β₯1mm", "NSTEMI / Unstable Angina", "Troponin, heparin, cardiology"],
["Downsloping STβ", "ST slopes down", "Significant ischemia", "Urgent cardiology review"],
["STβ V1-V3 only", "Isolated posterior", "Posterior STEMI!", "Add V7-V9 leads"],
["T inversion", "Inverted T wave", "Ischemia, PE, post-MI", "Echo, troponin, CTPA"],
["Wellens' Pattern", "Biphasic/deep Tβ V2-V3", "LAD critical stenosis", "Urgent angiography"],
["Hyperacute T", "Tall, peaked, symmetric", "Very early STEMI (mins)", "Serial ECG, troponin"],
["Diffuse STβ all leads", "Saddle-shaped + PRβ", "Pericarditis", "Echo, NSAIDs, colchicine"],
["CVA T-waves", "Deep wide T inversion", "Subarachnoid hemorrhage", "CT head, neurosurgery"],
]
ch_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), PURPLE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), PURPLE),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [LIGHT_GRAY, WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
])
ch_tbl = Table(changes_data, colWidths=[4.0*cm, 3.8*cm, 5.0*cm, 4.7*cm], style=ch_style)
elems.append(ch_tbl)
return elems
def page_supra_arrhythmias(styles):
elems = [section_header("PAGE 4 β SUPRAVENTRICULAR ARRHYTHMIAS (SVT, AF, FLUTTER)", styles, BLUE), Spacer(1, 0.15 * cm)]
wf_af = make_ecg(8.0*cm, 2.6*cm, 'af', color=BLUE, label='Atrial Fibrillation')
wf_fl = make_ecg(8.0*cm, 2.6*cm, 'flutter', color=PURPLE, label='Atrial Flutter')
row = Table([[wf_af, Spacer(0.3*cm,1), wf_fl]],
colWidths=[8.3*cm, 0.3*cm, 8.3*cm],
style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP')]))
elems.append(row)
elems.append(Spacer(1, 0.2*cm))
arr_data = [
[Paragraph("ARRHYTHMIA", styles['TableHeader']),
Paragraph("ECG FINDINGS", styles['TableHeader']),
Paragraph("STABLE TREATMENT", styles['TableHeader']),
Paragraph("UNSTABLE TREATMENT", styles['TableHeader'])],
["Sinus Tachycardia", "Normal P before QRS\nHR 100-150, regular", "Treat CAUSE (sepsis/fever/pain)\nNO specific antiarrhythmic", "β"],
["Atrial Fibrillation", "Irregularly irregular\nNo P waves, chaotic baseline\nVariable R-R intervals", "Rate control:\nMetoprolol 5mg IV\nor Diltiazem 0.25mg/kg IV\nAnticoag if >48hrs", "DC Cardioversion\n100-200J sync\nStat"],
["Atrial Flutter", "Regular sawtooth P waves\n300/min flutter, 150 ventricular\n2:1 or 4:1 block", "Rate control same as AF\nCardioversion 50-100J", "DC Cardioversion\n50-100J sync"],
["SVT (AVNRT)", "Narrow QRS tachycardia\nHR 150-250\nNo visible P waves", "Vagal maneuvers first\nAdenosine 6mg rapid IV\n(warn patient!)", "DC Cardioversion\n50-100J sync"],
["Sinus Bradycardia", "Normal P-QRS-T, HR<60\nRegular rhythm", "If asymptomatic: observe\nAtropine 0.5mg IV if symptomatic", "Pacing if refractory"],
]
arr_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), BLUE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), BLUE),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [INFO_BG, WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
])
arr_tbl = Table(arr_data, colWidths=[3.5*cm, 5.0*cm, 5.0*cm, 4.0*cm], style=arr_style)
elems.append(arr_tbl)
elems.append(Spacer(1, 0.15*cm))
# Adenosine warning
aden_data = [[
Paragraph(
"<b>β ADENOSINE WARNING:</b> WPW (Wolff-Parkinson-White) mein AVOID karo β AF with rapid conduction trigger ho sakti hai | "
"Always with crash cart ready | Central line se better, peripheral IV se bhi hota hai (rapid flush ke saath)",
ParagraphStyle('aw', fontSize=8, fontName='Helvetica', textColor=DARK_GRAY, leading=11))
]]
aden_tbl = Table(aden_data, colWidths=[17.5*cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), WARN_BG),
('BOX', (0, 0), (-1, -1), 0.8, ORANGE),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(aden_tbl)
return elems
def page_ventricular_arrhythmias(styles):
elems = [section_header("PAGE 5 β VENTRICULAR ARRHYTHMIAS (VT, VF, TORSADES) β HIGH ALERT!", styles, RED), Spacer(1, 0.15*cm)]
wf_vt = make_ecg(5.3*cm, 2.6*cm, 'vt', color=RED, label='Ventricular Tachycardia (VT)')
wf_vf = make_ecg(5.3*cm, 2.6*cm, 'vf', color=DARK_RED,label='Ventricular Fibrillation (VF)')
wf_tor = make_ecg(5.3*cm, 2.6*cm, 'torsades', color=PURPLE, label='Torsades de Pointes')
row = Table([[wf_vt, Spacer(0.2*cm,1), wf_vf, Spacer(0.2*cm,1), wf_tor]],
colWidths=[5.5*cm,0.2*cm,5.5*cm,0.2*cm,5.5*cm],
style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP')]))
elems.append(row)
elems.append(Spacer(1, 0.2*cm))
v_data = [
[Paragraph("RHYTHM", styles['TableHeader']),
Paragraph("ECG PATTERN", styles['TableHeader']),
Paragraph("PULSE?", styles['TableHeader']),
Paragraph("TREATMENT", styles['TableHeader'])],
["Monomorphic VT", "Wide QRS >0.12s\nRegular rate >100\nAV dissociation\nFusion/capture beats", "YES - Stable", "Amiodarone 150mg IV over 10min\nthen 1mg/min infusion"],
["Monomorphic VT", "Same wide QRS", "YES - Unstable\n(BPβ, syncope)", "SYNCHRONIZED DC Cardioversion\n100-200J biphasic"],
["Pulseless VT", "Wide complex, NO PULSE", "NO", "DEFIBRILLATE (unsynchronized)\n200J β CPR β 300J β CPR β 360J"],
["Ventricular Fibrillation", "Chaotic, no QRS\nNO PULSE", "NO", "DEFIBRILLATE 200J β CPR\nEpinephrine 1mg IV q3-5min\nAmiodarone 300mg IV"],
["Torsades de Pointes", "Twisting QRS amplitude\nPolymorphic, QT prolonged", "Usually YES", "MgSO4 2g IV over 5min STAT\nStop QT-prolonging drugs\nK+ >4.5 mEq/L target"],
["Accelerated Idioventricular", "Wide QRS, HR 60-100\nPost-MI reperfusion", "YES", "Usually benign\nReperfusion rhythm β observe"],
]
v_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), RED),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), RED),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.HexColor('#FDEDEC'), WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
])
v_tbl = Table(v_data, colWidths=[3.8*cm, 5.0*cm, 2.8*cm, 5.9*cm], style=v_style)
elems.append(v_tbl)
elems.append(Spacer(1, 0.15*cm))
# QT prolonging drugs
qt_data = [[
Paragraph(
"<b>COMMON QT-PROLONGING DRUGS IN ICU (Torsades Risk):</b><br/>"
"Amiodarone | Haloperidol | Methadone | Azithromycin | Fluconazole | Ondansetron | "
"Ciprofloxacin | Metronidazole | Quetiapine | TCA antidepressants | Hydroxychloroquine<br/>"
"<b>QTc >500ms = Stop offending drug + MgSO4 + KβΊ correction</b>",
ParagraphStyle('qt', fontSize=8, fontName='Helvetica', textColor=DARK_GRAY, leading=11))
]]
qt_tbl = Table(qt_data, colWidths=[17.5*cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), ALERT_BG),
('BOX', (0, 0), (-1, -1), 1.0, RED),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(qt_tbl)
return elems
def page_bradyarrhythmias(styles):
elems = [section_header("PAGE 6 β BRADYARRHYTHMIAS & HEART BLOCKS", styles, DARK_GREEN), Spacer(1, 0.15*cm)]
wf_chb = make_ecg(8.0*cm, 2.6*cm, 'chb', color=DARK_GREEN, label='Complete Heart Block (3rd Degree)')
wf_wen = make_ecg(8.0*cm, 2.6*cm, 'wenckebach', color=GREEN, label='Mobitz I (Wenckebach)')
row = Table([[wf_chb, Spacer(0.3*cm,1), wf_wen]],
colWidths=[8.3*cm, 0.3*cm, 8.3*cm],
style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP')]))
elems.append(row)
elems.append(Spacer(1, 0.2*cm))
hb_data = [
[Paragraph("BLOCK TYPE", styles['TableHeader']),
Paragraph("ECG FINDING", styles['TableHeader']),
Paragraph("RISK", styles['TableHeader']),
Paragraph("ICU ACTION", styles['TableHeader'])],
["1st Degree AV Block", "PR >0.20s (>5 small boxes)\nEvery P conducts normally", "Low\nUsually benign", "Monitor, check drugs\n(digoxin, beta-blockers)"],
["2nd Degree - Mobitz I (Wenckebach)", "PR progressively lengthens\nthen P wave drops (no QRS)\nPattern repeats", "Low-Moderate\nRarely progresses", "Observe, atropine if symptomatic\nCheck inferior MI"],
["2nd Degree - Mobitz II", "Fixed PR interval\nSuddenly P wave drops β no QRS\nNo warning!", "HIGH β can progress to CHB\nUnpredictable", "Transcutaneous pacing ready\nPermanent pacemaker indication\nCardiology STAT"],
["3rd Degree (Complete Heart Block)", "P waves & QRS completely INDEPENDENT\nP rate > QRS rate (dissociation)\nEscape rhythm: 30-50 bpm", "CRITICAL\nLow cardiac output", "Emergency pacing STAT\nAtropine 0.5-1mg IV (temp)\nDopamine/Epinephrine if needed"],
["Sinus Node Dysfunction", "Sinus pause >3s\nor HR <40 consistently", "Symptomatic bradycardia", "Atropine β pacing\nCheck hypothyroidism, drugs"],
]
hb_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), DARK_GREEN),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), DARK_GREEN),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [GOOD_BG, WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
# Highlight Mobitz II and CHB rows
('BACKGROUND', (0, 3), (-1, 3), colors.HexColor('#FEF5E7')),
('BACKGROUND', (0, 4), (-1, 4), ALERT_BG),
])
hb_tbl = Table(hb_data, colWidths=[4.0*cm, 5.0*cm, 3.5*cm, 5.0*cm], style=hb_style)
elems.append(hb_tbl)
elems.append(Spacer(1, 0.15*cm))
causes_data = [[
Paragraph(
"<b>ICU CAUSES OF BRADYCARDIA:</b> Beta-blocker/CCB overdose | Digoxin toxicity | "
"Inferior STEMI (RCA β SA/AV node ischemia) | Hypothyroidism | Hypothermia | "
"Raised ICP (Cushing reflex) | Vasovagal | Hyperkalemia | Post-cardiac surgery",
ParagraphStyle('bc', fontSize=8, fontName='Helvetica', textColor=DARK_GRAY, leading=11))
]]
causes_tbl = Table(causes_data, colWidths=[17.5*cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), GOOD_BG),
('BOX', (0, 0), (-1, -1), 0.8, GREEN),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
elems.append(causes_tbl)
return elems
def page_electrolytes(styles):
elems = [section_header("PAGE 7 β ELECTROLYTE ECG CHANGES (Harrison's Principles)", styles, ORANGE), Spacer(1, 0.15*cm)]
wf_hik = make_ecg(5.3*cm, 2.5*cm, 'hyperkalemia', color=RED, label='Hyperkalemia (KβΊ >5.5)')
wf_lok = make_ecg(5.3*cm, 2.5*cm, 'hypokalemia', color=BLUE, label='Hypokalemia (KβΊ <3.5)')
wf_qtl = make_ecg(5.3*cm, 2.5*cm, 'prolonged_qt', color=PURPLE,label='Prolonged QT (Caβ/Mgβ/Drugs)')
row = Table([[wf_hik, Spacer(0.2*cm,1), wf_lok, Spacer(0.2*cm,1), wf_qtl]],
colWidths=[5.5*cm,0.2*cm,5.5*cm,0.2*cm,5.5*cm],
style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP')]))
elems.append(row)
elems.append(Spacer(1, 0.2*cm))
el_data = [
[Paragraph("ELECTROLYTE", styles['TableHeader']),
Paragraph("ECG CHANGES (Sequence)", styles['TableHeader']),
Paragraph("TREATMENT", styles['TableHeader'])],
["Hyperkalemia KβΊ >5.5\n(CRITICAL >6.5)", "Early: Tall PEAKED (tented) T waves β FIRST SIGN\nModerate: PRβ, P wave flat/absent, wide QRS\nSevere: Sine wave pattern β Asystole / VF",
"1. Calcium gluconate 10% 10ml IV (membrane stabilize)\n2. Insulin 10U + Dextrose 50% 50ml IV\n3. Salbutamol nebulizer\n4. Kayexalate / Dialysis"],
["Hypokalemia KβΊ <3.5", "ST depression, T wave flat/inverted\nProminent U wave (after T wave)\nQT prolongation β Torsades risk\nPR prolongation",
"IV KCl: Max 10 mEq/hr peripheral\n20 mEq/hr central line\nTarget KβΊ >4.0 in cardiac patients\nAlways correct Mg2+ together"],
["Hypocalcemia Ca <8.5", "QT prolongation (ST segment lengthens)\nNo QRS change\nSevere: Cardiac arrest risk",
"IV Calcium gluconate 10ml 10%\nor Calcium chloride 5-10ml 10%\n(Central line preferred for CaCl)"],
["Hypercalcemia Ca >10.5", "QT shortening\nOsborn-like J point changes\nBradycardia, AV blocks\nBundle branch blocks",
"IV saline hydration\nFurosemide\nBisphosphonates\nDialysis if severe"],
["Hypomagnesemia Mg <1.7", "QT prolongation\nTorsades de Pointes\nFlattened T waves\nU waves",
"MgSO4 1-2g IV over 15-60min\nReplace KβΊ simultaneously\n(HypoMg causes refractory hypoK)"],
["Hypothermia Temp <32Β°C", "Osborn wave (J wave) β notch at J point\nBradycardia, QT prolongation\nPR/QRS widening\nVF risk at <28Β°C",
"Rewarm (active external/internal)\nManage VF with defibrillation\n(May need repeated shocks)"],
]
el_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), ORANGE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), ORANGE),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [WARN_BG, WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
# Highlight hyperkalemia (most critical)
('BACKGROUND', (0, 1), (-1, 1), ALERT_BG),
])
el_tbl = Table(el_data, colWidths=[3.5*cm, 7.2*cm, 6.8*cm], style=el_style)
elems.append(el_tbl)
return elems
def page_special_patterns(styles):
elems = [section_header("PAGE 8 β SPECIAL ICU PATTERNS (PE, TAMPONADE, DIGOXIN, HYPOTHERMIA)", styles, DARK_GRAY), Spacer(1, 0.15*cm)]
sp_data = [
[Paragraph("CONDITION", styles['TableHeader']),
Paragraph("ECG FINDINGS", styles['TableHeader']),
Paragraph("KEY DIFFERENTIATOR", styles['TableHeader']),
Paragraph("ACTION", styles['TableHeader'])],
["Pulmonary Embolism (PE)", "Sinus tachycardia (most common!)\nS1Q3T3 pattern (only 20%)\nNew RBBB\nT inversion V1-V4 (RV strain)\nAF (new onset)",
"S1Q3T3 = S wave in I\nQ wave in III, T inversion III",
"CTPA (gold standard)\nEchocardiography\nAnticoagulation\n(Thrombolysis if massive)"],
["Cardiac Tamponade", "Sinus tachycardia\nLow voltage (small QRS all leads)\nElectrical alternans (QRS size alternates beat-to-beat)\nPR segment flattening",
"Electrical alternans = highly specific\nConfirm with ECHO\nPulsus paradoxus clinically",
"Emergency pericardiocentesis\nEcho-guided preferred"],
["Digoxin Effect (Therapeutic)", "Scooped/Salvador Dali moustache STβ\nQT shortening\nT wave flattening/inversion\nBradycardia",
"Scooped ST = therapeutic\nNOT toxicity per se",
"Monitor digoxin level\nMaintain KβΊ >3.5"],
["Digoxin Toxicity", "Bradycardia + AV blocks\nBidirectional VT (pathognomonic!)\nPAT with block\nVT / VF",
"Bidirectional VT = digoxin until proven otherwise",
"Digoxin-Fab (Digibind) STAT\nNo cardioversion!\nPotassium correction"],
["Pericarditis", "Diffuse saddle-shaped STβ ALL leads\nPR depression (pathognomonic)\nNo reciprocal changes\nNo Q waves",
"Diffuse (not localized) STβ\nPR depression key!",
"NSAIDs + Colchicine\nEchocardiography\nRule out STEMI"],
["LVH (Left Ventricular Hypertrophy)", "Tall R in V5-V6 (>25mm)\nDeep S in V1-V2\nSTβ and T inversion V4-V6 (strain pattern)\nLAD (Left Axis Deviation)",
"Strain pattern mimics ischemia\nVoltage criteria needed",
"Echo for confirmation\nBP control\nMonitor for LBBB"],
["Early Repolarization", "J-point elevation V2-V5\nNotching at J-point\nConcave ST elevation\nNormal T waves",
"Young healthy patients\nAsymptomatic\nSt concave (vs STEMI oblique/convex)",
"Usually benign\nSerial ECG if symptoms\nRule out STEMI"],
["WPW (Wolff-Parkinson-White)", "Short PR (<0.12s)\nDelta wave (slurred QRS onset)\nWide QRS\nST/T changes secondary",
"Delta wave = key finding\nAF with WPW very dangerous",
"Avoid adenosine/digoxin/beta-blockers!\nProcainamide if AF+WPW\nRFA ablation"],
]
sp_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), DARK_GRAY),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), DARK_GRAY),
('FONTSIZE', (0, 0), (-1, -1), 7.2),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [LIGHT_GRAY, WHITE]),
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
])
sp_tbl = Table(sp_data, colWidths=[3.3*cm, 5.2*cm, 4.5*cm, 4.5*cm], style=sp_style)
elems.append(sp_tbl)
return elems
def page_emergency_table(styles):
elems = [section_header("PAGE 9 β ICU ECG EMERGENCY QUICK-ACTION TABLE", styles, RED), Spacer(1, 0.15*cm)]
# UNSTABLE vs STABLE banner
us_data = [[
Paragraph(
"<b>FIRST QUESTION ALWAYS: Patient STABLE hai ya UNSTABLE?</b><br/>"
"UNSTABLE = ANY of: BP <90/60 | Active chest pain | Altered sensorium | SpO2 <90% | Signs of shock<br/>"
"<b>If UNSTABLE β DC SHOCK / DEFIB / CPR PEHLE β phir diagnosis!</b>",
ParagraphStyle('us', fontSize=9, fontName='Helvetica', textColor=WHITE, leading=13))
]]
us_tbl = Table(us_data, colWidths=[17.5*cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), RED),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('LEFTPADDING', (0, 0), (-1, -1), 10),
('ROUNDEDCORNERS', [4,4,4,4]),
]))
elems.append(us_tbl)
elems.append(Spacer(1, 0.2*cm))
em_data = [
[Paragraph("ECG FINDING", styles['TableHeader']),
Paragraph("DIAGNOSIS", styles['TableHeader']),
Paragraph("IMMEDIATE ACTION", styles['TableHeader']),
Paragraph("TIME TARGET", styles['TableHeader'])],
["VF / Pulseless VT", "Cardiac Arrest\n(Shockable rhythm)", "CPR + DEFIBRILLATE 200J\nEpinephrine 1mg q3-5min\nAmiodarone 300mg IV", "< 2 min"],
["STEMI (STβ β₯2 leads)", "Acute MI\nCoronary occlusion", "Activate Cath Lab\nAspirin 300mg + Heparin\nMorphine / O2 / Nitrates", "Door-to-balloon < 90 min"],
["VT with pulse, UNSTABLE", "Hemodynamic compromise", "SYNCHRONIZED cardioversion\n100-200J biphasic", "< 5 min"],
["VT with pulse, STABLE", "Ventricular tachycardia", "Amiodarone 150mg IV over 10min\nIdentify & treat cause", "< 30 min"],
["Torsades de Pointes", "Polymorphic VT\n(Long QT)", "MgSO4 2g IV over 5 min STAT\nStop QT drugs, correct KβΊ", "< 10 min"],
["AF/Flutter UNSTABLE", "Rapid ventricular response\nHemodynamic compromise", "DC CARDIOVERSION\n100-200J (AF) / 50J (Flutter)", "< 5 min"],
["Complete Heart Block", "AV nodal failure\nEscape rhythm <50 bpm", "Atropine 0.5-1mg IV\nTranscutaneous pacing STAT\nCardiology consult", "< 5 min"],
["Tall peaked T waves", "Hyperkalemia\n(Cardiac arrest risk)", "Calcium gluconate 10ml 10% IV\nInsulin + Dextrose\nSalbutamol", "< 10 min"],
["Wide QRS + Bradycardia", "Hyperkalemia / Drug toxicity\nComplete heart block", "Calcium gluconate (if HyperK)\nStop offending drugs\nPacing if needed", "< 10 min"],
["S1Q3T3 + Tachycardia", "Pulmonary Embolism", "CTPA or Echo\nAnticoagulation (heparin)\nThrombolysis if massive", "< 60 min"],
["Low voltage + Electrical alternans", "Cardiac Tamponade", "Emergency pericardiocentesis\nEcho-guided\nVolume + vasopressors temp", "< 30 min"],
["Osborn (J) waves + Bradycardia", "Hypothermia", "Active rewarming\nVF β defibrillation\nHandle gently (VF risk)", "Continuous"],
]
em_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), RED),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('TEXTCOLOR', (0, 1), (0, -1), RED),
('FONTSIZE', (0, 0), (-1, -1), 7.5),
('TEXTCOLOR', (0, 1), (-1, -1), DARK_GRAY),
# Alternate rows
('ROWBACKGROUNDS', (0, 1), (-1, -1), [ALERT_BG, WHITE]),
# Very critical rows brighter
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#FADBD8')), # VF
('BACKGROUND', (0, 2), (-1, 2), colors.HexColor('#FADBD8')), # STEMI
('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#CCCCCC')),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
])
em_tbl = Table(em_data, colWidths=[4.0*cm, 3.5*cm, 6.3*cm, 3.0*cm], style=em_style)
elems.append(em_tbl)
elems.append(Spacer(1, 0.15*cm))
# Footer
foot_data = [[
Paragraph(
"Sources: Rosen's Emergency Medicine | Washington Manual of Medical Therapeutics | "
"Harrison's Principles of Internal Medicine 22e | Current Surgical Therapy 14e | "
"Miller's Anesthesia 10e | For educational use only β always confirm with supervising physician.",
ParagraphStyle('ft', fontSize=7, fontName='Helvetica', textColor=MID_GRAY,
alignment=TA_CENTER, leading=9))
]]
foot_tbl = Table(foot_data, colWidths=[17.5*cm], style=TableStyle([
('BACKGROUND', (0, 0), (-1, -1), LIGHT_GRAY),
('BOX', (0, 0), (-1, -1), 0.4, MID_GRAY),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 6),
]))
elems.append(foot_tbl)
return elems
# ββ Header / Footer callback βββββββββββββββββββββββββββββββββββββββββββββββββ
def on_page(canvas, doc):
"""Draw page header and footer on every page."""
canvas.saveState()
w, h = A4
# Top strip
canvas.setFillColor(HEADER_BG)
canvas.rect(0, h - 0.55*cm, w, 0.55*cm, fill=1, stroke=0)
canvas.setFont('Helvetica-Bold', 7.5)
canvas.setFillColor(WHITE)
canvas.drawString(1.5*cm, h - 0.40*cm, "ICU ECG PATTERN REFERENCE")
canvas.drawRightString(w - 1.5*cm, h - 0.40*cm, f"Page {doc.page}")
# Bottom strip
canvas.setFillColor(colors.HexColor('#EAECEE'))
canvas.rect(0, 0, w, 0.45*cm, fill=1, stroke=0)
canvas.setFont('Helvetica', 6.5)
canvas.setFillColor(MID_GRAY)
canvas.drawCentredString(w/2, 0.14*cm,
"Educational Reference Only | Confirm with Supervising Physician | Sources: Rosen's EM, Harrison's, Washington Manual")
canvas.restoreState()
# ββ Main builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_pdf(output_path):
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=1.2*cm, rightMargin=1.2*cm,
topMargin=1.0*cm, bottomMargin=0.8*cm,
title="ICU ECG Pattern Reference",
author="Orris Medical AI",
subject="Critical Care ECG Quick Reference"
)
styles = build_styles()
story = []
# Cover
story += build_cover(styles)
story.append(PageBreak())
# Page 1 - Normal ECG
story += page_normal_ecg(styles)
story.append(PageBreak())
# Page 2 - STEMI
story += page_stemi(styles)
story.append(PageBreak())
# Page 3 - ST depression / T changes
story += page_st_depression_t(styles)
story.append(PageBreak())
# Page 4 - Supraventricular
story += page_supra_arrhythmias(styles)
story.append(PageBreak())
# Page 5 - Ventricular arrhythmias
story += page_ventricular_arrhythmias(styles)
story.append(PageBreak())
# Page 6 - Bradyarrhythmias
story += page_bradyarrhythmias(styles)
story.append(PageBreak())
# Page 7 - Electrolytes
story += page_electrolytes(styles)
story.append(PageBreak())
# Page 8 - Special patterns
story += page_special_patterns(styles)
story.append(PageBreak())
# Page 9 - Emergency table
story += page_emergency_table(styles)
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
print(f"PDF created: {output_path}")
if __name__ == "__main__":
build_pdf("/home/daytona/workspace/ecg-reference/ICU_ECG_Reference.pdf")