Palpitations approach mwficine harrisosn flow chaet pdf
mkdir -p /tmp/workspace/palpitations && ls /tmp/workspace/palpitations/
~/palpitations/palpitations_flowchart.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import Paragraph
from reportlab.lib.enums import TA_CENTER, TA_LEFT
W, H = A4 # 595 x 842
# ── Color palette ──────────────────────────────────────────
DARK_BLUE = colors.HexColor("#1A3557")
MED_BLUE = colors.HexColor("#2E6DA4")
LIGHT_BLUE = colors.HexColor("#D6E8F7")
ORANGE = colors.HexColor("#E07B39")
ORANGE_LITE = colors.HexColor("#FDECD9")
GREEN = colors.HexColor("#2E7D53")
GREEN_LITE = colors.HexColor("#D4EDDA")
RED = colors.HexColor("#C0392B")
RED_LITE = colors.HexColor("#FAD7D3")
YELLOW = colors.HexColor("#F0C040")
YELLOW_LITE = colors.HexColor("#FEF9E7")
GRAY_LITE = colors.HexColor("#F0F4F8")
GRAY_MID = colors.HexColor("#B0BEC5")
WHITE = colors.white
BLACK = colors.black
def wrap_text(c, text, x, y, max_width, font, size, color=BLACK, align="center"):
"""Simple word-wrap text draw, returns final y."""
c.setFont(font, size)
c.setFillColor(color)
words = text.split()
line = ""
lines = []
for w in words:
test = (line + " " + w).strip()
if c.stringWidth(test, font, size) <= max_width:
line = test
else:
lines.append(line)
line = w
if line:
lines.append(line)
line_h = size * 1.35
total_h = len(lines) * line_h
start_y = y + total_h / 2 - line_h / 2
for i, l in enumerate(lines):
lw = c.stringWidth(l, font, size)
if align == "center":
c.drawString(x - lw / 2, start_y - i * line_h, l)
else:
c.drawString(x, start_y - i * line_h, l)
return start_y - len(lines) * line_h
def rounded_box(c, x, y, w, h, fill, stroke, text, font="Helvetica-Bold",
fsize=8.5, tcolor=BLACK, radius=8, text2="", fsize2=7.5):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(1.4)
c.roundRect(x, y, w, h, radius, fill=1, stroke=1)
# primary text
mid_x = x + w / 2
mid_y = y + h / 2 + (fsize * 0.6 if text2 else 0)
wrap_text(c, text, mid_x, mid_y, w - 10, font, fsize, tcolor)
if text2:
wrap_text(c, text2, mid_x, y + h / 2 - fsize2 * 0.5, w - 10,
"Helvetica", fsize2, tcolor)
def arrow(c, x1, y1, x2, y2, label="", color=DARK_BLUE):
c.setStrokeColor(color)
c.setLineWidth(1.3)
c.line(x1, y1, x2, y2)
# arrowhead
import math
angle = math.atan2(y2 - y1, x2 - x1)
al = 7
c.setFillColor(color)
p = c.beginPath()
p.moveTo(x2, y2)
p.lineTo(x2 - al * math.cos(angle - 0.4), y2 - al * math.sin(angle - 0.4))
p.lineTo(x2 - al * math.cos(angle + 0.4), y2 - al * math.sin(angle + 0.4))
p.close()
c.drawPath(p, fill=1, stroke=0)
if label:
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
c.setFont("Helvetica-Oblique", 7)
c.setFillColor(color)
lw = c.stringWidth(label, "Helvetica-Oblique", 7)
c.drawString(mx - lw / 2, my + 2, label)
def horiz_arrow(c, x1, y1, x2, label="", color=DARK_BLUE):
arrow(c, x1, y1, x2, y1, label, color)
def vert_arrow(c, x1, y1, y2, label="", color=DARK_BLUE):
arrow(c, x1, y1, x1, y2, label, color)
# ── Build PDF ──────────────────────────────────────────────
out = "/tmp/workspace/palpitations/Palpitations_Harrison_Flowchart.pdf"
c = canvas.Canvas(out, pagesize=A4)
# ══ PAGE 1 ─ APPROACH FLOWCHART ══════════════════════════
# Background
c.setFillColor(GRAY_LITE)
c.rect(0, 0, W, H, fill=1, stroke=0)
# Header bar
c.setFillColor(DARK_BLUE)
c.rect(0, H - 62, W, 62, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 17)
c.drawCentredString(W / 2, H - 30, "APPROACH TO PALPITATIONS")
c.setFont("Helvetica", 9)
c.drawCentredString(W / 2, H - 48, "Harrison's Principles of Internal Medicine, 22nd Ed (2025) — Chapter 45")
# ── ROW 0: Patient presents ──────────────────────────────
bw, bh = 220, 38
bx = W / 2 - bw / 2
by = H - 110
rounded_box(c, bx, by, bw, bh, MED_BLUE, DARK_BLUE,
"Patient Presents with PALPITATIONS",
fsize=9.5, tcolor=WHITE)
vert_arrow(c, W / 2, by, by - 20)
# ── ROW 1: History box ──────────────────────────────────
by1 = by - 20 - 72
bw1 = 460
bx1 = W / 2 - bw1 / 2
rounded_box(c, bx1, by1, bw1, 72, LIGHT_BLUE, MED_BLUE,
"HISTORY & CHARACTERISATION", fsize=9.5, tcolor=DARK_BLUE)
items_h = ["Character: thumping / pounding / fluttering",
"Pattern: intermittent vs sustained | regular vs irregular",
"Triggers: exercise, stress, position, substances",
"Associated Sx: syncope, presyncope, chest pain, dyspnea"]
c.setFont("Helvetica", 7.5)
c.setFillColor(DARK_BLUE)
for i, it in enumerate(items_h):
c.drawString(bx1 + 14, by1 + 58 - i * 13, u"\u2022 " + it)
vert_arrow(c, W / 2, by1, by1 - 16)
# ── ROW 2: Physical exam ─────────────────────────────────
by2 = by1 - 16 - 55
rounded_box(c, bx1, by2, bw1, 55, LIGHT_BLUE, MED_BLUE,
"PHYSICAL EXAMINATION", fsize=9.5, tcolor=DARK_BLUE)
items_p = ["Vital signs (HR, BP, temperature)",
"JVP and peripheral pulse character",
"Precordium / auscultation (murmurs, gallops)"]
c.setFont("Helvetica", 7.5)
c.setFillColor(DARK_BLUE)
for i, it in enumerate(items_p):
c.drawString(bx1 + 14, by2 + 44 - i * 13, u"\u2022 " + it)
vert_arrow(c, W / 2, by2, by2 - 16)
# ── ROW 3: Resting ECG ───────────────────────────────────
by3 = by2 - 16 - 38
bw3 = 220
bx3 = W / 2 - bw3 / 2
rounded_box(c, bx3, by3, bw3, 38, YELLOW_LITE, ORANGE,
"RESTING 12-LEAD ECG", fsize=9.5, tcolor=DARK_BLUE)
vert_arrow(c, W / 2, by3, by3 - 16)
# ── ROW 4: Decision — Arrhythmia documented? ─────────────
by4 = by3 - 16 - 42
bw4 = 260
bx4 = W / 2 - bw4 / 2
rounded_box(c, bx4, by4, bw4, 42, ORANGE_LITE, ORANGE,
"Arrhythmia documented on ECG?", fsize=9.5, tcolor=DARK_BLUE)
# YES branch (left)
arrow(c, bx4, by4 + 21, bx4 - 90, by4 + 21, "YES", ORANGE)
bwY = 130
bxY = bx4 - 90 - bwY
rounded_box(c, bxY, by4 + 3, bwY, 36, GREEN_LITE, GREEN,
"Treat specific arrhythmia", fsize=8.5, tcolor=GREEN)
# NO branch (right)
arrow(c, bx4 + bw4, by4 + 21, bx4 + bw4 + 90, by4 + 21, "NO", ORANGE)
bxN = bx4 + bw4 + 90
bwN = 130
rounded_box(c, bxN, by4 + 3, bwN, 36, ORANGE_LITE, ORANGE,
"Further monitoring required", fsize=8.5, tcolor=DARK_BLUE)
vert_arrow(c, W / 2, by4, by4 - 16)
# ── ROW 5: Risk stratification ───────────────────────────
by5 = by4 - 16 - 70
bw5 = 460
bx5 = W / 2 - bw5 / 2
rounded_box(c, bx5, by5, bw5, 70, RED_LITE, RED,
"RISK STRATIFICATION — High-Risk Features?", fsize=9.5, tcolor=RED)
risk_items = ["Structural heart disease / CAD / prior MI",
"Syncope or pre-syncope with palpitations",
"Angina / dyspnea during palpitation episode",
"Ventricular dysfunction, AS, HCM, or MS"]
c.setFont("Helvetica", 7.5)
c.setFillColor(RED)
for i, it in enumerate(risk_items):
c.drawString(bx5 + 14, by5 + 58 - i * 13, u"\u25B6 " + it)
# ── ROW 6: Two branches ──────────────────────────────────
vert_arrow(c, W / 2, by5, by5 - 16)
by6 = by5 - 16 - 75
# LEFT: High risk
bwL = 205
bxL = 30
rounded_box(c, bxL, by6, bwL, 75, RED_LITE, RED,
"HIGH RISK", fsize=9.5, tcolor=RED)
risk_mgmt = ["Urgent cardiology referral",
"Inpatient monitoring",
"Echo, EP study if indicated",
"Treat underlying cause"]
c.setFont("Helvetica", 7.5)
c.setFillColor(RED)
for i, it in enumerate(risk_mgmt):
c.drawString(bxL + 12, by6 + 63 - i * 13, u"\u2022 " + it)
arrow(c, W / 2 - 20, by5 - 16, bxL + bwL / 2, by6 + 75, "YES", RED)
# RIGHT: Low risk
bxR = W - 30 - bwL
rounded_box(c, bxR, by6, bwL, 75, GREEN_LITE, GREEN,
"LOW RISK", fsize=9.5, tcolor=GREEN)
low_mgmt = ["Ambulatory Holter (24-48 h)",
"Event / loop recorder (infrequent Sx)",
"Mobile cardiac telemetry",
"Exercise ECG if exertion-induced"]
c.setFont("Helvetica", 7.5)
c.setFillColor(GREEN)
for i, it in enumerate(low_mgmt):
c.drawString(bxR + 12, by6 + 63 - i * 13, u"\u2022 " + it)
arrow(c, W / 2 + 20, by5 - 16, bxR + bwL / 2, by6 + 75, "NO", GREEN)
# ── Footer ───────────────────────────────────────────────
c.setFillColor(DARK_BLUE)
c.rect(0, 0, W, 22, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica", 7)
c.drawCentredString(W / 2, 7, "Based on Harrison's Principles of Internal Medicine, 22nd Ed — Chapter 45: Palpitations (Joseph Loscalzo)")
c.showPage()
# ══ PAGE 2 ─ CAUSES & MONITORING TABLE ═══════════════════
c.setFillColor(GRAY_LITE)
c.rect(0, 0, W, H, fill=1, stroke=0)
# Header
c.setFillColor(DARK_BLUE)
c.rect(0, H - 62, W, 62, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 16)
c.drawCentredString(W / 2, H - 30, "CAUSES OF PALPITATIONS & MONITORING MODALITIES")
c.setFont("Helvetica", 9)
c.drawCentredString(W / 2, H - 48, "Harrison's Principles of Internal Medicine, 22nd Ed (2025)")
# ── Causes grid ──────────────────────────────────────────
causes = [
("CARDIOVASCULAR 43%", MED_BLUE, LIGHT_BLUE, [
"Premature atrial contractions (PAC)",
"Premature ventricular contractions (PVC)",
"Supraventricular tachycardias (SVT)",
"Ventricular tachyarrhythmias",
"Atrial fibrillation / flutter",
"Mitral valve prolapse",
"Aortic regurgitation (hyperdynamic)",
"Atrial myxoma",
"Pulmonary embolism",
"Myocarditis",
]),
("PSYCHIATRIC 31%", colors.HexColor("#7B3FA0"), colors.HexColor("#EDD9F7"), [
"Panic attacks / panic disorder",
"Generalized anxiety states",
"Somatization disorder",
"NOTE: Duration often >15 min",
"More associated symptoms",
]),
("MISCELLANEOUS 10%", ORANGE, ORANGE_LITE, [
"Thyrotoxicosis",
"Pheochromocytoma",
"Systemic mastocytosis",
"Post-COVID syndrome",
"Caffeine / alcohol / tobacco",
"Drugs: aminophylline, atropine,",
" thyroxine, cocaine, amphetamines",
"Chest wall muscle contractions",
"Athletes (endurance sports)",
]),
("UNKNOWN 16%", GRAY_MID, colors.HexColor("#ECEFF1"), [
"No identifiable cause found",
"After full workup",
]),
]
col_w = (W - 40) / 4
row_start = H - 90
box_h = 185
for i, (title, stroke_col, fill_col, items) in enumerate(causes):
bx_c = 20 + i * col_w
c.setFillColor(fill_col)
c.setStrokeColor(stroke_col)
c.setLineWidth(1.5)
c.roundRect(bx_c, row_start - box_h, col_w - 6, box_h, 8, fill=1, stroke=1)
c.setFillColor(stroke_col)
c.roundRect(bx_c, row_start - 28, col_w - 6, 28, 8, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 8)
tw = c.stringWidth(title, "Helvetica-Bold", 8)
c.drawString(bx_c + (col_w - 6 - tw) / 2, row_start - 18, title)
c.setFillColor(BLACK)
c.setFont("Helvetica", 7.5)
for j, it in enumerate(items):
c.drawString(bx_c + 8, row_start - 44 - j * 14, u"\u2022 " + it)
# ── Monitoring modalities ─────────────────────────────────
my = row_start - box_h - 30
c.setFillColor(DARK_BLUE)
c.setFont("Helvetica-Bold", 11)
c.drawCentredString(W / 2, my, "MONITORING MODALITIES (choose based on frequency of symptoms)")
my -= 15
mod_data = [
("Resting 12-Lead ECG", "Always first-line\nDocuments arrhythmia if present during recording", MED_BLUE, LIGHT_BLUE),
("Exercise ECG", "When symptoms triggered by exertion", GREEN, GREEN_LITE),
("Holter Monitor\n24-48 hours", "Daily or near-daily palpitations\nNote: Limited clinical utility for infrequent Sx", ORANGE, ORANGE_LITE),
("Event / Loop Recorder\n(External)", "Weekly or less frequent episodes\nPatient activates during symptoms", colors.HexColor("#7B3FA0"), colors.HexColor("#EDD9F7")),
("Implantable Loop Recorder", "Very infrequent / unexplained syncope+palpitations\nSafe, cost-effective; long-term monitoring", RED, RED_LITE),
("Mobile Cardiac Telemetry\n(Outpatient)", "Real-time remote ECG transmission\nPossibly most cost-effective for recurrent Sx", colors.HexColor("#1A7A4A"), GREEN_LITE),
]
mod_bw = (W - 40) / 3
mod_bh = 68
for i, (title, desc, stroke_c, fill_c) in enumerate(mod_data):
col = i % 3
row = i // 3
mx_box = 20 + col * mod_bw
my_box = my - row * (mod_bh + 8) - mod_bh
c.setFillColor(fill_c)
c.setStrokeColor(stroke_c)
c.setLineWidth(1.2)
c.roundRect(mx_box, my_box, mod_bw - 6, mod_bh, 7, fill=1, stroke=1)
c.setFillColor(stroke_c)
c.setFont("Helvetica-Bold", 8.5)
lines_t = title.split("\n")
for li, lt in enumerate(lines_t):
c.drawString(mx_box + 8, my_box + mod_bh - 14 - li * 11, lt)
c.setFillColor(BLACK)
c.setFont("Helvetica", 7.5)
lines_d = desc.split("\n")
for li, ld in enumerate(lines_d):
c.drawString(mx_box + 8, my_box + mod_bh - 14 - len(lines_t) * 11 - li * 11 - 4, u"\u2022 " + ld)
# ── Management notes ──────────────────────────────────────
note_y = my - 2 * (mod_bh + 8) - mod_bh - 22
c.setFillColor(YELLOW_LITE)
c.setStrokeColor(YELLOW)
c.setLineWidth(1.5)
c.roundRect(20, note_y - 72, W - 40, 72, 8, fill=1, stroke=1)
c.setFillColor(DARK_BLUE)
c.setFont("Helvetica-Bold", 9.5)
c.drawString(28, note_y - 16, "MANAGEMENT PRINCIPLES (Harrison's)")
c.setFont("Helvetica", 8)
notes = [
"Most patients do NOT have serious arrhythmias or structural heart disease — reassure once serious causes excluded.",
"Benign PACs/PVCs: beta-blocker therapy if sufficiently troubling to the patient.",
"Substance-induced (alcohol, tobacco, caffeine, cocaine): abstinence is the treatment.",
"Drug-induced: consider alternative agents when clinically possible.",
"Psychiatric causes: cognitive behavioural therapy and/or pharmacotherapy.",
"Patient diary or electronic marker essential for correlation with monitoring studies.",
]
c.setFillColor(BLACK)
for i, n in enumerate(notes):
c.drawString(28, note_y - 32 - i * 11, u"\u2022 " + n)
# Footer
c.setFillColor(DARK_BLUE)
c.rect(0, 0, W, 22, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica", 7)
c.drawCentredString(W / 2, 7, "Based on Harrison's Principles of Internal Medicine, 22nd Ed — Chapter 45: Palpitations (Joseph Loscalzo)")
c.showPage()
c.save()
print("PDF saved:", out)
cd /tmp/workspace/palpitations && python palpitations_flowchart.py
Palpitations Harrison Flowchart
PDF Document · PDF
| Category | Frequency | Examples |
|---|---|---|
| Cardiovascular | 43% | PAC, PVC, SVT, VT, AF, MVP, AR, PE |
| Psychiatric | 31% | Panic disorder, anxiety, somatization |
| Miscellaneous | 10% | Thyrotoxicosis, pheo, drugs, post-COVID |
| Unknown | 16% | No cause found |
Create a quick reference card (PDF) with differential diagnosis by rhythm pattern
~/palpitations/qrc_rhythm_ddx.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib import colors
from reportlab.pdfgen import canvas
import math
W, H = A4 # 595 x 842
# ── Palette ────────────────────────────────────────────────
C = {
"dark": colors.HexColor("#0D2B45"),
"navy": colors.HexColor("#1A3F63"),
"blue": colors.HexColor("#2E6DA4"),
"lblue": colors.HexColor("#D6EAF8"),
"teal": colors.HexColor("#117A65"),
"lteal": colors.HexColor("#D1F2EB"),
"purple": colors.HexColor("#6C3483"),
"lpurple": colors.HexColor("#E8DAEF"),
"orange": colors.HexColor("#CA6F1E"),
"lorange": colors.HexColor("#FDEBD0"),
"red": colors.HexColor("#A93226"),
"lred": colors.HexColor("#FADBD8"),
"green": colors.HexColor("#196F3D"),
"lgreen": colors.HexColor("#D5F5E3"),
"gold": colors.HexColor("#B7950B"),
"lgold": colors.HexColor("#FEF9E7"),
"gray": colors.HexColor("#717D7E"),
"lgray": colors.HexColor("#F2F3F4"),
"white": colors.white,
"black": colors.black,
}
def rbox(c, x, y, w, h, fill, stroke, r=6, lw=1.2):
c.setFillColor(fill); c.setStrokeColor(stroke); c.setLineWidth(lw)
c.roundRect(x, y, w, h, r, fill=1, stroke=1)
def hdr(c, x, y, w, h, fill, text, fsize=8.5, tcolor=None):
tcolor = tcolor or C["white"]
rbox(c, x, y, w, h, fill, fill, r=5)
c.setFillColor(tcolor); c.setFont("Helvetica-Bold", fsize)
c.drawCentredString(x + w/2, y + h/2 - fsize*0.35, text)
def bullet(c, x, y, text, fsize=7, color=None, indent=6, sym=u"\u2022"):
color = color or C["black"]
c.setFont("Helvetica", fsize); c.setFillColor(color)
c.drawString(x, y, sym + " " + text)
def tag(c, x, y, text, fill, tcolor=None, fsize=6.5):
tcolor = tcolor or C["white"]
tw = c.stringWidth(text, "Helvetica-Bold", fsize) + 8
rbox(c, x, y - fsize, tw, fsize + 4, fill, fill, r=3, lw=0)
c.setFillColor(tcolor); c.setFont("Helvetica-Bold", fsize)
c.drawString(x + 4, y - fsize*0.2, text)
return tw + 4
# ═══════════════════════════════════════════════════════════
# PAGE 1 — Main Quick Reference Card
# ═══════════════════════════════════════════════════════════
out = "/tmp/workspace/palpitations/Palpitations_Rhythm_DDx_QRC.pdf"
cv = canvas.Canvas(out, pagesize=A4)
# Background
cv.setFillColor(C["lgray"]); cv.rect(0, 0, W, H, fill=1, stroke=0)
# ── HEADER ─────────────────────────────────────────────────
cv.setFillColor(C["dark"]); cv.rect(0, H-58, W, 58, fill=1, stroke=0)
cv.setFillColor(C["white"]); cv.setFont("Helvetica-Bold", 16)
cv.drawCentredString(W/2, H-26, "PALPITATIONS: DIFFERENTIAL DIAGNOSIS BY RHYTHM PATTERN")
cv.setFont("Helvetica", 8)
cv.drawCentredString(W/2, H-44, "Quick Reference Card | Based on Harrison's 22e, Braunwald's Heart Disease, Goldman-Cecil Medicine")
# ── COLUMN LAYOUT: 3 equal cols ───────────────────────────
MARGIN = 10
GAP = 7
col_w = (W - 2*MARGIN - 2*GAP) / 3
col_x = [MARGIN, MARGIN + col_w + GAP, MARGIN + 2*(col_w + GAP)]
TOP = H - 68
# ────────────────────────────────────────────────────────────
# SECTION 1: INTERMITTENT SINGLE BEATS (col 0)
# ────────────────────────────────────────────────────────────
y = TOP
sh = 17 # section header height
rbox(cv, col_x[0], y - 175, col_w, 175, C["lblue"], C["blue"], r=7)
hdr(cv, col_x[0], y - sh, col_w, sh, C["blue"],
"INTERMITTENT / SINGLE BEATS", 8)
rows = [
("Premature Atrial Contraction", "PAC", C["blue"],
["\"Skipped beat\" or flip-flop sensation",
"Pause followed by forceful beat",
"Common, usually benign",
"Worse with caffeine/stress/fatigue"]),
("Premature Ventricular Contraction", "PVC", C["navy"],
["Similar to PAC but often more forceful",
"\"Thump\" or \"kick\" in chest",
"Post-extrasystolic potentiation",
"High burden (>10%) may cause CMP"]),
]
ry = y - sh - 6
for name, abbr, acol, pts in rows:
cv.setFont("Helvetica-Bold", 7.5); cv.setFillColor(acol)
cv.drawString(col_x[0]+6, ry, name + f" [{abbr}]")
ry -= 2
for pt in pts:
bullet(cv, col_x[0]+8, ry, pt, fsize=7, color=C["dark"])
ry -= 11
ry -= 4
# ── ECG clue box
ry -= 2
cv.setFillColor(C["lgold"]); cv.setStrokeColor(C["gold"]); cv.setLineWidth(1)
cv.roundRect(col_x[0]+4, ry-26, col_w-8, 26, 4, fill=1, stroke=1)
cv.setFillColor(C["gold"]); cv.setFont("Helvetica-Bold", 7)
cv.drawString(col_x[0]+8, ry-10, u"\u26A1 ECG: Isolated early beats; compensatory pause")
cv.setFont("Helvetica", 7); cv.setFillColor(C["dark"])
cv.drawString(col_x[0]+8, ry-21, "PVC: wide aberrant QRS. PAC: narrow, premature P wave")
# ────────────────────────────────────────────────────────────
# SECTION 2: REGULAR SUSTAINED (col 0, lower)
# ────────────────────────────────────────────────────────────
y2 = y - 175 - GAP
rbox(cv, col_x[0], y2 - 265, col_w, 265, C["lteal"], C["teal"], r=7)
hdr(cv, col_x[0], y2 - sh, col_w, sh, C["teal"],
"REGULAR SUSTAINED PALPITATIONS", 8)
reg_rows = [
("Sinus Tachycardia", "ST", C["teal"],
["Gradual onset & offset",
"Rate 100-130 bpm (can be higher)",
"Causes: exercise, pain, fever, anxiety,",
" hypovolemia, anemia, thyrotoxicosis,",
" PE, drugs, pheochromocytoma"]),
("AVNRT / AVRT (SVT)", "SVT", C["green"],
["Sudden onset & sudden offset",
"\"Heart jumping out of chest\"",
"Rate 150-250 bpm, too fast to count",
"Responds to vagal maneuvers",
"Polyuria after episode (ANP release)",
"Triggers: sudden movement, bending"]),
("Atrial Tachycardia (Focal)", "AT", C["teal"],
["Short bursts seconds-minutes",
"Normal rhythm between episodes",
"Automatic focal mechanism"]),
("Ventricular Tachycardia", "VT", C["red"],
["High risk — structural heart disease",
"Syncope / near-syncope common",
"Wide complex on ECG",
"Urgent evaluation required"]),
]
ry2 = y2 - sh - 6
for name, abbr, acol, pts in reg_rows:
cv.setFont("Helvetica-Bold", 7.5); cv.setFillColor(acol)
cv.drawString(col_x[0]+6, ry2, name + f" [{abbr}]")
ry2 -= 2
for pt in pts:
bullet(cv, col_x[0]+8, ry2, pt, fsize=7, color=C["dark"])
ry2 -= 11
ry2 -= 3
# ECG clue
ry2 -= 2
cv.setFillColor(C["lgold"]); cv.setStrokeColor(C["gold"]); cv.setLineWidth(1)
cv.roundRect(col_x[0]+4, ry2-26, col_w-8, 26, 4, fill=1, stroke=1)
cv.setFillColor(C["gold"]); cv.setFont("Helvetica-Bold", 7)
cv.drawString(col_x[0]+8, ry2-10, u"\u26A1 ECG: Narrow QRS = SVT likely")
cv.setFont("Helvetica", 7); cv.setFillColor(C["dark"])
cv.drawString(col_x[0]+8, ry2-21, "Wide QRS = VT until proven otherwise. Check prior ECG")
# ────────────────────────────────────────────────────────────
# COLUMN 1: IRREGULAR PATTERNS
# ────────────────────────────────────────────────────────────
y = TOP
rbox(cv, col_x[1], y - 265, col_w, 265, C["lpurple"], C["purple"], r=7)
hdr(cv, col_x[1], y - sh, col_w, sh, C["purple"],
"IRREGULAR PALPITATIONS", 8)
irr_rows = [
("Atrial Fibrillation", "AF", C["purple"],
["Irregularly irregular rhythm",
"Prolonged racing, hours to days",
"Rate typically 100-160 bpm",
"Dyspnea, fatigue, reduced exercise tolerance",
"Risk: hypertension, valvular disease,",
" hyperthyroidism, OSA, alcohol",
"Stroke risk — requires anticoagulation",
"First presentation may be incidental"]),
("Atrial Flutter", "AFL", C["purple"],
["Usually regular 2:1 block → ~150 bpm",
"Can be irregular with variable block",
"\"Sawtooth\" flutter waves on ECG",
"Often co-exists with AF"]),
("Multifocal Atrial Tachycardia", "MAT", C["purple"],
["≥3 different P wave morphologies",
"Irregular, rate 100-180 bpm",
"Classic: severe COPD, critical illness",
"Correct hypoxia / electrolytes first"]),
("High-burden PAC/PVC", "", C["gray"],
["Felt as \"constantly missing beats\"",
"Continuous irregular sensation",
"Differentiate from AF with Holter"]),
]
ry = y - sh - 6
for name, abbr, acol, pts in irr_rows:
lbl = f" [{abbr}]" if abbr else ""
cv.setFont("Helvetica-Bold", 7.5); cv.setFillColor(acol)
cv.drawString(col_x[1]+6, ry, name + lbl)
ry -= 2
for pt in pts:
bullet(cv, col_x[1]+8, ry, pt, fsize=7, color=C["dark"])
ry -= 11
ry -= 3
# ECG clue
ry -= 2
cv.setFillColor(C["lgold"]); cv.setStrokeColor(C["gold"]); cv.setLineWidth(1)
cv.roundRect(col_x[1]+4, ry-26, col_w-8, 26, 4, fill=1, stroke=1)
cv.setFillColor(C["gold"]); cv.setFont("Helvetica-Bold", 7)
cv.drawString(col_x[1]+8, ry-10, u"\u26A1 ECG: Absent P waves + irregular = AF")
cv.setFont("Helvetica", 7); cv.setFillColor(C["dark"])
cv.drawString(col_x[1]+8, ry-21, "Sawtooth at 300 bpm = flutter. Varying P morphology = MAT")
# ── NON-CARDIAC DDx (col 1, lower) ──────────────────────
y2 = y - 265 - GAP
rbox(cv, col_x[1], y2 - 175, col_w, 175, C["lorange"], C["orange"], r=7)
hdr(cv, col_x[1], y2 - sh, col_w, sh, C["orange"],
"NON-CARDIAC DIFFERENTIAL (31-26%)", 8)
noncard = [
("Panic Disorder / Anxiety", C["orange"],
["Duration often >15 min",
"Multiple somatic symptoms",
"Fear of dying during episode",
"Hyperventilation, tingling, derealization"]),
("Thyrotoxicosis", C["orange"],
["Heat intolerance, weight loss",
"Tremor, diarrhea, eye signs",
"Sinus tach or AF as first presentation",
"Check TSH + free T4"]),
("Pheochromocytoma", C["red"],
["Episodic headache + sweating + palpitations",
"Hypertensive crisis during episodes",
"24h urine metanephrines / catecholamines"]),
("Post-COVID Syndrome", C["gray"],
["POTS features common",
"Tachycardia on standing",
"Fatigue, brain fog"]),
]
ry2 = y2 - sh - 6
for name, acol, pts in noncard:
cv.setFont("Helvetica-Bold", 7.5); cv.setFillColor(acol)
cv.drawString(col_x[1]+6, ry2, name)
ry2 -= 2
for pt in pts:
bullet(cv, col_x[1]+8, ry2, pt, fsize=7, color=C["dark"])
ry2 -= 11
ry2 -= 3
# ────────────────────────────────────────────────────────────
# COLUMN 2: WIDE-COMPLEX TACHYCARDIA TREE + SPECIAL PATTERNS
# ────────────────────────────────────────────────────────────
y = TOP
bh_tree = 210
rbox(cv, col_x[2], y - bh_tree, col_w, bh_tree, C["lred"], C["red"], r=7)
hdr(cv, col_x[2], y - sh, col_w, sh, C["red"],
"WIDE-COMPLEX TACHYCARDIA (QRS ≥120 ms)", 8)
# Tree structure
ty = y - sh - 12
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C["red"])
cv.drawString(col_x[2]+6, ty, "REGULAR")
ty -= 11
wc_reg = [
(u"\u2192 Ventricular Tachycardia (VT)", C["red"],
["Monomorphic — fixed QRS morphology",
"Structural heart disease common",
"Brugada / ARVC / ischemic scar"]),
(u"\u2192 SVT with aberrancy", C["navy"],
["Bundle branch block pattern",
"Previous BBB or rate-related BBB"]),
(u"\u2192 SVT via accessory pathway", C["navy"],
["WPW: delta wave in sinus rhythm",
"Pre-excited AF → irregular wide-complex"]),
]
for lbl, lc, subs in wc_reg:
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(lc)
cv.drawString(col_x[2]+8, ty, lbl); ty -= 2
for s in subs:
bullet(cv, col_x[2]+16, ty, s, fsize=6.5, color=C["dark"]); ty -= 10
ty -= 2
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C["red"])
cv.drawString(col_x[2]+6, ty, "IRREGULAR / POLYMORPHIC")
ty -= 11
wc_irr = [
(u"\u2192 Torsades de Pointes (TdP)", C["red"],
["Prolonged QT (congenital or acquired)",
"Drugs: antiarrhythmics, macrolides,",
" antipsychotics, fluoroquinolones",
"Electrolyte: \u2193K\u207a, \u2193Mg\u00b2\u207a, \u2193Ca\u00b2\u207a"]),
(u"\u2192 Pre-excited AF (WPW)", C["purple"],
["Irregular wide-complex tachycardia",
"AVOID AV nodal blockers (digoxin, CCB)"]),
]
for lbl, lc, subs in wc_irr:
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(lc)
cv.drawString(col_x[2]+8, ty, lbl); ty -= 2
for s in subs:
bullet(cv, col_x[2]+16, ty, s, fsize=6.5, color=C["dark"]); ty -= 10
ty -= 2
# ECG rule-of-thumb
cv.setFillColor(C["lgold"]); cv.setStrokeColor(C["gold"]); cv.setLineWidth(1)
cv.roundRect(col_x[2]+4, ty-24, col_w-8, 24, 4, fill=1, stroke=1)
cv.setFillColor(C["gold"]); cv.setFont("Helvetica-Bold", 7)
cv.drawString(col_x[2]+8, ty-10, u"\u26A1 Wide + hemodynamic instability = VT until proven")
cv.setFont("Helvetica", 7); cv.setFillColor(C["dark"])
cv.drawString(col_x[2]+8, ty-20, "Use Brugada / Vereckei criteria to differentiate VT vs SVT")
# ── BRADYCARDIA / PAUSES (col 2, middle) ────────────────
y2b = y - bh_tree - GAP
bh_brad = 120
rbox(cv, col_x[2], y2b - bh_brad, col_w, bh_brad, C["lgold"], C["gold"], r=7)
hdr(cv, col_x[2], y2b - sh, col_w, sh, C["gold"],
"BRADYCARDIA / PAUSES", 8, tcolor=C["dark"])
brad_data = [
("Sinus Bradycardia / SA Block", C["gold"],
["\"Thump\" followed by pause",
"Athletes, vasovagal, sick sinus syndrome"]),
("AV Block (2nd / 3rd degree)", C["orange"],
["Dropped beats — irregular rhythm",
"Syncope in complete heart block",
"Check for Lyme disease, sarcoidosis"]),
("Sinus Arrest", C["red"],
["Sudden pause — dizziness/near-syncope",
"Sick sinus syndrome"]),
]
ry2b = y2b - sh - 8
for name, acol, pts in brad_data:
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(acol)
cv.drawString(col_x[2]+6, ry2b, name); ry2b -= 2
for pt in pts:
bullet(cv, col_x[2]+8, ry2b, pt, fsize=6.5, color=C["dark"]); ry2b -= 10
ry2b -= 3
# ── POSITIONAL / STRUCTURAL (col 2, lower) ──────────────
y3 = y2b - bh_brad - GAP
bh3 = 93
rbox(cv, col_x[2], y3 - bh3, col_w, bh3, C["lgreen"], C["green"], r=7)
hdr(cv, col_x[2], y3 - sh, col_w, sh, C["green"],
"POSITIONAL / STRUCTURAL", 8)
struct_data = [
("Atrial Myxoma", ["Positional palpitations", "Tumor plop on auscultation", "Echo diagnostic"]),
("Mediastinal Mass", ["Positional component", "CXR / CT chest needed"]),
("Mitral Valve Prolapse", ["Mid-systolic click", "Associated with PAC/SVT", "Usually benign"]),
("Hyperdynamic States", ["Aortic regurgitation", "Anaemia, thyrotoxicosis", "Pregnancy"]),
]
ry3 = y3 - sh - 8
for name, pts in struct_data:
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(C["green"])
cv.drawString(col_x[2]+6, ry3, name); ry3 -= 2
for pt in pts:
bullet(cv, col_x[2]+8, ry3, pt, fsize=6.5, color=C["dark"]); ry3 -= 10
ry3 -= 2
# ────────────────────────────────────────────────────────────
# BOTTOM BAND: Key Clinical Questions + Red Flags
# ────────────────────────────────────────────────────────────
bot_h = 68
bot_y = 24
rbox(cv, MARGIN, bot_y, W - 2*MARGIN, bot_h, C["dark"], C["dark"], r=6)
# Red flags column
cv.setFillColor(C["lred"]); cv.setFont("Helvetica-Bold", 8)
cv.drawString(MARGIN+8, bot_y + bot_h - 14, u"\U0001F6A8 RED FLAGS — Urgent Cardiology")
red_flags = ["Syncope/presyncope with palpitations",
"Structural heart disease / prior MI",
"Wide-complex or polymorphic tachycardia",
"Family Hx sudden death <50 yrs"]
cv.setFont("Helvetica", 7); cv.setFillColor(C["lred"])
for i, rf in enumerate(red_flags):
cv.drawString(MARGIN+8, bot_y + bot_h - 26 - i*10, u"\u25B6 " + rf)
# Divider
cv.setStrokeColor(C["blue"]); cv.setLineWidth(0.5)
cv.line(MARGIN + col_w + 4, bot_y+6, MARGIN + col_w + 4, bot_y + bot_h - 4)
# Ask-the-patient column
cv.setFillColor(C["lblue"]); cv.setFont("Helvetica-Bold", 8)
cv.drawString(col_x[1], bot_y + bot_h - 14, u"\U0001F4CB ASK: Characterise the Rhythm")
ask_q = ["Regular or irregular? Fast or slow?",
"Sudden or gradual onset/offset?",
"Tap out the rhythm with your finger",
"Duration of each episode?"]
cv.setFont("Helvetica", 7); cv.setFillColor(C["lblue"])
for i, q in enumerate(ask_q):
cv.drawString(col_x[1], bot_y + bot_h - 26 - i*10, u"\u2022 " + q)
# Divider
cv.line(col_x[2], bot_y+6, col_x[2], bot_y + bot_h - 4)
# Investigations column
cv.setFillColor(C["lgreen"]); cv.setFont("Helvetica-Bold", 8)
cv.drawString(col_x[2], bot_y + bot_h - 14, u"\U0001F4C8 INVESTIGATIONS")
inv = ["ECG (always) | Holter / event recorder",
"Echo if structural disease suspected",
"TSH, FBC, electrolytes, glucose",
"24h urine metanephrines if pheo suspected"]
cv.setFont("Helvetica", 7); cv.setFillColor(C["lgreen"])
for i, inv_item in enumerate(inv):
cv.drawString(col_x[2], bot_y + bot_h - 26 - i*10, u"\u2022 " + inv_item)
# Footer line
cv.setFillColor(C["dark"]); cv.rect(0, 0, W, 22, fill=1, stroke=0)
cv.setFillColor(C["white"]); cv.setFont("Helvetica", 7)
cv.drawCentredString(W/2, 7,
"Sources: Harrison's 22e (Loscalzo), Braunwald's Heart Disease (Calkins et al.), Goldman-Cecil Medicine, Frameworks for Internal Medicine")
cv.showPage()
# ═══════════════════════════════════════════════════════════
# PAGE 2 — SUMMARY TABLE: Rhythm Pattern at a Glance
# ═══════════════════════════════════════════════════════════
cv.setFillColor(C["lgray"]); cv.rect(0, 0, W, H, fill=1, stroke=0)
cv.setFillColor(C["dark"]); cv.rect(0, H-55, W, 55, fill=1, stroke=0)
cv.setFillColor(C["white"]); cv.setFont("Helvetica-Bold", 14)
cv.drawCentredString(W/2, H-28, "PALPITATIONS: AT-A-GLANCE RHYTHM PATTERN TABLE")
cv.setFont("Helvetica", 8)
cv.drawCentredString(W/2, H-44, "Harrison's 22e · Braunwald's · Goldman-Cecil")
# Table columns
cols_def = [
("RHYTHM", 80),
("PATTERN", 70),
("ONSET/OFFSET", 72),
("RATE (bpm)", 60),
("KEY FEATURES", 160),
("ECG CLUE", 120),
]
total_w = sum(v for _, v in cols_def)
scale = (W - 20) / total_w
col_widths = [v * scale for _, v in cols_def]
col_labels = [l for l, _ in cols_def]
TABLE_X = 10
TABLE_TOP = H - 62
ROW_H = 14
# Header row
cv.setFillColor(C["navy"])
xc = TABLE_X
for i, (lbl, cw) in enumerate(zip(col_labels, col_widths)):
cv.setFillColor(C["navy"]); cv.rect(xc, TABLE_TOP - ROW_H, cw, ROW_H, fill=1, stroke=0)
cv.setFillColor(C["white"]); cv.setFont("Helvetica-Bold", 7)
cv.drawCentredString(xc + cw/2, TABLE_TOP - ROW_H + 4, lbl)
xc += cw
# Data rows
table_data = [
# Rhythm, Pattern, Onset/Off, Rate, KeyFeatures, ECG
("PAC / PVC", "Intermittent", "Spontaneous", "N/A", "Skipped beat; forceful post-ectopic beat; caffeine/stress trigger; high PVC burden may cause CMP", "Isolated early beat; wide (PVC) or narrow (PAC) QRS; compensatory pause", C["lblue"]),
("Sinus Tachycardia","Regular", "Gradual / Gradual","100-180", "Secondary cause always; fever, anaemia, hypovolemia, thyrotoxicosis, PE, pain, anxiety", "Normal P waves; PR/QRS normal; rate slows with Valsalva", C["lteal"]),
("AVNRT", "Regular", "Sudden / Sudden", "150-250", "Rapid racing too fast to count; responds to vagal; polyuria after; women > men", "Narrow QRS; retrograde P in/after QRS (\"pseudo-R'\" in V1)", C["lteal"]),
("AVRT (WPW)", "Regular", "Sudden / Sudden", "150-250", "Young patients; orthodromic = narrow; antidromic = wide; risk of pre-excited AF", "Delta wave in sinus rhythm; short PR. Wide-complex if antidromic", C["lteal"]),
("Focal AT", "Regular/bursts", "Sudden / Gradual","100-200", "Short bursts seconds-minutes; automatic focus; warmup phenomenon", "Abnormal P wave morphology; may have variable AV block", C["lteal"]),
("Atrial Flutter", "Regular*", "Sudden / Sudden", "~150 (2:1)","Regular at ~150 = flutter until proven; can be irregular with variable block", "Sawtooth flutter waves 300 bpm; variable block pattern", C["lpurple"]),
("Atrial Fibrillation","Irregular", "Gradual or sudden","100-160", "Irregularly irregular; loss of atrial kick; stroke risk; palpitations ± dyspnea", "Absent P waves; irregular RR; fibrillatory baseline", C["lpurple"]),
("MAT", "Irregular", "Gradual", "100-180", "COPD / critical illness; ≥3 P morphologies; treat underlying hypoxia first", "≥3 P-wave morphologies; varying PR, PP, RR intervals", C["lpurple"]),
("VT (Monomorphic)","Regular", "Sudden / Sudden", "120-200", "Structural HD; prior MI; syncope; haemodynamic compromise; EMERGENCY", "Wide QRS; AV dissociation; fusion/capture beats; Brugada criteria", C["lred"]),
("Torsades (TdP)", "Irregular/poly", "Sudden", "200-250", "Prolonged QT; drugs (QT-prolonging); electrolyte deficiency (K, Mg, Ca)", "Twisting QRS around isoelectric line; long QT at baseline", C["lred"]),
("Pre-excited AF", "Irregular", "Sudden", "200-300", "LIFE-THREATENING; WPW + AF; AVOID digoxin/CCB/adenosine; use procainamide/DC cardioversion", "Irregular wide-complex; varying delta waves; fast ventricular rate", C["lred"]),
("SA Block/Arrest", "Pause", "Sudden pause", "Slow", "Sick sinus syndrome; vagal; athletes; dizziness/presyncope; thump then pause", "Long pause (multiple of PP); junctional escape; sinus arrest = no P", C["lgold"]),
("2nd/3rd AV Block","Irregular/slow", "Gradual", "30-60", "Dropped beats; Lyme disease; sarcoidosis; inferior MI; syncope in CHB", "Mobitz I: lengthening PR. Mobitz II: fixed PR/dropped. CHB: AV diss.", C["lgold"]),
("Anxiety/Panic", "Variable", "Gradual or sudden",">100", "Duration >15 min; hyperventilation; tingling; multiple somatic symptoms; no ECG correlate", "Sinus tachycardia during episode; normal between", C["lorange"]),
("Thyrotoxicosis", "Regular", "Gradual", "100-140", "Weight loss, heat intolerance, tremor, diarrhea; sinus tach or AF; check TSH", "Sinus tachycardia or AF; tall T waves", C["lorange"]),
("Pheochromocytoma","Episodic", "Sudden / Sudden", "100-200", "Classic triad: headache + sweating + palpitations; hypertensive crisis", "Sinus tachycardia during crisis; 24h urine metanephrines", C["lorange"]),
("MVP", "Irregular", "Variable", "Variable", "Mid-systolic click on auscultation; PAC/SVT association; usually benign", "PAC or SVT; may have ST changes in inferior leads", C["lgreen"]),
("Atrial Myxoma", "Positional", "Positional change","Variable", "Positional palpitations; tumor plop; constitutional symptoms; echo diagnostic", "Variable; may mimic AF or flutter; echocardiogram is key test", C["lgreen"]),
]
alt_colors = [C["white"], C["lgray"]]
row_y = TABLE_TOP - ROW_H
for ri, row in enumerate(table_data):
*cells, row_color = row
xc = TABLE_X
bg = row_color
for ci, (cell, cw) in enumerate(zip(cells, col_widths)):
cv.setFillColor(bg)
cv.setStrokeColor(C["gray"]); cv.setLineWidth(0.3)
cv.rect(xc, row_y - ROW_H, cw, ROW_H, fill=1, stroke=1)
cv.setFillColor(C["dark"])
fn = "Helvetica-Bold" if ci == 0 else "Helvetica"
fs = 6.5 if ci >= 4 else 7
cv.setFont(fn, fs)
# truncate to fit
txt = cell
while cv.stringWidth(txt, fn, fs) > cw - 4 and len(txt) > 3:
txt = txt[:-4] + "..."
cv.drawString(xc + 3, row_y - ROW_H + 4, txt)
xc += cw
row_y -= ROW_H
# Color legend
legend_y = row_y - 14
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C["dark"])
cv.drawString(TABLE_X, legend_y, "COLOR KEY:")
legend_items = [
(C["lteal"], "Regular SVT"),
(C["lpurple"], "Atrial / Irregular"),
(C["lred"], "URGENT / Wide-complex"),
(C["lgold"], "Bradycardia / Conduction"),
(C["lorange"], "Non-cardiac"),
(C["lgreen"], "Structural"),
]
lx = TABLE_X + 75
for fill, lbl in legend_items:
cv.setFillColor(fill); cv.setStrokeColor(C["gray"]); cv.setLineWidth(0.5)
cv.rect(lx, legend_y - 8, 10, 10, fill=1, stroke=1)
cv.setFillColor(C["dark"]); cv.setFont("Helvetica", 7)
cv.drawString(lx + 12, legend_y - 2, lbl)
lx += cv.stringWidth(lbl, "Helvetica", 7) + 26
# Footer
cv.setFillColor(C["dark"]); cv.rect(0, 0, W, 22, fill=1, stroke=0)
cv.setFillColor(C["white"]); cv.setFont("Helvetica", 7)
cv.drawCentredString(W/2, 7,
"Sources: Harrison's 22e (Loscalzo), Braunwald's Heart Disease, Goldman-Cecil Medicine, Frameworks for Internal Medicine")
cv.showPage()
cv.save()
print("Saved:", out)
cd /tmp/workspace/palpitations && python qrc_rhythm_ddx.py
Palpitations Rhythm DDx QRC
PDF Document · PDF
| Column | Content |
|---|---|
| Left | Intermittent/single beats (PAC, PVC) + Regular sustained (ST, SVT, AT, VT) |
| Middle | Irregular rhythms (AF, AFL, MAT) + Non-cardiac DDx (panic, thyrotoxicosis, pheo, post-COVID) |
| Right | Wide-complex tachycardia tree (regular/irregular, VT vs SVT with aberrancy vs WPW) + Bradycardia/pauses + Positional/structural |