Teach like a beginner how to read and interprete ECG
how to read ECG for beginners step by step guide

Each tiny square = 0.04 seconds (40 ms) wide, 1 mm tall
Each big square = 0.20 seconds (200 ms) wide, 5 mm tall
Standard speed = 25 mm per second
Standard height = 1 mV = 10 mm (10 small squares)


| Territory | Leads |
|---|---|
| Inferior wall | II, III, aVF |
| Anterior wall | V1 - V4 |
| Lateral wall | I, aVL, V5, V6 |
| Right ventricle | V1, V3R, V4R |
1 big box → 300 bpm
2 big boxes → 150 bpm
3 big boxes → 100 bpm
4 big boxes → 75 bpm
5 big boxes → 60 bpm
6 big boxes → 50 bpm
| Lead I | Lead aVF | Axis |
|---|---|---|
| Positive (up) | Positive (up) | Normal (-30° to +90°) |
| Positive (up) | Negative (down) | Left axis deviation |
| Negative (down) | Positive (up) | Right axis deviation |
| Negative (down) | Negative (down) | Extreme / Northwest |
| Interval | Normal | Too Long means... |
|---|---|---|
| PR | 0.12 - 0.20 s | AV block (1st, 2nd, 3rd degree) |
| QRS | < 0.12 s | Bundle branch block or ventricular rhythm |
| QTc | <440 ms (men), <460 ms (women) | Drug effect, electrolyte problem, risk of arrhythmia |
| Feature | Normal Value | Abnormal clue |
|---|---|---|
| Heart rate | 60-100 bpm | Tachy (>100) or Brady (<60) |
| PR interval | 0.12-0.20 s | Long = AV block; Short = pre-excitation |
| QRS duration | <0.12 s | Wide = BBB or ventricular rhythm |
| QTc | <440/460 ms | Long = arrhythmia risk |
| P wave | Upright in II, <2.5 mm, <0.12 s | Absent = AF; Wide = LAE; Tall = RAE |
| ST segment | Isoelectric | Elevation = STEMI; Depression = ischemia |
| T wave | Upright, same direction as QRS | Inverted = ischemia; Peaked = hyperK |
| Axis | -30° to +90° | LAD or RAD = pathology |
Generate a quick-reference PDF cheat sheet on ECG patterns
ls -la /home/daytona/workspace/ 2>/dev/null && mkdir -p /home/daytona/workspace/ecg-cheatsheet
~/ecg-cheatsheet/generate_ecg_cheatsheet.py
#!/usr/bin/env python3
"""
ECG Quick-Reference Cheat Sheet
Generates a professional, color-coded 2-page PDF cheat sheet.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus.flowables import Flowable
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
# ── Colour palette ──────────────────────────────────────────────────────────
C_NAVY = HexColor("#0D2137")
C_TEAL = HexColor("#0A7E8C")
C_RED = HexColor("#C0392B")
C_ORANGE = HexColor("#E67E22")
C_GREEN = HexColor("#1A7A4A")
C_PURPLE = HexColor("#6C3483")
C_YELLOW = HexColor("#F1C40F")
C_LBLUE = HexColor("#EBF5FB")
C_LGREY = HexColor("#F4F6F7")
C_LRED = HexColor("#FDEDEC")
C_LGREEN = HexColor("#EAFAF1")
C_LORANGE = HexColor("#FEF9E7")
C_LTEAL = HexColor("#E8F8F5")
C_LPURP = HexColor("#F5EEF8")
C_WHITE = colors.white
C_BLACK = colors.black
W, H = A4 # 210 x 297 mm
# ── Styles ───────────────────────────────────────────────────────────────────
def make_styles():
return {
"title": ParagraphStyle("title", fontName="Helvetica-Bold",
fontSize=18, textColor=C_WHITE,
alignment=TA_CENTER, spaceAfter=2),
"subtitle": ParagraphStyle("subtitle", fontName="Helvetica",
fontSize=9, textColor=HexColor("#D6EAF8"),
alignment=TA_CENTER),
"sec_hdr": ParagraphStyle("sec_hdr", fontName="Helvetica-Bold",
fontSize=9, textColor=C_WHITE,
alignment=TA_CENTER, spaceAfter=1,
spaceBefore=1),
"body": ParagraphStyle("body", fontName="Helvetica",
fontSize=7.5, leading=10,
textColor=C_NAVY, spaceAfter=2),
"body_sm": ParagraphStyle("body_sm", fontName="Helvetica",
fontSize=6.8, leading=9,
textColor=C_NAVY),
"bold_sm": ParagraphStyle("bold_sm", fontName="Helvetica-Bold",
fontSize=7.5, leading=10,
textColor=C_NAVY),
"red": ParagraphStyle("red", fontName="Helvetica-Bold",
fontSize=7.5, textColor=C_RED),
"green": ParagraphStyle("green", fontName="Helvetica-Bold",
fontSize=7.5, textColor=C_GREEN),
"teal": ParagraphStyle("teal", fontName="Helvetica-Bold",
fontSize=7.5, textColor=C_TEAL),
"footer": ParagraphStyle("footer", fontName="Helvetica",
fontSize=6, textColor=colors.grey,
alignment=TA_CENTER),
"wave_label": ParagraphStyle("wave_label", fontName="Helvetica-Bold",
fontSize=8, textColor=C_WHITE,
alignment=TA_CENTER),
"cell_hdr": ParagraphStyle("cell_hdr", fontName="Helvetica-Bold",
fontSize=7, textColor=C_WHITE,
alignment=TA_CENTER),
"cell_body": ParagraphStyle("cell_body", fontName="Helvetica",
fontSize=6.8, leading=9,
textColor=C_NAVY),
"cell_bold": ParagraphStyle("cell_bold", fontName="Helvetica-Bold",
fontSize=6.8, leading=9,
textColor=C_NAVY),
"cell_red": ParagraphStyle("cell_red", fontName="Helvetica-Bold",
fontSize=6.8, textColor=C_RED),
"cell_green": ParagraphStyle("cell_green", fontName="Helvetica-Bold",
fontSize=6.8, textColor=C_GREEN),
"mem": ParagraphStyle("mem", fontName="Helvetica-Oblique",
fontSize=7, textColor=C_PURPLE, leading=9),
}
S = make_styles()
# ── Section header banner ────────────────────────────────────────────────────
def section_header(text, bg=C_NAVY, fg=C_WHITE, width=None):
w = width or (W - 2*cm)
return Table(
[[Paragraph(text, ParagraphStyle("sh", fontName="Helvetica-Bold",
fontSize=9, textColor=fg,
alignment=TA_CENTER))]],
colWidths=[w],
style=TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
])
)
# ── Waveform ASCII art drawn with canvas ──────────────────────────────────
class ECGWaveformArt(Flowable):
"""Draws a simple ECG waveform sketch using reportlab canvas primitives."""
def __init__(self, width=160*mm, height=28*mm):
super().__init__()
self.width = width
self.height = height
def draw(self):
c = self.canv
w, h = self.width, self.height
base = h * 0.35 # baseline y
# background
c.setFillColor(C_NAVY)
c.rect(0, 0, w, h, fill=1, stroke=0)
# grid lines
c.setStrokeColor(HexColor("#1A3A5C"))
c.setLineWidth(0.3)
for i in range(0, int(w), int(5*mm)):
c.line(i, 0, i, h)
for j in range(0, int(h), int(5*mm)):
c.line(0, j, w, j)
# ECG trace
c.setStrokeColor(HexColor("#00E5FF"))
c.setLineWidth(1.4)
scale = mm
# baseline lead-in
pts = [
(5*scale, base),
(20*scale, base),
# P wave (small hump)
(22*scale, base),
(24*scale, base + 5*scale),
(26*scale, base),
# PR segment
(30*scale, base),
# Q dip
(31*scale, base - 2*scale),
# R peak
(32.5*scale, base + 16*scale),
# S dip
(34*scale, base - 3*scale),
# ST segment
(36*scale, base),
(42*scale, base),
# T wave
(44*scale, base),
(47*scale, base + 6*scale),
(50*scale, base),
# U wave (small)
(53*scale, base + 1.5*scale),
(55*scale, base),
# baseline
(65*scale, base),
# 2nd beat: P
(67*scale, base),
(69*scale, base + 5*scale),
(71*scale, base),
(75*scale, base),
(76*scale, base - 2*scale),
(77.5*scale, base + 16*scale),
(79*scale, base - 3*scale),
(81*scale, base),
(87*scale, base),
(89*scale, base),
(92*scale, base + 6*scale),
(95*scale, base),
(100*scale, base),
]
p = c.beginPath()
p.moveTo(*pts[0])
for x, y in pts[1:]:
p.lineTo(x, y)
c.drawPath(p, stroke=1, fill=0)
# Labels
c.setFont("Helvetica-Bold", 6.5)
c.setFillColor(HexColor("#FFD700"))
labels = [
("P", 24*scale, base + 7*scale),
("Q", 31*scale, base - 5.5*scale),
("R", 32.5*scale, base + 18*scale),
("S", 34*scale, base - 5.5*scale),
("T", 47*scale, base + 8.5*scale),
("U", 53*scale, base + 3.5*scale),
]
for lbl, lx, ly in labels:
c.drawCentredString(lx, ly, lbl)
# Interval arrows
c.setStrokeColor(HexColor("#FF6B6B"))
c.setLineWidth(0.7)
# PR interval arrow
arrow_y = base - 8*scale
c.line(22*scale, arrow_y, 30*scale, arrow_y)
c.drawCentredString(26*scale, arrow_y - 4*scale, "PR int.")
# QRS interval
arrow_y2 = base - 11*scale
c.setStrokeColor(HexColor("#90EE90"))
c.line(31*scale, arrow_y2, 36*scale, arrow_y2)
c.setFont("Helvetica-Bold", 6)
c.setFillColor(HexColor("#90EE90"))
c.drawCentredString(33.5*scale, arrow_y2 - 4*scale, "QRS")
# QT interval
c.setStrokeColor(HexColor("#FFA500"))
c.line(31*scale, 3*scale, 50*scale, 3*scale)
c.setFillColor(HexColor("#FFA500"))
c.drawCentredString(40.5*scale, 0.5*scale, "QT interval")
# ST segment label
c.setFillColor(HexColor("#FFFFFF"))
c.setFont("Helvetica", 6)
c.drawCentredString(39*scale, base + 3*scale, "ST")
# ── Page 1 content ───────────────────────────────────────────────────────────
def build_page1(S):
story = []
col_w = (W - 2*cm) / 2 - 2*mm
# ── HEADER BANNER ──────────────────────────────────────────────
header_table = Table(
[[Paragraph("ECG QUICK-REFERENCE CHEAT SHEET", S["title"]),
Paragraph("Harrison's / Goldberger | For Educational Use", S["subtitle"])]],
colWidths=[W - 2*cm],
style=TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("SPAN", (0,0), (-1,-1)),
])
)
story.append(header_table)
story.append(Spacer(1, 3*mm))
# ── WAVEFORM DIAGRAM ──────────────────────────────────────────
story.append(ECGWaveformArt(width=W - 2*cm, height=30*mm))
story.append(Spacer(1, 3*mm))
# ── WAVES & INTERVALS TABLE ───────────────────────────────────
story.append(section_header("WAVES, SEGMENTS & INTERVALS", C_TEAL))
story.append(Spacer(1, 1.5*mm))
wi_data = [
[Paragraph("Feature", S["cell_hdr"]),
Paragraph("Represents", S["cell_hdr"]),
Paragraph("Normal Values", S["cell_hdr"]),
Paragraph("Abnormal = Think...", S["cell_hdr"])],
[Paragraph("P wave", S["cell_bold"]),
Paragraph("Atrial depolarisation", S["cell_body"]),
Paragraph("< 0.12s wide\n< 2.5 mm tall\nUpright in II", S["cell_body"]),
Paragraph("Absent → AF\nTall → RAE\nWide/notched → LAE", S["cell_body"])],
[Paragraph("PR interval", S["cell_bold"]),
Paragraph("AV node conduction delay", S["cell_body"]),
Paragraph("0.12 – 0.20 s\n(3–5 small boxes)", S["cell_body"]),
Paragraph("Long → AV block\nShort → WPW / pre-excitation", S["cell_body"])],
[Paragraph("QRS complex", S["cell_bold"]),
Paragraph("Ventricular depolarisation", S["cell_body"]),
Paragraph("< 0.12 s narrow\n(< 3 small boxes)", S["cell_body"]),
Paragraph("Wide → BBB, V-tach\nDeep Q → old MI", S["cell_body"])],
[Paragraph("ST segment", S["cell_bold"]),
Paragraph("Ventricular plateau (isoelectric)", S["cell_body"]),
Paragraph("Flat at baseline\n(isoelectric)", S["cell_body"]),
Paragraph("Elevation ≥1mm → STEMI (emergency!)\nDepression → ischaemia / NSTEMI", S["cell_red"])],
[Paragraph("T wave", S["cell_bold"]),
Paragraph("Ventricular repolarisation", S["cell_body"]),
Paragraph("Upright, same dir. as QRS\nRounded", S["cell_body"]),
Paragraph("Inverted → ischaemia / BBB\nPeaked → Hyperkalaemia\nFlat → Hypokalaemia", S["cell_body"])],
[Paragraph("QTc interval", S["cell_bold"]),
Paragraph("Total ventricular electrical cycle", S["cell_body"]),
Paragraph("< 440 ms (men)\n< 460 ms (women)", S["cell_body"]),
Paragraph("Prolonged → Torsades risk\n(drugs, electrolytes, congenital)", S["cell_body"])],
[Paragraph("U wave", S["cell_bold"]),
Paragraph("Purkinje repolarisation", S["cell_body"]),
Paragraph("Small, same dir. as T\n(may be absent)", S["cell_body"]),
Paragraph("Prominent → Hypokalaemia", S["cell_body"])],
]
wi_table = Table(wi_data, colWidths=[2.5*cm, 3.5*cm, 3.5*cm, 5.5*cm])
wi_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_TEAL),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LBLUE]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("LINEBELOW", (0,0), (-1,0), 1, C_NAVY),
]))
story.append(wi_table)
story.append(Spacer(1, 3*mm))
# ── TWO COLUMNS: Heart Rate + Rhythm ──────────────────────────
# Heart rate column
hr_header = section_header("HEART RATE CALCULATION", C_ORANGE, width=col_w)
hr_data = [
[Paragraph("Large boxes (R-R)", S["cell_bold"]),
Paragraph("Rate (bpm)", S["cell_hdr"])],
[Paragraph("1", S["cell_body"]), Paragraph("300", S["cell_red"])],
[Paragraph("2", S["cell_body"]), Paragraph("150", S["cell_red"])],
[Paragraph("3", S["cell_body"]), Paragraph("100", S["cell_body"])],
[Paragraph("4", S["cell_body"]), Paragraph("75", S["cell_green"])],
[Paragraph("5", S["cell_body"]), Paragraph("60", S["cell_green"])],
[Paragraph("6", S["cell_body"]), Paragraph("50", S["cell_body"])],
]
hr_table = Table(hr_data, colWidths=[col_w*0.55, col_w*0.45])
hr_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_ORANGE),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LORANGE]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("ALIGN", (1,0), (1,-1), "CENTER"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
hr_note = Paragraph(
"<b>Irregular rhythm:</b> Count QRS in 10-sec strip × 6\n"
"<b>Normal:</b> 60–100 bpm <b>Brady:</b> <60 <b>Tachy:</b> >100",
S["body_sm"])
# Rhythm column
rhy_header = section_header("RHYTHM QUICK ID", C_PURPLE, width=col_w)
rhy_data = [
[Paragraph("Pattern", S["cell_hdr"]),
Paragraph("Likely Rhythm", S["cell_hdr"])],
[Paragraph("Regular, P before QRS, rate 60-100", S["cell_body"]),
Paragraph("Normal Sinus Rhythm", S["cell_green"])],
[Paragraph("No P waves, irregularly irregular", S["cell_body"]),
Paragraph("Atrial Fibrillation", S["cell_red"])],
[Paragraph("Sawtooth P waves ~300/min, 2:1 block", S["cell_body"]),
Paragraph("Atrial Flutter", S["cell_red"])],
[Paragraph("PR lengthens then dropped beat", S["cell_body"]),
Paragraph("Mobitz I (Wenckebach)", S["cell_body"])],
[Paragraph("Constant PR, then sudden dropped QRS", S["cell_body"]),
Paragraph("Mobitz II (dangerous)", S["cell_red"])],
[Paragraph("P & QRS march independently", S["cell_body"]),
Paragraph("Complete (3rd degree) Block", S["cell_red"])],
[Paragraph("Wide QRS, fast, regular", S["cell_body"]),
Paragraph("Ventricular Tachycardia (VT)", S["cell_red"])],
[Paragraph("No organised activity, wavy baseline", S["cell_body"]),
Paragraph("Ventricular Fibrillation (VF)", S["cell_red"])],
]
rhy_table = Table(rhy_data, colWidths=[col_w*0.58, col_w*0.42])
rhy_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_PURPLE),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LPURP]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
two_col = Table(
[[KeepTogether([hr_header, Spacer(1, 1.5*mm), hr_table, Spacer(1, 2*mm), hr_note]),
KeepTogether([rhy_header, Spacer(1, 1.5*mm), rhy_table])]],
colWidths=[col_w + 2*mm, col_w + 2*mm],
style=TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 3*mm),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
])
)
story.append(two_col)
return story
# ── Page 2 content ───────────────────────────────────────────────────────────
def build_page2(S):
story = []
story.append(PageBreak())
col_w = (W - 2*cm) / 2 - 2*mm
# ── AXIS TABLE ────────────────────────────────────────────────
story.append(section_header("QRS AXIS (Leads I & aVF)", C_TEAL))
story.append(Spacer(1, 1.5*mm))
ax_data = [
[Paragraph("Lead I", S["cell_hdr"]),
Paragraph("Lead aVF", S["cell_hdr"]),
Paragraph("Axis", S["cell_hdr"]),
Paragraph("Think of...", S["cell_hdr"])],
[Paragraph("↑ Positive", S["cell_body"]), Paragraph("↑ Positive", S["cell_body"]),
Paragraph("Normal (-30° to +90°)", S["cell_green"]), Paragraph("Normal heart", S["cell_body"])],
[Paragraph("↑ Positive", S["cell_body"]), Paragraph("↓ Negative", S["cell_body"]),
Paragraph("Left Axis Deviation (LAD)", S["cell_body"]),
Paragraph("LAFB, inferior MI, LVH", S["cell_body"])],
[Paragraph("↓ Negative", S["cell_body"]), Paragraph("↑ Positive", S["cell_body"]),
Paragraph("Right Axis Deviation (RAD)", S["cell_body"]),
Paragraph("RVH, PE, LPFB, tall/thin normal", S["cell_body"])],
[Paragraph("↓ Negative", S["cell_body"]), Paragraph("↓ Negative", S["cell_body"]),
Paragraph("Extreme / NW Axis", S["cell_red"]),
Paragraph("Ventricular tachycardia, dextrocardia", S["cell_body"])],
]
ax_table = Table(ax_data, colWidths=[3*cm, 3*cm, 5*cm, 4.5*cm])
ax_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_TEAL),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LTEAL]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
]))
story.append(ax_table)
story.append(Spacer(1, 3*mm))
# ── TWO COLUMNS: ST changes + Bundle Branch Blocks ───────────
# ST changes
st_header = section_header("ST CHANGES & ISCHAEMIA", C_RED, width=col_w)
st_data = [
[Paragraph("Finding", S["cell_hdr"]), Paragraph("Meaning", S["cell_hdr"])],
[Paragraph("ST elevation ≥ 1 mm\n(≥ 2 mm in V1-V3) in ≥2\nconsecutive leads",
S["cell_red"]),
Paragraph("STEMI — call code NOW\nEmergency revascularisation", S["cell_red"])],
[Paragraph("Diffuse ST elevation +\nPR depression", S["cell_body"]),
Paragraph("Pericarditis\n(saddle-shaped)", S["cell_body"])],
[Paragraph("ST depression\n(horizontal/downsloping)", S["cell_body"]),
Paragraph("NSTEMI / Unstable angina\nDigoxin effect (upsloping)", S["cell_body"])],
[Paragraph("Deep T-wave inversion\nV2-V3 (Wellens sign)", S["cell_red"]),
Paragraph("Critical LAD stenosis\nPre-infarction — urgent!", S["cell_red"])],
[Paragraph("ST depression + tall\nR wave in V1", S["cell_body"]),
Paragraph("Posterior MI (mirror image)\nGet posterior leads V7-V9", S["cell_body"])],
]
st_table = Table(st_data, colWidths=[col_w*0.5, col_w*0.5])
st_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_RED),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LRED]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
# Bundle Branch Blocks
bbb_header = section_header("BUNDLE BRANCH BLOCKS (BBB)", C_PURPLE, width=col_w)
bbb_data = [
[Paragraph("Feature", S["cell_hdr"]),
Paragraph("RBBB", S["cell_hdr"]),
Paragraph("LBBB", S["cell_hdr"])],
[Paragraph("QRS width", S["cell_bold"]),
Paragraph("≥ 0.12 s", S["cell_body"]),
Paragraph("≥ 0.12 s", S["cell_body"])],
[Paragraph("V1 pattern", S["cell_bold"]),
Paragraph("rSR' (\"bunny ears\")", S["cell_body"]),
Paragraph("QS or rS (W-shape)", S["cell_body"])],
[Paragraph("V6 pattern", S["cell_bold"]),
Paragraph("Wide S wave", S["cell_body"]),
Paragraph("Broad R (M-shape)", S["cell_body"])],
[Paragraph("Mnemonic", S["cell_bold"]),
Paragraph("WiRRy\n(Wide R in Right V1)", S["mem"]),
Paragraph("WiLLiaM\n(W in V1, M in V6)", S["mem"])],
[Paragraph("Clinical", S["cell_bold"]),
Paragraph("Often benign; watch for new RBBB", S["cell_body"]),
Paragraph("Do NOT diagnose MI on LBBB ECG", S["cell_red"])],
]
bbb_table = Table(bbb_data, colWidths=[col_w*0.3, col_w*0.35, col_w*0.35])
bbb_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_PURPLE),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LPURP]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
two_col = Table(
[[KeepTogether([st_header, Spacer(1, 1.5*mm), st_table]),
KeepTogether([bbb_header, Spacer(1, 1.5*mm), bbb_table])]],
colWidths=[col_w + 2*mm, col_w + 2*mm],
style=TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 3*mm),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
])
)
story.append(two_col)
story.append(Spacer(1, 3*mm))
# ── TWO COLUMNS: Electrolytes + Lead Territories ──────────────
# Electrolytes
elec_header = section_header("ELECTROLYTE ECG CHANGES", C_GREEN, width=col_w)
elec_data = [
[Paragraph("Electrolyte", S["cell_hdr"]),
Paragraph("ECG Finding", S["cell_hdr"])],
[Paragraph("Hyperkalaemia ↑K+", S["cell_bold"]),
Paragraph("Peaked T → wide QRS → sine wave → VF", S["cell_red"])],
[Paragraph("Hypokalaemia ↓K+", S["cell_bold"]),
Paragraph("Flat T, prominent U wave, long QT", S["cell_body"])],
[Paragraph("Hypercalcaemia ↑Ca2+", S["cell_bold"]),
Paragraph("Short QT interval", S["cell_body"])],
[Paragraph("Hypocalcaemia ↓Ca2+", S["cell_bold"]),
Paragraph("Prolonged QT interval", S["cell_body"])],
[Paragraph("Hypomagnesaemia ↓Mg2+", S["cell_bold"]),
Paragraph("Long QT, Torsades de Pointes risk", S["cell_red"])],
[Paragraph("Digoxin toxicity", S["cell_bold"]),
Paragraph("\"Salvador Dali\" ST (scooped), short QT,\nbradyarrhythmias", S["cell_body"])],
]
elec_table = Table(elec_data, colWidths=[col_w*0.42, col_w*0.58])
elec_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_GREEN),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LGREEN]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
# Lead territories
terr_header = section_header("LEAD TERRITORIES (MI Localisation)", C_ORANGE, width=col_w)
terr_data = [
[Paragraph("Territory", S["cell_hdr"]),
Paragraph("Leads", S["cell_hdr"]),
Paragraph("Artery", S["cell_hdr"])],
[Paragraph("Inferior", S["cell_bold"]),
Paragraph("II, III, aVF", S["cell_body"]),
Paragraph("RCA", S["cell_body"])],
[Paragraph("Anterior", S["cell_bold"]),
Paragraph("V1 – V4", S["cell_body"]),
Paragraph("LAD", S["cell_body"])],
[Paragraph("Lateral", S["cell_bold"]),
Paragraph("I, aVL, V5, V6", S["cell_body"]),
Paragraph("LCx", S["cell_body"])],
[Paragraph("Anterolateral", S["cell_bold"]),
Paragraph("V1–V6, I, aVL", S["cell_body"]),
Paragraph("LAD/LCx", S["cell_body"])],
[Paragraph("Posterior", S["cell_bold"]),
Paragraph("V7–V9 (posterior leads)\nMirror: tall R + ST dep in V1-V2", S["cell_body"]),
Paragraph("RCA/LCx", S["cell_body"])],
[Paragraph("RV infarct", S["cell_bold"]),
Paragraph("V3R, V4R (right-sided leads)", S["cell_body"]),
Paragraph("RCA (proximal)", S["cell_red"])],
]
terr_table = Table(terr_data, colWidths=[col_w*0.28, col_w*0.43, col_w*0.29])
terr_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_ORANGE),
("ROWBACKGROUNDS", (0,1), (-1,-1), [C_WHITE, C_LORANGE]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
two_col2 = Table(
[[KeepTogether([elec_header, Spacer(1, 1.5*mm), elec_table]),
KeepTogether([terr_header, Spacer(1, 1.5*mm), terr_table])]],
colWidths=[col_w + 2*mm, col_w + 2*mm],
style=TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 3*mm),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
])
)
story.append(two_col2)
story.append(Spacer(1, 3*mm))
# ── SYSTEMATIC APPROACH BANNER ────────────────────────────────
story.append(section_header("7-STEP SYSTEMATIC APPROACH", C_NAVY))
story.append(Spacer(1, 1.5*mm))
steps = [
("1", "RATE", "Count large boxes between R-R peaks; divide 300 by that number. Irregular? Count QRS in 10 s × 6."),
("2", "RHYTHM", "Are R-R intervals regular? Is there a P before every QRS? QRS after every P? → Normal sinus or arrhythmia?"),
("3", "AXIS", "Leads I and aVF: both positive = normal. I+/aVF- = LAD. I-/aVF+ = RAD. Both negative = extreme."),
("4", "INTERVALS", "PR: 0.12–0.20 s. QRS: <0.12 s. QTc: <440 ms men / <460 ms women. Long QRS = BBB."),
("5", "P WAVES", "Upright in II? Present before every QRS? Width/height normal? Abnormal → atrial issues or non-sinus rhythm."),
("6", "ST & T WAVES", "Flat ST baseline? Any elevation or depression? T wave direction and shape? This step catches MI and ischaemia."),
("7", "SPECIAL", "Hypertrophy criteria? Bundle branch block? Electrolyte pattern? Delta waves (WPW)? Clinical context?"),
]
step_data = [[
Paragraph(f"<b>{num}</b>", ParagraphStyle("sn", fontName="Helvetica-Bold",
fontSize=10, textColor=C_WHITE, alignment=TA_CENTER)),
Paragraph(f"<b>{name}</b>", ParagraphStyle("sn2", fontName="Helvetica-Bold",
fontSize=8, textColor=C_YELLOW)),
Paragraph(desc, S["cell_body"]),
] for num, name, desc in steps]
step_table = Table(step_data, colWidths=[0.8*cm, 2.5*cm, W - 2*cm - 3.3*cm])
step_table.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [C_NAVY, HexColor("#162944")]),
("GRID", (0,0), (-1,-1), 0.3, HexColor("#2C4A6A")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TEXTCOLOR", (2,0), (2,-1), C_WHITE),
]))
story.append(step_table)
story.append(Spacer(1, 2*mm))
# ── FOOTER ────────────────────────────────────────────────────
story.append(HRFlowable(width=W - 2*cm, thickness=0.5, color=colors.grey))
story.append(Spacer(1, 1*mm))
story.append(Paragraph(
"For educational use only. Not a substitute for clinical judgment. "
"Sources: Harrison's Principles of Internal Medicine 22E (2025) | "
"Goldberger's Clinical Electrocardiography 10E (2024)",
S["footer"]))
return story
# ── Page canvas decoration ───────────────────────────────────────────────────
def add_page_decorations(canvas_obj, doc):
"""Add page number and thin border to each page."""
canvas_obj.saveState()
# thin border
canvas_obj.setStrokeColor(C_NAVY)
canvas_obj.setLineWidth(1.5)
canvas_obj.rect(0.5*cm, 0.5*cm, W - 1*cm, H - 1*cm, stroke=1, fill=0)
# page number
canvas_obj.setFont("Helvetica", 7)
canvas_obj.setFillColor(colors.grey)
canvas_obj.drawRightString(W - 0.8*cm, 0.65*cm,
f"Page {doc.page}")
canvas_obj.restoreState()
# ── Main ─────────────────────────────────────────────────────────────────────
def main():
out_path = "/home/daytona/workspace/ecg-cheatsheet/ECG_QuickReference.pdf"
doc = SimpleDocTemplate(
out_path,
pagesize=A4,
leftMargin=1*cm,
rightMargin=1*cm,
topMargin=1*cm,
bottomMargin=1.2*cm,
title="ECG Quick-Reference Cheat Sheet",
author="Orris Medical Education",
subject="ECG Interpretation",
)
story = build_page1(S) + build_page2(S)
doc.build(story, onFirstPage=add_page_decorations,
onLaterPages=add_page_decorations)
print(f"PDF generated: {out_path}")
if __name__ == "__main__":
main()
cd /home/daytona/workspace/ecg-cheatsheet && python3 generate_ecg_cheatsheet.py 2>&1
~/ecg-cheatsheet/generate_ecg_cheatsheet.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-cheatsheet/generate_ecg_cheatsheet.py'
~/ecg-cheatsheet/generate_ecg_cheatsheet.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-cheatsheet/generate_ecg_cheatsheet.py'
~/ecg-cheatsheet/generate_ecg_cheatsheet.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/ecg-cheatsheet/generate_ecg_cheatsheet.py'
cd /home/daytona/workspace/ecg-cheatsheet && python3 generate_ecg_cheatsheet.py 2>&1
ls -lh /home/daytona/workspace/ecg-cheatsheet/ECG_QuickReference.pdf
ECG QuickReference
PDF Document · PDF
Calculate the ml/hr of the following: Dobutamine stock=250mg/5ml, patient's weight = 75kg. Dose=2mcg/kg/min
| Parameter | Value |
|---|---|
| Stock concentration | 250 mg / 5 ml |
| Patient weight | 75 kg |
| Prescribed dose | 2 mcg/kg/min |
| Step | Calculation | Result |
|---|---|---|
| Stock concentration | 250 mg ÷ 5 ml | 50 mg/ml = 50,000 mcg/ml |
| Required dose | 2 mcg × 75 kg | 150 mcg/min |
| Per hour | 150 × 60 | 9,000 mcg/hr |
| Infusion rate | 9,000 ÷ 50,000 | 0.18 ml/hr |
Clinical note: 0.18 ml/hr is an extremely low infusion rate that most standard syringe pumps can deliver, but always verify the pump's minimum delivery threshold (typically 0.1 ml/hr). If using a diluted solution (e.g. 250 mg in 250 ml = 1,000 mcg/ml), the rate would be 9 ml/hr - which is more practical for bedside administration. Always confirm with your institution's pharmacy protocol.
Create a reference table of common IV infusion calculations
mkdir -p /home/daytona/workspace/iv-infusion-ref
~/iv-infusion-ref/generate_iv_reference.py
#!/usr/bin/env python3
"""
IV Infusion Calculations - Clinical Reference Table PDF
Multi-page, color-coded, printable A4 reference.
"""
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.lib.colors import HexColor
from reportlab.pdfgen import canvas as rl_canvas
# ── Palette ──────────────────────────────────────────────────────────────────
NAVY = HexColor("#0D2137")
TEAL = HexColor("#0A7E8C")
DKBLUE = HexColor("#1A3A6C")
RED = HexColor("#C0392B")
ORANGE = HexColor("#D35400")
GREEN = HexColor("#1A7A4A")
PURPLE = HexColor("#6C3483")
MAROON = HexColor("#7B241C")
SLATE = HexColor("#2E4057")
GOLD = HexColor("#B7950B")
LBLUE = HexColor("#EBF5FB")
LTEAL = HexColor("#E8F8F5")
LRED = HexColor("#FDEDEC")
LGREEN = HexColor("#EAFAF1")
LORANGE = HexColor("#FEF5E7")
LPURP = HexColor("#F5EEF8")
LGREY = HexColor("#F4F6F7")
LYELLOW = HexColor("#FDFBE4")
WHITE = colors.white
W, H = landscape(A4) # 297 x 210 mm landscape
# ── Paragraph Styles ─────────────────────────────────────────────────────────
def ps(name, font="Helvetica", size=7.5, color=NAVY, align=TA_LEFT,
bold=False, leading=10, space_before=0, space_after=1):
fn = "Helvetica-Bold" if bold else font
return ParagraphStyle(name, fontName=fn, fontSize=size, textColor=color,
alignment=align, leading=leading,
spaceBefore=space_before, spaceAfter=space_after)
STYLES = {
"title": ps("title", size=15, color=WHITE, align=TA_CENTER, bold=True, leading=18),
"subtitle": ps("sub", size=8, color=HexColor("#AED6F1"), align=TA_CENTER),
"sec": ps("sec", size=8.5, color=WHITE, align=TA_CENTER, bold=True, leading=11),
"hdr": ps("hdr", size=7, color=WHITE, align=TA_CENTER, bold=True, leading=9),
"body": ps("body", size=7, leading=9.5),
"bodyc": ps("bodyc", size=7, leading=9.5, align=TA_CENTER),
"bold": ps("bold", size=7, bold=True, leading=9.5),
"boldc": ps("boldc", size=7, bold=True, leading=9.5, align=TA_CENTER),
"red": ps("red", size=7, color=RED, bold=True, leading=9.5),
"redc": ps("redc", size=7, color=RED, bold=True, leading=9.5, align=TA_CENTER),
"green": ps("green", size=7, color=GREEN, bold=True, leading=9.5),
"greenc": ps("greenc", size=7, color=GREEN, bold=True, leading=9.5, align=TA_CENTER),
"orange": ps("orange", size=7, color=ORANGE, bold=True, leading=9.5),
"formula": ps("formula",size=7.5,color=NAVY, bold=True, leading=11,
font="Courier-Bold"),
"formulac": ps("formulac",size=7,color=DKBLUE, bold=True, leading=10,
font="Courier-Bold", align=TA_CENTER),
"note": ps("note", size=6, color=HexColor("#555555"), leading=8),
"notec": ps("notec", size=6, color=HexColor("#555555"), leading=8, align=TA_CENTER),
"footer": ps("footer", size=6, color=colors.grey, align=TA_CENTER),
"warn": ps("warn", size=6.5,color=RED, bold=True, leading=9),
}
S = STYLES
# ── Helper: section banner ────────────────────────────────────────────────────
def banner(text, bg=NAVY, width=None):
w = width or (W - 1.6*cm)
return Table(
[[Paragraph(text, S["sec"])]],
colWidths=[w],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
("LEFTPADDING", (0,0),(-1,-1), 6),
("RIGHTPADDING", (0,0),(-1,-1), 6),
])
)
# ── Helper: make a styled data table ────────────────────────────────────────
def make_table(data, col_widths, header_bg=NAVY, row_colors=(WHITE, LGREY),
grid_color=HexColor("#BFC9CA"), v_pad=3, h_pad=4):
t = Table(data, colWidths=col_widths, repeatRows=1)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), header_bg),
("ROWBACKGROUNDS",(0,1), (-1,-1), row_colors),
("GRID", (0,0), (-1,-1), 0.4, grid_color),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), v_pad),
("BOTTOMPADDING", (0,0), (-1,-1), v_pad),
("LEFTPADDING", (0,0), (-1,-1), h_pad),
("RIGHTPADDING", (0,0), (-1,-1), h_pad),
("LINEBELOW", (0,0), (-1,0), 1, NAVY),
]))
return t
# ── Page canvas callback ──────────────────────────────────────────────────────
def page_deco(c, doc):
c.saveState()
c.setStrokeColor(NAVY)
c.setLineWidth(1.5)
c.rect(0.4*cm, 0.4*cm, W-0.8*cm, H-0.8*cm, stroke=1, fill=0)
c.setFont("Helvetica", 6.5)
c.setFillColor(colors.grey)
c.drawRightString(W-0.6*cm, 0.55*cm, f"Page {doc.page}")
c.drawString(0.6*cm, 0.55*cm, "IV Infusion Calculations Reference | For Educational Use Only")
c.restoreState()
# ══════════════════════════════════════════════════════════════════════════════
# CONTENT BUILDERS
# ══════════════════════════════════════════════════════════════════════════════
def page1_formulas(story):
"""Master formulas box + unit conversion table + drop rate table."""
# ── TITLE HEADER ──────────────────────────────────────────────────────
hdr = Table(
[[Paragraph("COMMON IV INFUSION CALCULATIONS", S["title"])],
[Paragraph(
"Clinical Reference Table | Nurses, Pharmacists & Medical Staff | "
"Always verify with local protocols and pharmacy",
S["subtitle"])]],
colWidths=[W - 1.6*cm],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), NAVY),
("TOPPADDING", (0,0),(-1,-1), 7),
("BOTTOMPADDING", (0,0),(-1,-1), 7),
])
)
story.append(hdr)
story.append(Spacer(1, 3*mm))
# ── SECTION 1: Core Formulas ───────────────────────────────────────────
story.append(banner("SECTION 1 — MASTER FORMULAS", TEAL))
story.append(Spacer(1, 2*mm))
# Two-column formula layout
LEFT_W = (W - 1.6*cm) * 0.5 - 3*mm
RIGHT_W = (W - 1.6*cm) * 0.5 - 3*mm
formulas_left = [
("ml/hr from mcg/kg/min",
"ml/hr = (Dose mcg/kg/min x Wt kg x 60)\n"
" ----------------------------------\n"
" Concentration mcg/ml",
"Catecholamines (dopamine, dobutamine,\nnoradrenaline, adrenaline)"),
("ml/hr from mcg/min",
"ml/hr = (Dose mcg/min x 60)\n"
" -----------------------\n"
" Concentration mcg/ml",
"Non-weight-based infusions\n(e.g. GTN, sodium nitroprusside)"),
("ml/hr from mg/hr",
"ml/hr = Dose mg/hr\n"
" ---------------\n"
" Concentration mg/ml",
"Morphine, midazolam, labetalol"),
("ml/hr from units/hr",
"ml/hr = Dose units/hr\n"
" -----------------\n"
" Concentration units/ml",
"Insulin, heparin, oxytocin"),
]
formulas_right = [
("Concentration (mg/ml)",
"Concentration = Total drug (mg)\n"
" -------------------\n"
" Total volume (ml)",
"First step in ALL calculations"),
("Dose check (reverse calc)",
"Dose = ml/hr x Concentration mcg/ml\n"
" ----------------------------------\n"
" 60 x Weight kg",
"Verify the running rate makes sense"),
("Drop rate (drops/min)",
"Drops/min = Volume (ml) x Drop factor\n"
" -----------------------------\n"
" Time (min)",
"Used when IV pump not available"),
("Infusion time (hours)",
"Time (hr) = Volume (ml)\n"
" ---------------\n"
" Rate (ml/hr)",
"Estimate when a bag will finish"),
]
def formula_block(title, formula, use):
return [
Paragraph(title, S["bold"]),
Paragraph(formula, S["formulac"]),
Paragraph(f"Use: {use}", S["note"]),
]
left_cells = [formula_block(t,f,u) for t,f,u in formulas_left]
right_cells = [formula_block(t,f,u) for t,f,u in formulas_right]
def formula_table(cells, bg, row_colors):
data = [[Paragraph("Formula", S["hdr"]),
Paragraph("Expression", S["hdr"]),
Paragraph("Common Use", S["hdr"])]]
for title, formula, use in [(t,f,u) for t,f,u in
(formulas_left if bg==TEAL else formulas_right)]:
data.append([
Paragraph(title, S["bold"]),
Paragraph(formula, S["formulac"]),
Paragraph(f"Use: {use}", S["note"]),
])
return make_table(data,
[LEFT_W*0.28, LEFT_W*0.42, LEFT_W*0.30],
header_bg=bg, row_colors=row_colors)
f_left = formula_table(left_cells, TEAL, (WHITE, LTEAL))
f_right = formula_table(right_cells, DKBLUE, (WHITE, LBLUE))
two_col = Table(
[[f_left, f_right]],
colWidths=[LEFT_W + 3*mm, RIGHT_W + 3*mm],
style=TableStyle([
("VALIGN", (0,0),(-1,-1), "TOP"),
("LEFTPADDING", (0,0),(-1,-1), 0),
("RIGHTPADDING",(0,0),(-1,-1), 3*mm),
("TOPPADDING", (0,0),(-1,-1), 0),
("BOTTOMPADDING",(0,0),(-1,-1), 0),
])
)
story.append(two_col)
story.append(Spacer(1, 3*mm))
# ── SECTION 2: Unit Conversions ────────────────────────────────────────
story.append(banner("SECTION 2 — UNIT CONVERSIONS & KEY EQUIVALENCES", SLATE))
story.append(Spacer(1, 2*mm))
UC_W = W - 1.6*cm
uc_data = [
[Paragraph("From", S["hdr"]),
Paragraph("To", S["hdr"]),
Paragraph("Multiply by", S["hdr"]),
Paragraph("Example", S["hdr"]),
Paragraph("", S["hdr"]), # separator
Paragraph("From", S["hdr"]),
Paragraph("To", S["hdr"]),
Paragraph("Multiply by", S["hdr"]),
Paragraph("Example", S["hdr"])],
[Paragraph("mg", S["body"]),
Paragraph("mcg (micrograms)", S["body"]),
Paragraph("x 1,000", S["boldc"]),
Paragraph("5 mg = 5,000 mcg", S["body"]),
Paragraph("", S["body"]),
Paragraph("mcg/min", S["body"]),
Paragraph("mcg/hr", S["body"]),
Paragraph("x 60", S["boldc"]),
Paragraph("10 mcg/min = 600 mcg/hr", S["body"])],
[Paragraph("mcg", S["body"]),
Paragraph("mg", S["body"]),
Paragraph("÷ 1,000", S["boldc"]),
Paragraph("2,500 mcg = 2.5 mg", S["body"]),
Paragraph("", S["body"]),
Paragraph("mg/min", S["body"]),
Paragraph("mg/hr", S["body"]),
Paragraph("x 60", S["boldc"]),
Paragraph("2 mg/min = 120 mg/hr", S["body"])],
[Paragraph("g", S["body"]),
Paragraph("mg", S["body"]),
Paragraph("x 1,000", S["boldc"]),
Paragraph("1 g = 1,000 mg", S["body"]),
Paragraph("", S["body"]),
Paragraph("units/min", S["body"]),
Paragraph("units/hr", S["body"]),
Paragraph("x 60", S["boldc"]),
Paragraph("3 units/min = 180 units/hr", S["body"])],
[Paragraph("mg", S["body"]),
Paragraph("g", S["body"]),
Paragraph("÷ 1,000", S["boldc"]),
Paragraph("500 mg = 0.5 g", S["body"]),
Paragraph("", S["body"]),
Paragraph("mcg/kg/min", S["body"]),
Paragraph("mcg/min (70 kg)", S["body"]),
Paragraph("x 70", S["boldc"]),
Paragraph("5 mcg/kg/min = 350 mcg/min", S["body"])],
[Paragraph("nanogram (ng)", S["body"]),
Paragraph("mcg", S["body"]),
Paragraph("÷ 1,000", S["boldc"]),
Paragraph("1,000 ng = 1 mcg", S["body"]),
Paragraph("", S["body"]),
Paragraph("% solution", S["body"]),
Paragraph("mg/ml", S["body"]),
Paragraph("x 10", S["boldc"]),
Paragraph("0.9% NaCl = 9 mg/ml NaCl", S["body"])],
]
cw = UC_W / 9
uc_col_w = [cw*1.1, cw*1.4, cw*0.9, cw*1.7, cw*0.2, cw*1.1, cw*1.4, cw*0.9, cw*1.3]
uc_table = make_table(uc_data, uc_col_w, header_bg=SLATE, row_colors=(WHITE, LGREY))
# Add vertical divider styling
uc_table.setStyle(TableStyle([
("BACKGROUND", (4,0), (4,-1), HexColor("#D0D3D4")), # separator col
("BACKGROUND", (0,0), (-1,0), SLATE),
("ROWBACKGROUNDS",(0,1), (-1,-1), (WHITE, LGREY)),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("LINEBELOW", (0,0), (-1,0), 1, NAVY),
]))
story.append(uc_table)
story.append(Spacer(1, 3*mm))
# ── SECTION 3: Drop Factor / Gravity Reference ────────────────────────
story.append(banner("SECTION 3 — GRAVITY DRIP RATE (Drops/min) REFERENCE", GOLD))
story.append(Spacer(1, 2*mm))
drop_intro = Table(
[[Paragraph(
"Formula: Drops/min = Volume (ml) x Drop Factor / Time (min) "
"| Standard drop factors: Macrodrip = 10, 15, or 20 gtts/ml "
"| Microdrip = 60 gtts/ml",
S["formulac"])]],
colWidths=[W - 1.6*cm],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), LYELLOW),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.8, GOLD),
])
)
story.append(drop_intro)
story.append(Spacer(1, 2*mm))
# Drop rate lookup table: volume vs time
volumes = [50, 100, 150, 250, 500, 1000]
times_hr = [0.5, 1, 2, 4, 6, 8]
df = 20 # drop factor for table
drop_hdr = [Paragraph("Volume (ml)", S["hdr"])]
for t in times_hr:
lbl = f"{int(t*60)} min" if t < 1 else f"{int(t)} hr"
drop_hdr.append(Paragraph(lbl, S["hdr"]))
drop_data = [drop_hdr]
for vol in volumes:
row = [Paragraph(str(vol), S["boldc"])]
for t in times_hr:
mins = t * 60
rate = (vol * df) / mins
style = S["redc"] if rate > 60 else (S["greenc"] if rate <= 30 else S["bodyc"])
row.append(Paragraph(f"{rate:.0f}", style))
drop_data.append(row)
drop_note_row = [
Paragraph("", S["note"]),
Paragraph("", S["note"]),
Paragraph("Red = >60 drops/min (difficult to count)", S["warn"]),
Paragraph("", S["note"]),
Paragraph("Green = ≤30 drops/min (easy to count)", S["note"]),
Paragraph("", S["note"]),
Paragraph("", S["note"]),
]
dcw = (W - 1.6*cm) / 7
drop_table = make_table(drop_data, [dcw]*7, header_bg=GOLD,
row_colors=(WHITE, LYELLOW))
story.append(Paragraph(
f"Drop rate lookup table (drop factor = {df} gtts/ml) — values in drops/min",
S["notec"]))
story.append(Spacer(1, 1*mm))
story.append(drop_table)
def page2_drugs(story):
"""Common IV drugs: standard dilutions, dose ranges, infusion rates."""
story.append(PageBreak())
# ── SECTION 4: Common IV Drug Reference ──────────────────────────────
story.append(banner("SECTION 4 — COMMON IV DRUG INFUSIONS: STANDARD DILUTIONS & DOSE RANGES", RED))
story.append(Spacer(1, 2*mm))
story.append(Paragraph(
"Note: Concentrations vary by institution. Always check local formulary. "
"Worked ml/hr values use a 70 kg patient for weight-based drugs.",
S["warn"]))
story.append(Spacer(1, 2*mm))
# Data: [Drug, Category, Stock, Standard dilution, Usual dose range,
# Concentration, Example ml/hr (70kg), Key nursing notes]
drug_data_raw = [
# VASOPRESSORS / INOTROPES
("Noradrenaline\n(Norepinephrine)", "Vasopressor",
"4 mg / 4 ml (1 mg/ml)", "4 mg in 48 ml NS = 50 ml\n(80 mcg/ml)",
"0.01–3 mcg/kg/min", "80 mcg/ml",
"0.01 x 70 x 60 / 80 = 0.5 ml/hr\n(at 0.01 mcg/kg/min)",
"Central line only. Titrate to MAP ≥65. Monitor for peripheral ischaemia."),
("Adrenaline\n(Epinephrine)", "Vasopressor/Inotrope",
"1 mg/ml ampoule", "1 mg in 49 ml NS = 50 ml\n(20 mcg/ml)",
"0.01–1 mcg/kg/min", "20 mcg/ml",
"0.1 x 70 x 60 / 20 = 21 ml/hr\n(at 0.1 mcg/kg/min)",
"Central line preferred. Risk of arrhythmia. Monitor lactate."),
("Dopamine", "Vasopressor/Inotrope",
"200 mg / 5 ml (40 mg/ml)", "200 mg in 45 ml NS = 50 ml\n(4,000 mcg/ml)",
"2–20 mcg/kg/min", "4,000 mcg/ml",
"5 x 70 x 60 / 4000 = 5.25 ml/hr\n(at 5 mcg/kg/min)",
"Low dose (2-5): renal. Mid (5-10): cardiac. High (>10): vasopressor."),
("Dobutamine", "Inotrope",
"250 mg / 5 ml (50 mg/ml)", "250 mg in 45 ml NS = 50 ml\n(5,000 mcg/ml)",
"2–20 mcg/kg/min", "5,000 mcg/ml",
"5 x 70 x 60 / 5000 = 4.2 ml/hr\n(at 5 mcg/kg/min)",
"May cause tachycardia. Monitor BP closely. Not for obstructive cardiomyopathy."),
("Vasopressin", "Vasopressor",
"20 units / ml", "20 units in 30 ml NS = 50 ml\n(0.4 units/ml)",
"0.01–0.04 units/min\n(fixed dose, NOT wt-based)", "0.4 units/ml",
"0.04 units/min x 60 / 0.4 = 6 ml/hr\n(at 0.04 units/min)",
"Fixed dose adjunct in septic shock. Mesenteric ischaemia risk."),
# ANTIARRHYTHMICS
("Amiodarone", "Antiarrhythmic",
"150 mg / 3 ml (50 mg/ml)", "300 mg in 250 ml D5W\n(1.2 mg/ml)",
"Load: 5 mg/kg over 1 hr\nMaint: 10-20 mg/kg/day", "1.2 mg/ml",
"Load 70 kg: 350 mg over 1 hr\n= 350/1.2 = 292 ml/hr x 1 hr",
"Vesicant — use central line for prolonged infusion. Prolongs QT. Check TFTs."),
("Lignocaine\n(Lidocaine)", "Antiarrhythmic",
"200 mg / 10 ml (20 mg/ml)", "1 g in 500 ml NS\n(2 mg/ml)",
"1–4 mg/min", "2 mg/ml",
"2 mg/min x 60 / 2 = 60 ml/hr", "Monitor for toxicity: tinnitus, seizures. Reduce in liver failure."),
# ANTICOAGULANTS
("Unfractionated\nHeparin", "Anticoagulant",
"1,000 units/ml or\n5,000 units/ml", "25,000 units in 50 ml NS\n(500 units/ml)",
"Per weight-based protocol\n(e.g. 18 units/kg/hr start)", "500 units/ml",
"18 x 70 / 500 = 2.5 ml/hr\n(initial rate)",
"Monitor APTT 6-hrly. Target APTT ratio 1.5-2.5. Adjust per protocol."),
# SEDATION / ANALGESIA
("Morphine", "Opioid Analgesic",
"10 mg / ml ampoule", "50 mg in 50 ml NS\n(1 mg/ml)",
"0–10 mg/hr (adult)\nOR PCA protocol", "1 mg/ml",
"5 mg/hr / 1 mg/ml = 5 ml/hr", "Titrate to pain score. Monitor respiratory rate, GCS. Naloxone on hand."),
("Fentanyl", "Opioid Analgesic",
"500 mcg / 10 ml\n(50 mcg/ml)", "500 mcg in 50 ml NS\n(10 mcg/ml)",
"25–200 mcg/hr", "10 mcg/ml",
"50 mcg/hr / 10 mcg/ml = 5 ml/hr", "ICU analgesia. Chest wall rigidity at high doses. Titrate cautiously."),
("Midazolam", "Benzodiazepine Sedative",
"5 mg/ml ampoule", "50 mg in 50 ml NS\n(1 mg/ml)",
"0.01–0.1 mg/kg/hr", "1 mg/ml",
"0.05 x 70 / 1 = 3.5 ml/hr\n(at 0.05 mg/kg/hr)",
"Titrate to RASS/Richmond scale. Accumulates in renal/hepatic failure."),
("Propofol", "IV Anaesthetic/Sedative",
"10 mg/ml emulsion\n(ready to use)", "Use undiluted\n(10 mg/ml)",
"0.5–4 mg/kg/hr (ICU)\n1–4 mg/kg/hr (GA)", "10 mg/ml",
"1 mg/kg/hr x 70 / 10 = 7 ml/hr\n(at 1 mg/kg/hr)",
"Propofol infusion syndrome at >4 mg/kg/hr > 48 hrs. Provides calories (1.1 kcal/ml)."),
# VASODILATORS
("GTN\n(Glyceryl Trinitrate)", "Vasodilator",
"5 mg / ml ampoule", "50 mg in 50 ml NS\n(1,000 mcg/ml) or\n50 mg in 500 ml = 100 mcg/ml",
"10–200 mcg/min", "1,000 mcg/ml\n(concentrated)\nOR 100 mcg/ml",
"100 mcg/min x 60 / 1000 = 6 ml/hr\n(concentrated bag)",
"Non-PVC tubing required. Tolerance after 24 hrs. Monitor BP closely."),
("Labetalol", "Alpha/Beta-Blocker",
"5 mg / ml ampoule", "200 mg in 200 ml NS\n(1 mg/ml)",
"0.5–2 mg/min", "1 mg/ml",
"1 mg/min x 60 / 1 = 60 ml/hr", "Hypertensive emergency/eclampsia. Avoid in asthma, severe bradycardia."),
# ELECTROLYTES / OTHERS
("Potassium Chloride\n(KCl)", "Electrolyte",
"1 mmol/ml (ready-to-use)", "NEVER give undiluted!\n20 mmol in 100 ml or 250 ml",
"Max 10 mmol/hr peripheral\nMax 20 mmol/hr central", "0.1 mmol/ml (100 ml bag)\nor 0.08 mmol/ml (250 ml)",
"10 mmol/hr: 100 ml/hr (peripheral)\n20 mmol/hr: 100 ml/hr (central, 250 ml bag)",
"FATAL if given undiluted IV push! Cardiac monitoring required >10 mmol/hr."),
("Insulin (Actrapid)", "Hormone",
"100 units/ml", "50 units in 50 ml NS\n(1 unit/ml)",
"Vary by BGL protocol\n(0.5–10 units/hr typical)", "1 unit/ml",
"2 units/hr / 1 unit/ml = 2 ml/hr", "BGL every 1-2 hr. Separate flush syringe. Insulin adheres to PVC tubing."),
("Magnesium Sulphate", "Electrolyte/Tocolytic",
"2 g / 10 ml\n(0.2 g/ml)", "8 g in 100 ml NS\n(0.08 g/ml)\nor per protocol",
"Loading: 4 g over 20 min\nMaint: 1–2 g/hr", "0.08 g/ml",
"1 g/hr: 12.5 ml/hr (in 0.08 g/ml bag)", "Eclampsia: loading dose 4 g IV. Monitor reflexes, resp rate, urine output. Antidote: calcium gluconate."),
("Oxytocin\n(Syntocinon)", "Uterotonic",
"10 units / ml", "30 units in 500 ml NS\n(0.06 units/ml)",
"Labour: 1–20 milliunits/min\nPost-partum: per protocol", "0.06 units/ml\n= 60 milliunits/ml",
"6 milliunits/min x 60 / 60 = 6 ml/hr\n(at 6 milliunits/min)",
"Electronic pump mandatory. Monitor uterine tone and CTG. Risk of uterine hyperstimulation."),
]
drug_hdr = [
Paragraph("Drug", S["hdr"]),
Paragraph("Category", S["hdr"]),
Paragraph("Stock / Ampoule", S["hdr"]),
Paragraph("Standard Dilution", S["hdr"]),
Paragraph("Usual Dose Range", S["hdr"]),
Paragraph("Working Conc.", S["hdr"]),
Paragraph("Example ml/hr (70 kg)", S["hdr"]),
Paragraph("Key Nursing Notes", S["hdr"]),
]
drug_rows = [drug_hdr]
CATS = {
"Vasopressor": (LRED, RED),
"Vasopressor/Inotrope": (LRED, RED),
"Inotrope": (LRED, RED),
"Antiarrhythmic": (LPURP, PURPLE),
"Anticoagulant": (LORANGE, ORANGE),
"Opioid Analgesic": (LTEAL, TEAL),
"Benzodiazepine Sedative": (LTEAL, TEAL),
"IV Anaesthetic/Sedative": (LTEAL, TEAL),
"Vasodilator": (LBLUE, DKBLUE),
"Alpha/Beta-Blocker": (LBLUE, DKBLUE),
"Electrolyte": (LGREEN, GREEN),
"Hormone": (LGREEN, GREEN),
"Uterotonic": (LYELLOW, GOLD),
}
for row in drug_data_raw:
drug, cat, stock, dilution, dose, conc, example, notes = row
drug_rows.append([
Paragraph(drug, S["bold"]),
Paragraph(cat, S["body"]),
Paragraph(stock, S["body"]),
Paragraph(dilution, S["formulac"]),
Paragraph(dose, S["body"]),
Paragraph(conc, S["boldc"]),
Paragraph(example, S["formulac"]),
Paragraph(notes, S["note"]),
])
total_w = W - 1.6*cm
d_col_w = [
total_w * 0.095, # Drug
total_w * 0.075, # Category
total_w * 0.100, # Stock
total_w * 0.115, # Dilution
total_w * 0.095, # Dose range
total_w * 0.085, # Concentration
total_w * 0.145, # Example ml/hr
total_w * 0.290, # Notes
]
drug_table = Table(drug_rows, colWidths=d_col_w, repeatRows=1)
# Build alternating row colors based on category
row_styles = [
("BACKGROUND", (0,0), (-1,0), RED),
("LINEBELOW", (0,0), (-1,0), 1, NAVY),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 3),
("RIGHTPADDING", (0,0), (-1,-1), 3),
]
prev_cat = None
for i, row in enumerate(drug_data_raw):
cat = row[1]
light_color, dark_color = CATS.get(cat, (LGREY, NAVY))
row_idx = i + 1 # 0 is header
row_styles.append(("BACKGROUND", (0, row_idx), (-1, row_idx), light_color))
# Thicker border when category changes
if cat != prev_cat and i > 0:
row_styles.append(("LINEABOVE", (0, row_idx), (-1, row_idx), 1.2, dark_color))
prev_cat = cat
drug_table.setStyle(TableStyle(row_styles))
story.append(drug_table)
def page3_worked(story):
"""Worked examples and common pitfalls."""
story.append(PageBreak())
# ── SECTION 5: Worked Examples ────────────────────────────────────────
story.append(banner("SECTION 5 — WORKED EXAMPLES (Step-by-Step)", GREEN))
story.append(Spacer(1, 2*mm))
EX_W = (W - 1.6*cm) / 3 - 2*mm
examples = [
{
"title": "Example 1 — Weight-Based (mcg/kg/min)",
"bg": LTEAL, "border": TEAL,
"steps": [
("Given", "Drug: Dobutamine\nStock: 250 mg/5 ml\nWeight: 80 kg\nDose: 5 mcg/kg/min"),
("Step 1: Concentration",
"250 mg / 5 ml = 50 mg/ml\n50 mg/ml x 1000 = 50,000 mcg/ml"),
("Step 2: Dose required/min",
"5 mcg/kg/min x 80 kg\n= 400 mcg/min"),
("Step 3: Dose/hr",
"400 mcg/min x 60 min\n= 24,000 mcg/hr"),
("Step 4: ml/hr",
"24,000 mcg/hr / 50,000 mcg/ml\n= 0.48 ml/hr"),
("Answer", "0.48 ml/hr\n(Consider diluting for practicality)"),
]
},
{
"title": "Example 2 — Non-Weight-Based (mcg/min)",
"bg": LBLUE, "border": DKBLUE,
"steps": [
("Given", "Drug: GTN\nDilution: 50 mg in 500 ml\nDose: 60 mcg/min"),
("Step 1: Concentration",
"50 mg / 500 ml = 0.1 mg/ml\n0.1 x 1000 = 100 mcg/ml"),
("Step 2: Dose/hr",
"60 mcg/min x 60 min\n= 3,600 mcg/hr"),
("Step 3: ml/hr",
"3,600 mcg/hr / 100 mcg/ml\n= 36 ml/hr"),
("Answer", "36 ml/hr"),
("Check", "36 ml/hr x 100 mcg/ml / 60\n= 60 mcg/min CORRECT"),
]
},
{
"title": "Example 3 — Units/hr (Heparin)",
"bg": LORANGE, "border": ORANGE,
"steps": [
("Given", "Drug: Heparin\nDilution: 25,000 u in 50 ml\nDose: 1,500 units/hr"),
("Step 1: Concentration",
"25,000 units / 50 ml\n= 500 units/ml"),
("Step 2: ml/hr",
"1,500 units/hr / 500 units/ml\n= 3 ml/hr"),
("Answer", "3 ml/hr"),
("Reverse check",
"3 ml/hr x 500 units/ml\n= 1,500 units/hr CORRECT"),
("Note", "Always reconcile with APTT\nresult per protocol"),
]
},
]
col_tables = []
for ex in examples:
rows = [[Paragraph(ex["title"],
ParagraphStyle("et", fontName="Helvetica-Bold",
fontSize=7.5, textColor=WHITE,
alignment=TA_CENTER))]]
rows_data = []
for label, content in ex["steps"]:
is_answer = label in ("Answer", "Check", "Reverse check")
rows_data.append([
Paragraph(label, S["bold"]),
Paragraph(content, S["formulac"] if "Step" in label or is_answer
else S["body"]),
])
step_table = Table(rows_data, colWidths=[EX_W*0.32, EX_W*0.68])
step_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), ex["bg"]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
# Highlight answer rows
("BACKGROUND", (0, len(rows_data)-2), (-1, len(rows_data)-2),
HexColor("#D5F5E3")),
]))
title_t = Table(
[[Paragraph(ex["title"], ParagraphStyle(
"et", fontName="Helvetica-Bold", fontSize=7.5,
textColor=WHITE, alignment=TA_CENTER))]],
colWidths=[EX_W],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), ex["border"]),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
])
)
col_tables.append([title_t, Spacer(1, 1*mm), step_table])
# Three-column layout
three_col = Table(
[[col_tables[0][0], col_tables[1][0], col_tables[2][0]],
[Spacer(1, 1*mm), Spacer(1, 1*mm), Spacer(1, 1*mm)],
[col_tables[0][2], col_tables[1][2], col_tables[2][2]]],
colWidths=[EX_W + 2*mm, EX_W + 2*mm, EX_W + 2*mm],
style=TableStyle([
("VALIGN", (0,0),(-1,-1), "TOP"),
("LEFTPADDING", (0,0),(-1,-1), 0),
("RIGHTPADDING", (0,0),(-1,-1), 3*mm),
("TOPPADDING", (0,0),(-1,-1), 0),
("BOTTOMPADDING",(0,0),(-1,-1), 0),
])
)
story.append(three_col)
story.append(Spacer(1, 3*mm))
# ── SECTION 6: Common Pitfalls ────────────────────────────────────────
story.append(banner("SECTION 6 — COMMON CALCULATION ERRORS & SAFETY TIPS", MAROON))
story.append(Spacer(1, 2*mm))
pitfalls = [
("1", "Unit mismatch",
"Mixing mg and mcg without converting. Always convert to a single unit before calculating.",
RED),
("2", "Forgetting the x 60",
"Dose is in mcg/min but formula needs mcg/hr. Always multiply by 60 for time conversion.",
RED),
("3", "Using total ampoule volume, not mg",
"Stock says '5 mg/5 ml'. Concentration = 1 mg/ml, NOT 5 mg/ml. Divide dose by volume.",
RED),
("4", "Not rechecking concentration after dilution",
"If you add 250 mg to a 50 ml bag you already had 45 ml in, final volume is 50 ml — double check.",
ORANGE),
("5", "Running weight-based drugs without weight",
"Always confirm actual body weight (not estimated). For obese patients, clarify if ABW or IBW.",
ORANGE),
("6", "KCl undiluted IV push",
"FATAL. Potassium chloride must ALWAYS be diluted. Never bolus undiluted KCl.",
RED),
("7", "Forgetting pump minimum rate",
"Most pumps cannot reliably deliver <0.1 ml/hr. If calculated rate is below this, dilute further.",
ORANGE),
("8", "Not doing a reverse check",
"After calculating ml/hr, work backwards: ml/hr x conc / 60 / weight = should equal original dose.",
GREEN),
]
pitfall_hdr = [
Paragraph("#", S["hdr"]),
Paragraph("Error Type", S["hdr"]),
Paragraph("What Goes Wrong & How to Avoid It", S["hdr"]),
]
pitfall_rows = [pitfall_hdr]
for num, etype, desc, col in pitfalls:
sty = ParagraphStyle("ps", fontName="Helvetica-Bold", fontSize=7,
textColor=col, leading=9.5)
pitfall_rows.append([
Paragraph(num, S["boldc"]),
Paragraph(etype, sty),
Paragraph(desc, S["body"]),
])
total_w = W - 1.6*cm
p_table = Table(pitfall_rows,
colWidths=[total_w*0.04, total_w*0.18, total_w*0.78])
p_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), MAROON),
("ROWBACKGROUNDS",(0,1), (-1,-1), (WHITE, LRED)),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BFC9CA")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("LINEBELOW", (0,0), (-1,0), 1, NAVY),
]))
story.append(p_table)
story.append(Spacer(1, 2*mm))
# ── REVERSE CHECK FORMULA ─────────────────────────────────────────────
rev_table = Table(
[[Paragraph(
"REVERSE CHECK (always verify your answer): "
"Dose = ml/hr x Concentration (mcg/ml) / 60 / Weight (kg) = should equal prescribed dose",
ParagraphStyle("rc", fontName="Courier-Bold", fontSize=8,
textColor=NAVY, alignment=TA_CENTER)
)]],
colWidths=[W - 1.6*cm],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), LYELLOW),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING", (0,0),(-1,-1), 5),
("BOX", (0,0),(-1,-1), 1.2, GOLD),
])
)
story.append(rev_table)
story.append(Spacer(1, 1.5*mm))
# Footer
story.append(HRFlowable(width=W-1.6*cm, thickness=0.5, color=colors.grey))
story.append(Spacer(1, 1*mm))
story.append(Paragraph(
"For educational purposes only. Not a substitute for clinical judgment, "
"institutional protocols, or pharmacist advice. "
"Doses and concentrations may vary by institution — always verify locally.",
S["footer"]))
# ── MAIN ─────────────────────────────────────────────────────────────────────
def main():
out = "/home/daytona/workspace/iv-infusion-ref/IV_Infusion_Reference.pdf"
doc = SimpleDocTemplate(
out,
pagesize=landscape(A4),
leftMargin=0.8*cm,
rightMargin=0.8*cm,
topMargin=0.8*cm,
bottomMargin=1.1*cm,
title="IV Infusion Calculations Reference",
author="Orris Medical Education",
subject="IV Drug Infusion Calculations",
)
story = []
page1_formulas(story)
page2_drugs(story)
page3_worked(story)
doc.build(story, onFirstPage=page_deco, onLaterPages=page_deco)
print(f"Done: {out}")
if __name__ == "__main__":
main()
cd /home/daytona/workspace/iv-infusion-ref && python3 generate_iv_reference.py 2>&1
ls -lh /home/daytona/workspace/iv-infusion-ref/IV_Infusion_Reference.pdf
IV Infusion Reference
PDF Document · PDF
What will be your best recommendations for NCLEX study
NCLEX study recommendations 2025 Next Generation NCLEX passing strategies
| Item Type | What It Tests | Notes |
|---|---|---|
| Multiple choice (single answer) | Core knowledge | Still the most common |
| Select All That Apply (SATA) | Recognizing multiple correct facts | Better to underselect than guess |
| Extended drag-and-drop | Prioritization, ordering | Read all options first |
| Matrix/Grid questions | Multiple decisions in one scenario | Very common in NGN |
| Bow-tie items | Linking conditions > actions > outcomes | Requires full clinical reasoning |
| Cloze/drop-down | Fill-in-the-blank from a dropdown | Tests precision |
| Extended case studies (6-item sets) | Unfolding patient scenarios | Unique to NGN - very high yield |
"The patient is anxious" → assess first "The patient is apneic" → act now
| Resource | Questions | Best For |
|---|---|---|
| UWorld | 5,000+ (750+ NGN) | Top-rated rationales, harder than real NCLEX = good prep |
| NCSBN Learning Extension | Official source | Most authentic NGN question format |
| Kaplan QBank | 3,800+ | Decision tree strategy, strong elimination practice |
| BoardVitals | 3,000+ | Good for NGN-specific item types |
| SimpleNursing | 1,200+ | Best for visual/auditory learners |
| Delegate to RN | Delegate to LPN/LVN | Delegate to CNA/PCT |
|---|---|---|
| Initial assessment | Stable, chronic conditions | ADLs (bathing, feeding) |
| Teaching | Routine medications (oral) | Vital signs (stable patients) |
| Unstable patients | Wound care (routine) | Ambulation (stable patients) |
| IV push medications | Catheter insertion | Collecting specimens |
| Care plan changes | NG tube care | Repositioning |
| Topic | Why High-Yield |
|---|---|
| Acid-base imbalances | Appears in respiratory, renal, OB, and pharmacology questions |
| Fluid & electrolytes | Hypo/hyperkalemia, sodium, calcium - tied to ECG changes |
| Sepsis & SIRS | Priority interventions, sepsis bundle |
| Therapeutic communication | Mental health + every single patient interaction question |
| Infection control | Isolation precautions, PPE selection - tested constantly |
| Lab values | Critical highs/lows you must act on immediately |
| Medication safety | 5 rights, high-alert meds (insulin, heparin, digoxin, warfarin) |
| Post-op complications | Hemorrhage, atelectasis, DVT, paralytic ileus |
| Prioritization frameworks | Every question is a prioritization question |
| Lab | Critical Low | Critical High | Action |
|---|---|---|---|
| K+ (Potassium) | <3.0 mEq/L | >6.0 mEq/L | ECG changes - notify immediately |
| Na+ (Sodium) | <120 mEq/L | >160 mEq/L | Seizure risk |
| Glucose | <50 mg/dL | >500 mg/dL | Hypoglycemia protocol / DKA |
| Hgb | <7 g/dL | - | Transfusion threshold |
| INR | - | >3.5 (non-therapeutic) | Bleeding risk |
| Digoxin | - | >2 ng/ml | Toxicity - hold dose |
| Lithium | - | >1.5 mEq/L | Toxicity - hold dose |
| pH (blood) | <7.35 | >7.45 | Acidosis / Alkalosis |
| O2 Sat | <90% | - | Supplemental O2 immediately |
| Week | Focus | Daily Goal |
|---|---|---|
| 1 | Fundamentals + Infection Control | 50 Q + 2 hrs content |
| 2 | Pharmacology (high-alert meds) | 75 Q + 2 hrs content |
| 3 | Cardio-Respiratory Med-Surg | 75 Q + 2 hrs content |
| 4 | Neuro, Renal, Endocrine | 75 Q + 1.5 hrs content |
| 5 | OB/Maternal + Pediatrics | 100 Q + 1.5 hrs content |
| 6 | Mental Health + Delegation | 100 Q + NGN practice |
| 7 | Full CAT simulations x2 | 150 Q timed simulations |
| 8 | Weak areas only + Rest | 75 Q targeted review |
"This is a safety exam. Think like a nurse, not a student."
Find all the available mnemonics for NCLEX prep
NCLEX nursing mnemonics complete list 2025 clinical judgment prioritization
https://goodnurse.com/article/189/the-best-nursing-mnemonics…
nursing mnemonics pharmacology electrolytes acid base mental health NCLEX complete
Over 100 mnemonics organized by category. Bookmark this page.
Assessment → Diagnosis → Planning → Implementation → Evaluation
Airway → Breathing → Circulation
Meals (physiological) → Safety → Affection (love/belonging) → Looks (esteem) → Own potential (self-actualization) → Winning (reaching full potential)
Situation → Background → Assessment → Recommendation
Signs & symptoms → Allergies → Medications → Past medical history → Last meal → Events leading to this visit
Onset → Location → Duration → Character → Aggravating factors → Relieving factors → Timing → Severity
Provocative/Palliative → Quality → Radiation → Severity → Timing
Problem → Intervention → Evaluation
Complex assessments → Unstable patients → Reaching diagnoses → Education (initial)
Do not delegate initial assessments Avoid delegating unstable patients No invasive procedures to CNAs Check the 5 rights of delegation Evaluate after delegating
Right task → Right circumstance → Right person → Right direction/communication → Right supervision
Clean dressings (non-sterile, routine) → Ambulation (stable patients) → Normal vital signs (stable) → Dressing assistance (ADLs) → Oral hygiene, feeding (non-tube)
| Suffix | Drug Class | Memory Hook |
|---|---|---|
| -pril | ACE inhibitors | "APRIL cough" - causes dry cough + hyperkalemia |
| -sartan | ARBs | "S-ARTAN skips the cough" - same as ACE but no cough |
| -lol | Beta-blockers | "LOL slows the heart" - bradycardia, low BP |
| -dipine | Calcium channel blockers | "DIP in the heart rate" |
| -statin | Statins | "STATIN Saves The Arteries" - monitor for myopathy |
| -prazole | Proton pump inhibitors | "PRAZOLE for the acid hole" - GERD/ulcers |
| -floxacin | Fluoroquinolones | "FLOX flops tendons" - tendon rupture risk |
| -azole | Antifungals | "AZOLE for the fungus hole" - monitor liver enzymes |
| -mycin/-micin | Aminoglycosides | "My-TOXIN" - nephrotoxic + ototoxic |
| -cillin | Penicillins | Check allergy - cross-reactive with cephalosporins |
| -olol | Beta-blockers | Same as -lol |
| -thiazide | Thiazide diuretics | "THIAZIDE Thirsty" - monitor potassium (wastes K+) |
| -pine | Calcium channel blockers | Peripheral edema side effect |
| -gliptin | DPP-4 inhibitors | Diabetes - pancreatitis risk |
| -mab | Monoclonal antibodies | Biologic agents - infection risk |
| -kinase | Thrombolytics | "Break the CLOT" - bleeding risk |
Morphine → Oxygen → Nitroglycerine → Aspirin
Lidocaine → Epinephrine → Atropine → Naloxone
IV access → Drug (Atropine) → External pacing → Advanced interventions (permanent pacemaker)
Morphine → AminoPhylline → Diuretics → Digoxin → Oxygen → Gases (ABG monitoring)
Digoxin toxicity = Diaphoresis, IGG (nausea/vomiting), Green-yellow halos in vision
Potassium chloride → Insulin → Narcotic opioids → Chemotherapy → Heparin
"Heparin Hates Protamine" - Heparin antidote = Protamine sulfate "Warfarin Wants Vitamin K" - Warfarin antidote = Vitamin K (+ FFP for urgent reversal) "Dabigatran Demands Idarucizumab" - Dabigatran antidote = Idarucizumab
Rapid-acting (Lispro/Aspart) → Intermediate (NPH) → A... → N... → Time (Glargine = no peak)
Facial twitching → Incoordination → Nausea/vomiting → Encephalopathy → Ataxia → Reflex changes → Tremors → Seizures
Foods high in K-vitamin = Avoid consistency changes Leafy greens, Evening primrose are high in Vitamin K - avoid drastic changes
Potassium Inside → Sodium Outside (the cell)
Muscle weakness → Urine (oliguria/anuria) → Respiratory distress → Decreased cardiac contractility → ECG changes (peaked T waves → wide QRS → sine wave) → Reflexes decreased
Alkalosis → Skeletal muscle weakness → Ileus (decreased bowel sounds) → Constipation → Weak pulse → Arrhythmias → Lethargy → Thready pulse
Lethargy → Leg cramps → Lax muscles (weakness) → Low, shallow respirations → Loss of bowel sounds → Lots of U-waves on ECG
Fever → Restlessness → Increased fluid retention → Edema → Decreased urination → Skin flushed → Agitation → Low-grade fever → Thirst (intense)
Seizures → Anorexia → Lethargy → Thyroid issues (rule out) → Low Na+ → Overload of free water → Stupor/confusion → Sodium replacement needed
Bone pain → Anorexia → Constipation → Kidney stones → Muscle weakness → ECG changes (short QT)
Convulsions → Arrhythmias → Tetany → Spasms (Chvostek's & Trousseau's signs)
Muscle weakness → Arrhythmias → GI symptoms → Is → Just → Loss of reflexes (early warning) → Obtunded → Weakness/respiratory arrest
Twitching → Torsades de pointes (fatal arrhythmia)
Risk → Injury → Failure → Loss (>4 weeks) → End-stage renal disease
Weight (daily) → Electrolytes → Intake & output → Grafts/fistula check → Hypotension risk → Symptoms of fluid overload
Respiratory Opposite → Metabolic Equal
Toxins (aspirin, methanol, ethanol overdose) → Insulin lack (DKA) → Chronic kidney disease → Shock (lactic acidosis)
Methanol → Uremia → DKA → Propylene glycol → Infection/sepsis → Lactic acidosis → Ethylene glycol → Salicylates
Drugs (opioids, sedatives) → Airway obstruction → Respiratory muscle failure → Emphysema/COPD
Liver failure → Oxygen (hypoxia compensation) → Brain injury → Emotional (anxiety, hyperventilation)
Character → History → ECG changes → ST changes → Time of onset → Precipitating factors → Associated symptoms → Intensity (0-10) → Nitro response
Facial drooping → Arm weakness → Speech difficulty → Time to call emergency
Balance → Eyes (vision changes) → Face → Arm → Speech → Time
Pitting edema → Right ventricle hypertrophy → Elevated JVP → Ascites → Signs of hypoxia → Exertional dyspnea → Respiratory changes → Edema (peripheral) → Ankle swelling → Distended neck veins → Hepatomegaly → Increased CVP → Split S2 → Tachycardia → Exercise intolerance → Xertional fatigue
Polyuria → Polydipsia → Polyphagia
- Hyperglycemia: Hot, dry, flushed skin (dehydrated)
- Hypoglycemia: Cool, clammy, diaphoretic (adrenergic response)
WiLLiaM = LBBB: W pattern in V1, M pattern in V6 MaRRoW = RBBB: M pattern in V1, W pattern in V6
Variable decelerations = Cord compression Early decelerations = Head compression Accelerations = OK (good sign) Late decelerations = Placental insufficiency (EMERGENCY - turn patient, O2, call MD)
Pain (severe, out of proportion) → Pallor → Pulselessness → Parasthesia → Paralysis
Extra: 6th P = Pressure (palpable tenseness over the compartment)
Protection → Rest → Ice → Compression → Elevation → Medication (analgesics)
Eye opening (1-4) + Verbal response (1-5) + Motor response (1-6) = Total 3-15
Alcohol/Drugs → Epilepsy (post-ictal) → Infection (meningitis, sepsis) → Overdose → Uremia (renal failure) → Trauma → Insulin (hypo/hyperglycemia) → Psychiatric → Stroke/Structural
Pupils Equal Round Reactive to Light and Accommodation
"On Old Olympus Towering Tops A Finn And German Viewed Some Hops" Olfactory, Optic, Oculomotor, Trochlear, Trigeminal, Abducens, Facial, Acoustic (Vestibulocochlear), Glossopharyngeal, Vagus, Spinal accessory, Hypoglossal
Female → Fat → Forty → Fertile → Fair-skinned (or Family history)
Bananas → Rice → Applesauce → Toast
Cobblestone appearance → Right lower quadrant pain → All layers of bowel affected (transmural) → Perianal fistulas/skip lesions
Pseudopolyps → Ulcers → Superficial mucosa only → Hemorrhage/bloody diarrhea
Breasts → Uterus → Bowel → Bladder → Lochia → Episiotomy/incision/extremities
Hemolysis → Elevated Liver enzymes → Low Platelets
Toxoplasmosis → Other (syphilis, HIV, Zika, parvovirus) → Rubella → Cytomegalovirus (CMV) → Herpes simplex
Tone (uterine atony - #1 cause) → Retained tissue (placenta) → Atonic uterus → Maternal coagulopathy → Placenta previa/accreta
Appearance (color) → Pulse (heart rate) → Grimace (reflex irritability) → Activity (muscle tone) → Respiration
Cord prolapse → Obstructed labor → Abruption (placental) → Transverse lie
Face → Legs → Activity → Cry → Consolability (scored 0-2 each, total 0-10)
Child can stack a tower of 6 blocks by age 2 years
Size (use weight-based dosing) → Airway (proportionally larger head = sniffing position) → Vein access → Epiglottis (larger, floppy) → Anxiety management → Cold (hypothermia risk) → Heart rate is key (not BP) → Infection (immature immunity) → Liver (immature metabolism) → Development (use age-appropriate communication)
Distractibility → Impulsivity → Grandiosity → Flight of ideas → Activity increased → Sleep decreased → Talkativeness (pressured speech)
Sex (male higher completion risk) → Age (elderly + adolescent) → Depression → Previous attempt (#1 predictor) → Ethanol/substance use → Rational thinking loss → Social support lacking → Organized plan → No spouse/partner → Sickness (chronic illness)
Amnesia (memory loss) → Aphasia (language) → Apraxia (motor skills) → Agnosia (recognition) → Attentional deficits (executive function)
Bedbound → Alcohol/substance abuse (caregiver) → Threats observed → Trauma signs → External control (isolated) → Dependence (financial/physical)
Threatening → Reassuring falsely → Advising → Probing
Hiding behind routine → Avoiding the patient's problem → Lecturing/moralizing → Telling what they should feel
Measles → Tuberculosis (TB) → Varicella (chickenpox)
Scarlet fever → Pertussis (whooping cough) → Influenza → Diphtheria → Epiglottitis → Rubella → Adenovirus → Mumps → Adrenal plague (pneumonic) → Neisseria meningitidis
MRSA → RSV → Skin infections (impetigo, scabies) → Wounds (draining) → Enteric infections (C. diff) → Ebola
Gown → Mask/respirator → Goggles/face shield → Gloves
Gloves → Goggles → Gown → Mask
Polyuria (excess urination) → Polydipsia (excess thirst) → Polyphagia (excess hunger)
DKA: BOTH Type 1 and Type 2, acidosis, ketones, glucose usually >250 HHS: Type 2 mainly, NO acidosis, NO ketones, glucose >600 (very high)
Change in bowel/bladder habits → Asore that doesn't heal → Unusual bleeding/discharge → Thickening/lump in breast or elsewhere → Indigestion or difficulty swallowing → Obvious change in a wart or mole → Nagging cough or hoarseness
Fever → Anemia → Murmur (new or changing) → Embolism (Janeway lesions, Osler nodes, splinter hemorrhages)
Drooling → Dysphagia → Dysphonia (muffled "hot potato" voice) → Distress (tripod position)
Pink Puffer = Emphysema (barrel chest, pursed-lip breathing, uses accessory muscles, thin) Blue Bloater = Chronic Bronchitis (cyanosis, productive cough, obese, edema)
Short-Acting Beta Agonist (albuterol) first in acute attack, then assess response
Right patient → Right drug → Right dose → Right route → Right time
White (clouds) over Green (grass) = White lead above Green lead Black (smoke) over Red (fire) = Black lead above Red lead Brown goes on the stomach (precordial leads)
Protection → Rest → Ice → Compression → Elevation
Therapeutic? → Harmful? → Informed consent? → Nurse competent? → Keep patient safe?
| Mnemonic | Topic |
|---|---|
| ABCs | Physiological prioritization |
| ADPIE | Nursing process |
| APGAR | Newborn scoring |
| A SIC WALT | Hypokalemia signs |
| AEIOU TIPS | Altered mental status |
| BACK ME | Hypercalcemia |
| BE-FAST | Stroke recognition |
| BLOCK 6 at 2 | Developmental milestone |
| BUBBLE | Postpartum assessment |
| CAUTION | Cancer warning signs |
| CATS | Hypocalcemia |
| COPD Pink/Blue | Emphysema vs bronchitis |
| DARE | Respiratory acidosis causes |
| DIG FAST | Mania signs |
| DKA vs HHS | Diabetic emergencies |
| DONNING/DOFFING | PPE order |
| FAME | Endocarditis |
| FAST/BE-FAST | Stroke |
| FFFFF | Gallstone risk |
| FRIED SALT | Hypernatremia |
| HELLP | Preeclampsia |
| Hot/Dry - Cold/Clammy | Hyper/hypoglycemia |
| IDEA | Bradycardia treatment |
| LEAN | ET tube emergency meds |
| LOBE | Respiratory alkalosis |
| MADD DOG | Pulmonary edema |
| MAG Is Just LOW | Hypermagnesemia |
| Maslow | Prioritization hierarchy |
| MONA | Acute MI care |
| MRS WEE | Contact precautions |
| MUDPILES | Anion gap acidosis |
| MURDER | Hyperkalemia |
| MTV | Airborne precautions |
| OLDCARTS | Symptom assessment |
| PERRLA | Pupil assessment |
| PINCH | High-alert medications |
| PISO | Electrolyte location |
| PQRST | Pain assessment |
| PRICE | Fracture first aid |
| ROME | ABG interpretation |
| SAD PERSONS | Suicide risk |
| SALT LOSS | Hyponatremia |
| SAMPLE | History taking |
| SBAR | Communication |
| SPIDERMAN | Droplet precautions |
| TICS | Metabolic acidosis causes |
| TORCH | Congenital infections |
| TRAMP | PPH causes |
| TRAP | Bad communication |
| VEAL CHOP | Fetal heart decelerations |
| WILLIAM MARROW | Bundle branch blocks |
| 5 P's | Compartment syndrome |
| 5 Rights | Medication safety |
| 6 L's | Hypokalemia |