~/nstemi-flowchart/generate_flowchart.py
#!/usr/bin/env python3
"""
NSTEMI Diagnosis & Management Flowchart PDF
Using reportlab for professional medical flowchart
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
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
import textwrap
# ─── Color Palette ────────────────────────────────────────────────────────────
C_RED = colors.HexColor("#C0392B")
C_ORANGE = colors.HexColor("#E67E22")
C_GREEN = colors.HexColor("#1A7A3F")
C_BLUE = colors.HexColor("#1A5276")
C_LBLUE = colors.HexColor("#2980B9")
C_PURPLE = colors.HexColor("#6C3483")
C_TEAL = colors.HexColor("#117A65")
C_YELLOW = colors.HexColor("#D4AC0D")
C_GRAY = colors.HexColor("#566573")
C_LGRAY = colors.HexColor("#D5D8DC")
C_WHITE = colors.white
C_BLACK = colors.black
C_BG = colors.HexColor("#F0F3F4")
# ─── Page Setup ───────────────────────────────────────────────────────────────
PAGE_W, PAGE_H = A4 # 595 x 842 pt
OUTPUT = "/home/daytona/workspace/nstemi-flowchart/NSTEMI_Management_Flowchart.pdf"
def draw_rounded_box(c, x, y, w, h, fill_color, stroke_color=None, radius=6,
text=None, text_color=C_WHITE, font_size=8, bold=False,
subtext=None, sub_font_size=6.5):
"""Draw a rounded rectangle with centered text."""
if stroke_color is None:
stroke_color = fill_color
c.setFillColor(fill_color)
c.setStrokeColor(stroke_color)
c.setLineWidth(1)
c.roundRect(x, y, w, h, radius, fill=1, stroke=1)
if text:
lines = text.split('\n')
total_lines = len(lines)
line_h = font_size * 1.3
sub_h = sub_font_size * 1.3 if subtext else 0
sub_lines = subtext.split('\n') if subtext else []
sub_total = len(sub_lines)
total_height = total_lines * line_h + (sub_total * sub_h if subtext else 0)
start_y = y + h / 2 + total_height / 2 - line_h * 0.8
c.setFillColor(text_color)
font_name = "Helvetica-Bold" if bold else "Helvetica"
c.setFont(font_name, font_size)
for i, line in enumerate(lines):
c.drawCentredString(x + w / 2, start_y - i * line_h, line)
if subtext:
c.setFont("Helvetica", sub_font_size)
c.setFillColor(text_color)
sub_start_y = start_y - total_lines * line_h + line_h * 0.3
for j, sline in enumerate(sub_lines):
c.drawCentredString(x + w / 2, sub_start_y - j * sub_h, sline)
def draw_diamond(c, cx, cy, half_w, half_h, fill_color, text, font_size=7.5,
text_color=C_WHITE):
"""Draw a diamond decision box."""
path = c.beginPath()
path.moveTo(cx, cy + half_h)
path.lineTo(cx + half_w, cy)
path.lineTo(cx, cy - half_h)
path.lineTo(cx - half_w, cy)
path.close()
c.setFillColor(fill_color)
c.setStrokeColor(fill_color)
c.setLineWidth(1)
c.drawPath(path, fill=1, stroke=1)
lines = text.split('\n')
line_h = font_size * 1.3
start_y = cy + (len(lines) - 1) * line_h / 2
c.setFillColor(text_color)
c.setFont("Helvetica-Bold", font_size)
for i, line in enumerate(lines):
c.drawCentredString(cx, start_y - i * line_h, line)
def arrow(c, x1, y1, x2, y2, label=None, label_color=C_BLACK,
color=C_GRAY, lw=1.2):
"""Draw an arrow from (x1,y1) to (x2,y2)."""
c.setStrokeColor(color)
c.setLineWidth(lw)
c.line(x1, y1, x2, y2)
# arrowhead
import math
angle = math.atan2(y2 - y1, x2 - x1)
aw = 5
ah = 3
c.setFillColor(color)
path = c.beginPath()
path.moveTo(x2, y2)
path.lineTo(x2 - aw * math.cos(angle) + ah * math.sin(angle),
y2 - aw * math.sin(angle) - ah * math.cos(angle))
path.lineTo(x2 - aw * math.cos(angle) - ah * math.sin(angle),
y2 - aw * math.sin(angle) + ah * math.cos(angle))
path.close()
c.drawPath(path, fill=1, stroke=0)
if label:
mx = (x1 + x2) / 2
my = (y1 + y2) / 2
c.setFont("Helvetica-Bold", 6.5)
c.setFillColor(label_color)
c.drawCentredString(mx + 8, my, label)
def h_arrow(c, x1, y, x2, label=None, label_side="top", color=C_GRAY, lw=1.2):
arrow(c, x1, y, x2, y, label=label, label_color=color, color=color, lw=lw)
def v_arrow(c, x, y1, y2, label=None, color=C_GRAY, lw=1.2):
arrow(c, x, y1, x, y2, label=label, color=color, lw=lw)
def draw_page1(c):
"""Page 1: Initial Presentation → Diagnosis → Risk Stratification"""
c.setPageSize(A4)
# ── Background ────────────────────────────────────────────────────────────
c.setFillColor(C_BG)
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# ── Header ────────────────────────────────────────────────────────────────
c.setFillColor(C_BLUE)
c.rect(0, PAGE_H - 52, PAGE_W, 52, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 15)
c.drawCentredString(PAGE_W / 2, PAGE_H - 22, "NSTEMI: Diagnosis & Risk Stratification")
c.setFont("Helvetica", 8)
c.drawCentredString(PAGE_W / 2, PAGE_H - 38,
"Based on ACC/AHA 2014 Guidelines & Harrison's Principles of Internal Medicine 22E (2025)")
# ── Footer ────────────────────────────────────────────────────────────────
c.setFillColor(C_BLUE)
c.rect(0, 0, PAGE_W, 18, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica", 6.5)
c.drawCentredString(PAGE_W / 2, 5, "Page 1 of 2 | For clinical use by qualified medical professionals only")
# ─── Layout constants ─────────────────────────────────────────────────────
BW = 155 # box width
BH = 30 # box height standard
CX = PAGE_W / 2 # center x
# ─── BOX 1: Patient Presentation ──────────────────────────────────────────
y1 = PAGE_H - 90
draw_rounded_box(c, CX - BW/2, y1, BW, BH+4, C_BLUE,
text="CHEST PAIN / ACS PRESENTATION\n(Suspected Acute Coronary Syndrome)",
font_size=8, bold=True, radius=8)
# ─── Presentation features box ────────────────────────────────────────────
feat_y = y1 - 58
feat_w = 390
draw_rounded_box(c, CX - feat_w/2, feat_y, feat_w, 45, C_LBLUE,
text="Chest pain ≥10 min at rest | Onset within 2 weeks | Crescendo pattern\nRadiation to arm/jaw | Diaphoresis, nausea, dyspnea | Age >40, DM, HTN, smoking history",
font_size=7.5, bold=False)
v_arrow(c, CX, y1, y1 - 10)
v_arrow(c, CX, feat_y + 45, feat_y + 45 - 2)
# ─── BOX 2: Immediate Actions ─────────────────────────────────────────────
imm_y = feat_y - 65
imm_w = 390
draw_rounded_box(c, CX - imm_w/2, imm_y, imm_w, 52, C_TEAL,
text="IMMEDIATE ACTIONS (0-10 minutes)\n"
"IV Access | 12-Lead ECG | Continuous ECG monitoring | SpO₂\n"
"O₂ if SaO₂ <90% | Aspirin 325mg chewed | IV NTG SL if pain ongoing\n"
"Blood: hsTnI/T (0h), CBC, BMP, Lipids, Coags, CXR",
font_size=7.5, bold=False, subtext=None)
v_arrow(c, CX, feat_y, imm_y + 52)
# ─── Diamond: ECG Interpretation ──────────────────────────────────────────
ecg_y = imm_y - 48
draw_diamond(c, CX, ecg_y, 105, 30, C_PURPLE,
"ECG: ST-Elevation\npresent?", font_size=7.5)
v_arrow(c, CX, imm_y, ecg_y + 30)
# STEMI branch → right
stemi_x = CX + 140
stemi_y = ecg_y - 5
draw_rounded_box(c, stemi_x, stemi_y - 14, 115, 28, C_RED,
text="→ STEMI Protocol\nActivate Cath Lab\n(PCI within 90 min)", font_size=7, bold=True)
h_arrow(c, CX + 105, ecg_y, stemi_x, label="YES", color=C_RED, lw=1.5)
# NO branch → down
v_arrow(c, CX, ecg_y - 30, ecg_y - 55, label="NO", color=C_GREEN, lw=1.5)
# ─── NSTE-ACS box ─────────────────────────────────────────────────────────
nste_y = ecg_y - 88
nste_w = 390
draw_rounded_box(c, CX - nste_w/2, nste_y, nste_w, 32, C_ORANGE,
text="NSTE-ACS: No ST elevation, but ST depression / T-wave changes / Normal ECG\n"
"→ Differentiate: NSTEMI vs Unstable Angina (UA) vs Non-cardiac",
font_size=7.5, bold=True)
v_arrow(c, CX, nste_y, nste_y - 8)
# ─── Troponin box ─────────────────────────────────────────────────────────
trop_y = nste_y - 55
trop_w = 380
draw_rounded_box(c, CX - trop_w/2, trop_y, trop_w, 44, C_BLUE,
text="HIGH-SENSITIVITY TROPONIN (hsTn) STRATEGY\n"
"0h / 1h Algorithm (preferred) or 0h / 3h Algorithm\n"
"Measure: hsTnI or hsTnT at presentation AND 1h (or 3h)",
font_size=7.5)
v_arrow(c, CX, nste_y - 8, trop_y + 44)
# ─── Diamond: Troponin result ──────────────────────────────────────────────
trop_d_y = trop_y - 45
draw_diamond(c, CX, trop_d_y, 105, 30, C_ORANGE,
"hsTn ELEVATED\nor rising?", font_size=7.5)
v_arrow(c, CX, trop_y, trop_d_y + 30)
# NSTEMI → left
nstemi_box_x = CX - 260
nstemi_box_y = trop_d_y - 16
draw_rounded_box(c, nstemi_box_x, nstemi_box_y - 12, 130, 28, C_RED,
text="NSTEMI\n(Troponin +ve)\nAdmit to CCU/HDU", font_size=7, bold=True)
arrow(c, CX - 105, trop_d_y, nstemi_box_x + 130, nstemi_box_y,
label="YES", color=C_RED, lw=1.5)
# UA → right
ua_box_x = CX + 125
ua_box_y = trop_d_y - 16
draw_rounded_box(c, ua_box_x, ua_box_y - 12, 115, 28, C_YELLOW,
text="UA or Non-cardiac\n(Troponin -ve)\nRepeat at 3h / 6h", font_size=7, bold=True,
text_color=C_BLACK)
h_arrow(c, CX + 105, trop_d_y, ua_box_x, label="NO", color=C_YELLOW, lw=1.5)
v_arrow(c, CX, trop_d_y - 30, trop_d_y - 52)
# ─── GRACE Risk Stratification ────────────────────────────────────────────
grace_y = trop_d_y - 95
grace_w = 410
# Title bar
draw_rounded_box(c, CX - grace_w/2, grace_y + 5, grace_w, 22, C_BLUE,
text="GRACE 2.0 RISK SCORE — Stratification for NSTE-ACS",
font_size=8.5, bold=True)
# Three risk columns
col_w = grace_w / 3 - 4
col_h = 52
# Low risk
draw_rounded_box(c, CX - grace_w/2, grace_y - col_h - 2, col_w, col_h, C_GREEN,
text="LOW RISK\nGRACE <109\n\nTroponin -ve\nNo ECG changes\nEarly discharge\nStress test / CTCA",
font_size=7)
# Intermediate
draw_rounded_box(c, CX - grace_w/2 + col_w + 4, grace_y - col_h - 2, col_w, col_h,
C_ORANGE,
text="INTERMEDIATE\nGRACE 109-140\n\nTroponin borderline\nMinor ST changes\nAngio within 72h\nInpatient monitoring",
font_size=7)
# High risk
draw_rounded_box(c, CX - grace_w/2 + 2*(col_w + 4), grace_y - col_h - 2, col_w, col_h,
C_RED,
text="HIGH RISK\nGRACE >140\n\nTroponin +ve\nST depression ≥0.5mm\nEarly angio ≤24h\nICU admission",
font_size=7)
# ─── GRACE inputs note ────────────────────────────────────────────────────
c.setFillColor(C_GRAY)
c.setFont("Helvetica-Oblique", 6.5)
c.drawCentredString(CX, grace_y - col_h - 60,
"GRACE inputs: Age, HR, SBP, Creatinine, Killip Class, ST deviation, Cardiac arrest, Elevated troponin")
# ─── Section divider ──────────────────────────────────────────────────────
c.setStrokeColor(C_LGRAY)
c.setLineWidth(0.5)
c.line(30, grace_y - col_h - 68, PAGE_W - 30, grace_y - col_h - 68)
c.setFont("Helvetica-Bold", 7)
c.setFillColor(C_BLUE)
c.drawCentredString(CX, grace_y - col_h - 78,
"Continued on Page 2 → Medical Treatment & Invasive Strategy")
c.showPage()
def draw_page2(c):
"""Page 2: Medical Treatment + Invasive Strategy + Disposition"""
c.setPageSize(A4)
# ── Background ────────────────────────────────────────────────────────────
c.setFillColor(C_BG)
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# ── Header ────────────────────────────────────────────────────────────────
c.setFillColor(C_BLUE)
c.rect(0, PAGE_H - 52, PAGE_W, 52, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 15)
c.drawCentredString(PAGE_W / 2, PAGE_H - 22,
"NSTEMI: Medical Treatment & Invasive Strategy")
c.setFont("Helvetica", 8)
c.drawCentredString(PAGE_W / 2, PAGE_H - 38,
"Based on ACC/AHA 2014 Guidelines & Harrison's Principles of Internal Medicine 22E (2025)")
# ── Footer ────────────────────────────────────────────────────────────────
c.setFillColor(C_BLUE)
c.rect(0, 0, PAGE_W, 18, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica", 6.5)
c.drawCentredString(PAGE_W / 2, 5, "Page 2 of 2 | For clinical use by qualified medical professionals only")
CX = PAGE_W / 2
y = PAGE_H - 70
# ─── DAPT section header ──────────────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 1: ANTIPLATELET THERAPY (DAPT) — Start Immediately",
font_size=9, bold=True)
y -= 56
# Two columns: Aspirin | P2Y12
col_w = (PAGE_W - 50) / 2
draw_rounded_box(c, 20, y, col_w, 48, C_LBLUE,
text="ASPIRIN\n• Loading: 325 mg (chewed)\n• Maintenance: 75-100 mg/day\n• Continue indefinitely",
font_size=8)
draw_rounded_box(c, 25 + col_w, y, col_w, 48, C_TEAL,
text="P2Y12 INHIBITOR (choose one)\n• Ticagrelor 180mg load → 90mg BD (preferred)\n• Clopidogrel 300-600mg load → 75mg/day\n• Prasugrel (post-PCI, no prior TIA/stroke)",
font_size=8)
y -= 32
# ─── Anticoagulation ──────────────────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 2: ANTICOAGULATION", font_size=9, bold=True)
y -= 56
col_w3 = (PAGE_W - 60) / 3
draw_rounded_box(c, 20, y, col_w3, 48, C_PURPLE,
text="FONDAPARINUX\n2.5 mg SC OD\n(Preferred — reduces\nbleeding vs LMWH)\nAvoid if CrCl <20",
font_size=7.5)
draw_rounded_box(c, 25 + col_w3, y, col_w3, 48, C_LBLUE,
text="ENOXAPARIN (LMWH)\n1 mg/kg SC BD\nReduce to 1mg/kg OD\nif CrCl 15-30\nHold 12h pre-PCI",
font_size=7.5)
draw_rounded_box(c, 30 + 2 * col_w3, y, col_w3, 48, C_ORANGE,
text="UFH (Unfractionated)\nBolus 60 U/kg IV\n(max 4000U)\nIf early PCI planned\nor CrCl <15",
font_size=7.5)
y -= 32
# ─── Anti-ischemic section ────────────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 3: ANTI-ISCHEMIC TREATMENT", font_size=9, bold=True)
y -= 56
col_w2 = (PAGE_W - 60) / 4
draw_rounded_box(c, 20, y, col_w2, 48, C_TEAL,
text="NITRATES\nSL NTG 0.3-0.6mg\nIV NTG 5-200mcg/min\nAvoid: SBP<90,\nRV infarct, PDE5i",
font_size=7)
draw_rounded_box(c, 25 + col_w2, y, col_w2, 48, C_LBLUE,
text="BETA-BLOCKER\nMetoprolol 25-50mg BD\nTarget HR 50-60/min\nAvoid: HR<50, PR>240,\nKillip III/IV",
font_size=7)
draw_rounded_box(c, 30 + 2*col_w2, y, col_w2, 48, C_PURPLE,
text="CCB (if BB CI)\nDiltiazem / Verapamil\nAvoid short-acting\nnifedipine\nAvoid in LV dysfunction",
font_size=7)
draw_rounded_box(c, 35 + 3*col_w2, y, col_w2, 48, C_GRAY,
text="MORPHINE\n1-5mg IV q5-30min\nOnly if severe pain\ndespite nitrates\nMay blunt P2Y12",
font_size=7)
y -= 32
# ─── Other meds ───────────────────────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 4: ADDITIONAL MEDICAL THERAPY", font_size=9, bold=True)
y -= 40
col_w2b = (PAGE_W - 50) / 2
draw_rounded_box(c, 20, y, col_w2b, 32, C_TEAL,
text="HIGH-INTENSITY STATIN (start immediately)\nAtorvastatin 80mg OD OR Rosuvastatin 40mg OD",
font_size=7.5)
draw_rounded_box(c, 25 + col_w2b, y, col_w2b, 32, C_LBLUE,
text="ACE INHIBITOR / ARB (if LVEF <40%, HTN, DM, CKD)\nRamipril 2.5-10mg OD | Start within 24h if stable",
font_size=7.5)
y -= 36
# ─── Invasive strategy decision ───────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 5: INVASIVE vs. CONSERVATIVE STRATEGY", font_size=9, bold=True)
y -= 8
# Diamond
draw_diamond(c, CX, y - 28, 100, 26, C_RED,
"High-risk features?", font_size=8)
v_arrow(c, CX, y, y - 2)
# Yes branch - early invasive
inv_y = y - 88
ei_w = 168
draw_rounded_box(c, CX - ei_w - 5, inv_y, ei_w, 55, C_RED,
text="EARLY INVASIVE STRATEGY\n(Angiography ≤24h)\n\nGRACE >140 / Refractory ischemia\nHemodynamic instability\nElevated troponin | ST depression\nSustained VT / VF",
font_size=7, bold=True)
arrow(c, CX - 100, y - 28, CX - ei_w + ei_w, inv_y + 55,
label="YES", color=C_RED, lw=1.5)
# No branch - conservative
draw_rounded_box(c, CX + 5, inv_y, ei_w, 55, C_GREEN,
text="CONSERVATIVE STRATEGY\n(Angiography if needed)\n\nGRACE <109 / Low-risk\nTroponin -ve / No ECG changes\nStress test or CTCA first\nAngio only if ischemia confirmed",
font_size=7, bold=True)
h_arrow(c, CX + 100, y - 28, CX + 5, label="NO", color=C_GREEN, lw=1.5)
y = inv_y - 30
# ─── Post-PCI / Discharge box ─────────────────────────────────────────────
draw_rounded_box(c, 20, y, PAGE_W - 40, 22, C_BLUE,
text="STEP 6: DISCHARGE PLANNING & SECONDARY PREVENTION",
font_size=9, bold=True)
y -= 44
draw_rounded_box(c, 20, y, PAGE_W - 40, 38, C_TEAL,
text="DAPT for 12 months (aspirin + ticagrelor/clopidogrel) | Statin lifelong | ACEi/ARB | Beta-blocker\n"
"Cardiac rehab | Smoking cessation | BP <130/80 | LDL <55 mg/dL | HbA1c <7% in DM\n"
"Repeat Echo at 6 weeks | Follow-up clinic 4-6 weeks post-discharge",
font_size=7.5)
# ─── Legend ───────────────────────────────────────────────────────────────
legend_y = y - 20
c.setFillColor(C_LGRAY)
c.roundRect(20, legend_y - 12, PAGE_W - 40, 14, 4, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 6.5)
c.setFillColor(C_BLUE)
c.drawString(30, legend_y - 7,
"Abbreviations: DAPT=Dual Antiplatelet Therapy | LMWH=Low Mol. Wt. Heparin | "
"UFH=Unfractionated Heparin | CCB=Ca-Channel Blocker | PDE5i=Phosphodiesterase-5 Inhibitor | "
"CI=Contraindicated | BB=Beta Blocker")
c.showPage()
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
c = canvas.Canvas(OUTPUT, pagesize=A4)
c.setTitle("NSTEMI Diagnosis & Management Flowchart")
c.setAuthor("Orris Medical AI")
c.setSubject("NSTEMI Clinical Management Protocol")
c.setCreator("reportlab")
draw_page1(c)
draw_page2(c)
c.save()
print(f"PDF saved to: {OUTPUT}")
if __name__ == "__main__":
main()