What is your diagnosis based on this given ecg
| Parameter | Value |
|---|---|
| Heart Rate | 48 bpm |
| P duration | 0 ms (absent) |
| PR interval | 0 ms (absent) |
| QRS duration | 75 ms (narrow) |
| QTc (Bazett) | 423 ms (normal) |
| QRS Axis | +57° (normal) |
| Paper speed | 25 mm/s, 10 mm/mV |
Junctional Escape Rhythm (Junctional Bradycardia) at 48 bpm
| Diagnosis | Likelihood |
|---|---|
| Sick Sinus Syndrome | High - common in elderly females; sinus node dysfunction allows junctional escape |
| High-degree (2nd or 3rd degree) AV block | Need rhythm strip +/- longer recording to fully exclude complete heart block with junctional escape |
| Drug effect (beta-blockers, digoxin, calcium channel blockers, amiodarone) | Must rule out - these suppress the sinus node |
| Hypothyroidism | Common cause of bradycardia in elderly women |
| Vagal hypertonia | Less likely at rest with complete P-wave absence |
Disclaimer: This ECG interpretation is for clinical reference only and must be correlated with the patient's full history, symptoms, and examination. Final diagnosis and management decisions should be made by the treating physician.
Create a summary PDF comparing junctional rhythms vs AV block ECG features
mkdir -p /tmp/workspace/ecg-summary && ls /tmp/workspace/ecg-summary/
~/ecg-summary/build_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import PageBreak
OUTPUT = "/tmp/workspace/ecg-summary/Junctional_vs_AV_Block_ECG_Summary.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2.2*cm,
bottomMargin=2*cm,
title="Junctional Rhythms vs AV Block – ECG Feature Comparison",
author="Orris Medical Reference"
)
# ── Colour Palette ──────────────────────────────────────────────────────────
DARK_BLUE = colors.HexColor("#1A3557")
MED_BLUE = colors.HexColor("#2E6DA4")
LIGHT_BLUE = colors.HexColor("#D6E8F7")
JUNCTION_BG = colors.HexColor("#E8F4E8") # soft green tint for junctional
AVBLOCK_BG = colors.HexColor("#FFF3E0") # soft amber tint for AV block
ROW_ALT = colors.HexColor("#F5F9FF")
RED_WARN = colors.HexColor("#C0392B")
DARK_GREEN = colors.HexColor("#1A6B2A")
ORANGE = colors.HexColor("#D35400")
WHITE = colors.white
LIGHT_GREY = colors.HexColor("#F2F2F2")
MID_GREY = colors.HexColor("#888888")
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
title_style = S("DocTitle",
fontSize=20, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, spaceAfter=4)
subtitle_style = S("DocSub",
fontSize=11, textColor=LIGHT_BLUE, fontName="Helvetica",
alignment=TA_CENTER, spaceAfter=2)
section_title = S("SectionTitle",
fontSize=13, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_LEFT, spaceBefore=6, spaceAfter=4, leftIndent=4)
body = S("Body",
fontSize=9.5, textColor=colors.black, fontName="Helvetica",
leading=14, alignment=TA_JUSTIFY)
bullet_j = S("BulletJ",
fontSize=9.5, textColor=DARK_GREEN, fontName="Helvetica",
leading=13, leftIndent=12, spaceBefore=1)
bullet_av = S("BulletAV",
fontSize=9.5, textColor=ORANGE, fontName="Helvetica",
leading=13, leftIndent=12, spaceBefore=1)
table_hdr = S("TableHdr",
fontSize=9, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=11)
table_cell = S("TableCell",
fontSize=8.8, textColor=colors.black, fontName="Helvetica",
alignment=TA_LEFT, leading=12)
table_cell_c = S("TableCellC",
fontSize=8.8, textColor=colors.black, fontName="Helvetica",
alignment=TA_CENTER, leading=12)
caption = S("Caption",
fontSize=7.5, textColor=MID_GREY, fontName="Helvetica-Oblique",
alignment=TA_CENTER)
footer_style = S("Footer",
fontSize=7.5, textColor=MID_GREY, fontName="Helvetica-Oblique",
alignment=TA_CENTER)
subhead = S("Subhead",
fontSize=10.5, textColor=DARK_BLUE, fontName="Helvetica-Bold",
spaceBefore=8, spaceAfter=3)
warning_style = S("Warning",
fontSize=9, textColor=RED_WARN, fontName="Helvetica-Bold",
alignment=TA_CENTER)
# ── Helper – section banner ──────────────────────────────────────────────────
def banner(text, bg=DARK_BLUE):
data = [[Paragraph(text, section_title)]]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("ROUNDEDCORNERS", [4]),
]))
return t
# ── Content Build ────────────────────────────────────────────────────────────
story = []
# ============================================================
# HEADER BOX
# ============================================================
header_data = [
[Paragraph("Junctional Rhythms vs AV Block", title_style)],
[Paragraph("ECG Feature Comparison — Clinical Reference Summary", subtitle_style)],
[Paragraph("Sources: Tintinalli's Emergency Medicine · Fuster & Hurst's The Heart 15e · Harrison's IM 22e · Frameworks for Internal Medicine", caption)],
]
header_tbl = Table(header_data, colWidths=[17*cm])
header_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
]))
story.append(header_tbl)
story.append(Spacer(1, 0.4*cm))
# ============================================================
# SECTION 1 – QUICK OVERVIEW
# ============================================================
story.append(banner("1. Quick Conceptual Overview", MED_BLUE))
story.append(Spacer(1, 0.3*cm))
overview_body = (
"The AV node and His-Purkinje system each have intrinsic automaticity that "
"becomes apparent only when the faster sinus node fails or its impulses are "
"blocked. Understanding whether the resulting bradycardia is a <b>junctional "
"escape</b> (AV node takes over as pacemaker) or an <b>AV block</b> (conduction "
"from atria to ventricles is pathologically impaired) is critical because it "
"determines urgency, reversibility, and pacemaker decisions."
)
story.append(Paragraph(overview_body, body))
story.append(Spacer(1, 0.3*cm))
# Two-column concept boxes
concept_data = [
[
Paragraph("<b>Junctional Rhythm</b>", S("jh", fontSize=10, textColor=DARK_GREEN,
fontName="Helvetica-Bold", alignment=TA_CENTER)),
Paragraph("<b>AV Block</b>", S("avh", fontSize=10, textColor=ORANGE,
fontName="Helvetica-Bold", alignment=TA_CENTER)),
],
[
Paragraph(
"The SA node slows or stops, allowing the AV node (junction) to escape "
"at its intrinsic rate of <b>40–60 bpm</b>. Conduction to the ventricles is "
"<i>intact</i> — the problem is upstream (no normal P wave).",
S("jb", fontSize=9, fontName="Helvetica", leading=13, alignment=TA_JUSTIFY)
),
Paragraph(
"The SA node fires normally but the impulse is <b>blocked</b> at or below "
"the AV node. P waves are present but fail to conduct (or conduct with delay). "
"The ventricles rely on an escape pacemaker.",
S("ab", fontSize=9, fontName="Helvetica", leading=13, alignment=TA_JUSTIFY)
),
]
]
concept_tbl = Table(concept_data, colWidths=[8.3*cm, 8.3*cm], hAlign="CENTER")
concept_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), JUNCTION_BG),
("BACKGROUND", (1,0), (1,-1), AVBLOCK_BG),
("BOX", (0,0), (0,-1), 1, DARK_GREEN),
("BOX", (1,0), (1,-1), 1, ORANGE),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.white),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(concept_tbl)
story.append(Spacer(1, 0.5*cm))
# ============================================================
# SECTION 2 – DETAILED COMPARISON TABLE
# ============================================================
story.append(banner("2. ECG Feature Comparison Table", MED_BLUE))
story.append(Spacer(1, 0.3*cm))
hdrs = [
Paragraph("ECG Feature", table_hdr),
Paragraph("Junctional Rhythm", S("jH", fontSize=9, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER)),
Paragraph("1° AV Block", table_hdr),
Paragraph("2° Mobitz I\n(Wenckebach)", table_hdr),
Paragraph("2° Mobitz II", table_hdr),
Paragraph("3° (Complete)\nHeart Block", table_hdr),
]
def J(t): return Paragraph(t, S("JC", fontSize=8.5, textColor=DARK_GREEN,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=11))
def A(t): return Paragraph(t, S("AC", fontSize=8.5, textColor=ORANGE,
fontName="Helvetica", alignment=TA_CENTER, leading=11))
def N(t): return Paragraph(t, table_cell_c)
rows = [
hdrs,
[N("P waves"), J("Absent or retrograde\n(inverted, near QRS)"), A("Present, upright,\nnormal"), A("Present, upright,\nnormal"), A("Present, upright,\nnormal"), A("Present but\ndissociated from QRS")],
[N("PR interval"), J("None (or short/\nretrograde)"), A("Prolonged >200 ms;\nCONSTANT"), A("Progressive\nlengthening"), A("Fixed, normal or\nprolonged"), A("Variable — no\nrelationship to QRS")],
[N("P:QRS ratio"), J("0:1 or 1:1\n(retrograde)"), A("1:1\n(all P conduct)"), A("Progressive until\ndrop; e.g. 3:2, 4:3"),A("Fixed ratio:\n2:1, 3:1, etc."), A("Atrial rate >\nventricular rate")],
[N("QRS width"), J("NARROW (<120 ms)\nunless BBB"), A("Normal width"), A("Normal width"), A("Often wide\n(BBB pattern)"), A("Narrow (nodal) or\nWide (His-Purl.)")],
[N("Heart rate"), J("40–60 bpm\n(escape)"), A("Usually normal"), A("May vary;\ngrouped beating"), A("Slow or normal;\nmissed beats"), A("25–45 bpm\n(ventricular escape)")],
[N("Rhythm regularity"),J("Regular"), A("Regular"), A("Irregular\n(grouped pattern)"), A("Regular with\nperiodic dropped"), A("Regular\n(AV dissociation)")],
[N("Site of block"), J("No block — SA node\nfailure / suppression"), A("AV node\n(delay only)"), A("AV node\n(Wenckebach)"), A("His bundle or\nbelow"), A("AV node or\nHis-Purkinje")],
[N("QRS escape type"),J("Junctional\n(narrow)"), A("N/A"), A("N/A"), A("Ventricular\n(wide, slow)"), A("Junctional (narrow)\nor Ventricular (wide)")],
[N("Hemodynamic impact"),J("Moderate — loss of\natrial kick at 40–60 bpm"),A("Usually none"), A("Mild — grouped\ndrops"), A("Significant if slow\nor wide escape"),A("Severe — syncope,\nsudden death risk")],
[N("Pacemaker need"), J("Often yes if\nsymptomatic"), A("No pacemaker;\nmonitor"), A("Pacemaker if\nsymptomatic"), A("Usually permanent\npacemaker"), A("Permanent pacemaker\n(Class I indication)")],
]
col_w = [2.8*cm, 2.8*cm, 2.5*cm, 2.5*cm, 2.5*cm, 2.5*cm]
comp_table = Table(rows, colWidths=col_w, repeatRows=1)
ts = TableStyle([
# Header row
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("BACKGROUND", (1,0), (1,0), DARK_GREEN),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,0), "MIDDLE"),
("TOPPADDING", (0,0), (-1,0), 7),
("BOTTOMPADDING", (0,0), (-1,0), 7),
# Alternating rows
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, ROW_ALT]),
# Junctional column tint
("BACKGROUND", (1,1), (1,-1), colors.HexColor("#F0FBF0")),
# Grid
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("BOX", (0,0), (-1,-1), 1, DARK_BLUE),
# First col bold
("FONTNAME", (0,1), (0,-1), "Helvetica-Bold"),
("FONTSIZE", (0,1), (0,-1), 8.5),
("BACKGROUND", (0,1), (0,-1), LIGHT_BLUE),
# Padding
("TOPPADDING", (0,1), (-1,-1), 5),
("BOTTOMPADDING", (0,1), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,1), (0,-1), "LEFT"),
])
comp_table.setStyle(ts)
story.append(comp_table)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"Junctional column highlighted in green. Colour coding: <font color='#1A6B2A'><b>Green = Junctional</b></font> | "
"<font color='#D35400'><b>Orange = AV Block variants</b></font>",
caption))
story.append(Spacer(1, 0.5*cm))
# ============================================================
# SECTION 3 – JUNCTIONAL RHYTHM DEEP DIVE
# ============================================================
story.append(banner("3. Junctional Rhythms — ECG Deep Dive", colors.HexColor("#1A6B2A")))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("<b>Mechanism</b>", subhead))
story.append(Paragraph(
"When the SA node discharges slowly or fails to reach the AV node, junctional escape beats "
"arise from the AV node (junction) at its intrinsic rate of <b>40–60 bpm</b>. In most cases "
"these do not conduct retrogradely into the atria, so a QRS complex without a preceding P wave "
"is seen. Rarely, retrograde conduction produces an <i>inverted P wave immediately before or "
"after</i> the QRS complex. (Tintinalli's Emergency Medicine)",
body))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("<b>ECG Hallmarks (Table 18-6, Tintinalli's EM)</b>", subhead))
j_features = [
"Absence of normal (sinus-mediated) P waves with normal PR interval",
"Retrograde P wave (rare): inverted, immediately adjacent to QRS — before or after",
"Narrow QRS complex (<120 ms) — normal His-Purkinje conduction intact",
"Rate 40–60 bpm (junctional escape) | 60–100 bpm (accelerated) | >100 bpm (junctional tachycardia)",
"Regular rhythm",
"No ST-elevation or ischemic changes unless co-existent coronary disease",
]
for f in j_features:
story.append(Paragraph(f"▶ {f}", bullet_j))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("<b>Classification by Rate</b>", subhead))
rate_data = [
[Paragraph("Type", table_hdr), Paragraph("Rate", table_hdr), Paragraph("Significance", table_hdr)],
[N("Junctional Escape"), N("40–60 bpm"), N("SA node failure / suppression; benign unless symptomatic")],
[N("Accelerated Junctional"), N("60–100 bpm"), N("Enhanced AV node automaticity; digitalis toxicity, post-cardiac surgery, inferior MI")],
[N("Junctional Tachycardia"), N(">100 bpm"), N("Pathologic enhanced automaticity; urgent treatment required")],
]
rate_tbl = Table(rate_data, colWidths=[4.5*cm, 3*cm, 9*cm])
rate_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_GREEN),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, JUNCTION_BG]),
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#AAAAAA")),
("BOX", (0,0), (-1,-1), 1, DARK_GREEN),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("ALIGN", (0,1), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(rate_tbl)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("<b>Common Causes</b>", subhead))
j_causes = [
"Sick sinus syndrome (most common in elderly)",
"Drug effect: beta-blockers, calcium channel blockers, digoxin, amiodarone",
"Inferior MI (RCA supplies SA and AV nodes — see Frameworks for Internal Medicine)",
"Myocarditis, cardiac surgery (post-operative)",
"Hypokalemia, digitalis toxicity",
"Vagal hypertonia (athletes, vasovagal episodes)",
"Hypothyroidism",
]
for c in j_causes:
story.append(Paragraph(f"▶ {c}", bullet_j))
story.append(Spacer(1, 0.5*cm))
# ============================================================
# SECTION 4 – AV BLOCK DEEP DIVE
# ============================================================
story.append(banner("4. AV Block — ECG Deep Dive (by Degree)", ORANGE))
story.append(Spacer(1, 0.3*cm))
# --- 1st degree
story.append(Paragraph("<b>First-Degree AV Block</b>", subhead))
story.append(Paragraph(
"Every P wave conducts to the ventricles but with a <b>prolonged, constant PR interval "
"(>200 ms)</b>. QRS is narrow. Rate is usually normal. This is a conduction delay, not "
"a true 'block'. Usually benign; no treatment required.",
body))
story.append(Spacer(1, 0.15*cm))
# --- 2nd degree Mobitz I
story.append(Paragraph("<b>Second-Degree AV Block — Mobitz Type I (Wenckebach)</b>", subhead))
w_features = [
"Progressive PR interval lengthening with each beat",
"Until one P wave is blocked (non-conducted) — dropped QRS",
"Grouped beating pattern (irregular rhythm)",
"QRS is narrow (block is at the AV node level)",
"R-R interval progressively SHORTENS before the dropped beat",
"Often benign; associated with inferior MI, increased vagal tone, drug effects",
]
for f in w_features:
story.append(Paragraph(f"▶ {f}", bullet_av))
story.append(Spacer(1, 0.15*cm))
# --- 2nd degree Mobitz II
story.append(Paragraph("<b>Second-Degree AV Block — Mobitz Type II</b>", subhead))
m2_features = [
"PR interval CONSTANT (does not lengthen) on conducted beats",
"Sudden, unexpected dropped QRS without prior PR lengthening",
"Fixed non-conduction ratio: 2:1, 3:1, 4:1 etc.",
"QRS often WIDE (bundle branch block pattern) — block is infranodal (His-Purkinje)",
"More dangerous than Mobitz I — higher risk of progressing to complete heart block",
"Permanent pacemaker usually indicated (Fuster & Hurst's The Heart 15e)",
]
for f in m2_features:
story.append(Paragraph(f"▶ {f}", bullet_av))
story.append(Spacer(1, 0.15*cm))
# --- 3rd degree
story.append(Paragraph("<b>Third-Degree (Complete) AV Block</b>", subhead))
chb_features = [
"Complete failure of all atrial impulses to reach the ventricles",
"P waves and QRS complexes are COMPLETELY DISSOCIATED — no fixed PR relationship",
"Atrial rate > Ventricular rate (atria faster, ventricles slower)",
"Escape rhythm: narrow QRS (40–60 bpm) if block at AV node; wide QRS (20–40 bpm) if infranodal",
"Block at AV node level: junctional escape — narrow, relatively reliable",
"Block at His-Purkinje level: ventricular escape — wide, slow, unreliable (syncope/sudden death risk)",
"Causes: Lenegre's disease, inferior/anterior MI, TAVR, cardiac surgery, Lyme carditis, sarcoidosis, amyloid",
"Permanent pacemaker is a Class I indication (Fuster & Hurst's The Heart 15e)",
]
for f in chb_features:
story.append(Paragraph(f"▶ {f}", bullet_av))
story.append(Spacer(1, 0.4*cm))
# ============================================================
# SECTION 5 – DIAGNOSTIC APPROACH FLOWCHART (text-based)
# ============================================================
story.append(banner("5. Bedside Diagnostic Approach to Bradycardia with Narrow QRS", MED_BLUE))
story.append(Spacer(1, 0.3*cm))
flow_data = [
[Paragraph("<b>Step</b>", table_hdr), Paragraph("<b>Question</b>", table_hdr), Paragraph("<b>Yes → Diagnosis</b>", table_hdr), Paragraph("<b>No → Next Step</b>", table_hdr)],
[N("1"), N("Are P waves ABSENT?"), N("Junctional escape OR SA arrest"), N("Go to Step 2")],
[N("2"), N("P waves present with 1:1 conduction?"), N("1° AV block (if PR >200 ms)"), N("Go to Step 3")],
[N("3"), N("PR progressively lengthens before dropped beat?"), N("2° AV block Mobitz I (Wenckebach)"), N("Go to Step 4")],
[N("4"), N("PR constant but random dropped QRS? Wide QRS?"), N("2° AV block Mobitz II (High risk!)"), N("Go to Step 5")],
[N("5"), N("P and QRS completely dissociated? Atrial rate > Ventricular rate?"), N("3° Complete heart block"), N("Re-assess — check electrolytes, meds, thyroid")],
]
flow_tbl = Table(flow_data, colWidths=[1.5*cm, 5*cm, 5.5*cm, 5*cm])
flow_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, LIGHT_BLUE]),
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#AAAAAA")),
("BOX", (0,0), (-1,-1), 1, DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("ALIGN", (0,0), (0,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("FONTNAME", (0,1), (0,-1), "Helvetica-Bold"),
("FONTSIZE", (0,1), (0,-1), 10),
]))
story.append(flow_tbl)
story.append(Spacer(1, 0.4*cm))
# ============================================================
# SECTION 6 – KEY DIFFERENTIATING PEARLS
# ============================================================
story.append(banner("6. Key Differentiating Pearls", MED_BLUE))
story.append(Spacer(1, 0.3*cm))
pearls = [
("<b>P waves are the key:</b>", "Absent P waves → junctional. Present but blocked P waves → AV block."),
("<b>QRS width narrows the field:</b>", "Wide QRS in bradycardia = Mobitz II or infranodal complete block (ventricular escape). Narrow QRS = junctional escape or nodal complete block."),
("<b>Junctional ≠ AV block:</b>", "Junctional rhythm has intact AV conduction — the SA node is the problem, not the AV node."),
("<b>3° block vs. AV dissociation:</b>", "In complete heart block, atrial rate is faster than ventricular. In AV dissociation from accelerated junction/VT, ventricular rate equals or exceeds atrial rate (Fuster & Hurst)."),
("<b>Inferior MI:</b>", "Commonly causes junctional escape or Wenckebach (nodal block, narrow QRS, often reversible). Anterior MI causes infranodal block (wide QRS, Mobitz II — much more dangerous)."),
("<b>Drug history is mandatory:</b>", "Beta-blockers, CCBs, digoxin, and amiodarone all suppress the SA node and can cause junctional bradycardia — always reversible on withdrawal."),
("<b>Wenckebach clue:</b>", "R-R interval shortens progressively before the pause (not lengthens). The P-P interval stays constant — only PR changes."),
("<b>Pacemaker urgency:</b>", "1° AV block: no pacemaker. Mobitz I: pacemaker only if symptomatic. Mobitz II & 3° block: permanent pacemaker (Class I indication per guidelines)."),
]
pearl_rows = []
for title, text in pearls:
pearl_rows.append([
Paragraph(title, S("PT", fontSize=9, fontName="Helvetica-Bold",
textColor=DARK_BLUE, leading=12)),
Paragraph(text, S("PB", fontSize=9, fontName="Helvetica",
leading=12, alignment=TA_JUSTIFY)),
])
pearl_tbl = Table(pearl_rows, colWidths=[4.5*cm, 12.3*cm])
pearl_tbl.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [WHITE, ROW_ALT]),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#DDDDDD")),
("BOX", (0,0), (-1,-1), 1, MED_BLUE),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(pearl_tbl)
story.append(Spacer(1, 0.4*cm))
# ============================================================
# SECTION 7 – MANAGEMENT SUMMARY
# ============================================================
story.append(banner("7. Management Summary", MED_BLUE))
story.append(Spacer(1, 0.3*cm))
mgmt_data = [
[Paragraph("Diagnosis", table_hdr), Paragraph("Immediate Action", table_hdr), Paragraph("Definitive Management", table_hdr)],
[N("Junctional escape rhythm"), N("ECG, IV access, vitals, SpO2\nStop offending drugs\nAtropine if symptomatic (0.5 mg IV)"), N("Treat reversible cause; Holter monitor; PPM if persistent and symptomatic (SSS)") ],
[N("1° AV Block"), N("Reassurance; review medications"), N("No pacemaker; follow-up ECG; Holter if symptomatic")],
[N("2° AV Block Mobitz I"), N("Identify cause; atropine if symptomatic; cardiology consult"), N("PPM if symptomatic or progressive. Treat reversible causes (inferior MI, drugs)")],
[N("2° AV Block Mobitz II"), N("Urgent cardiology consult\nTranscutaneous pacing on standby\nDO NOT give atropine (may worsen)"), N("Permanent pacemaker (Class I)")],
[N("3° Complete Heart Block"), N("URGENT: temporary transcutaneous/transvenous pacing\nAtropine if narrow QRS escape\nDopamine/isoproterenol if unstable"), N("Permanent pacemaker (Class I)\nExclude reversible causes first (Lyme, inferior MI, drug toxicity)")],
]
mgmt_tbl = Table(mgmt_data, colWidths=[4*cm, 6.5*cm, 6.5*cm], repeatRows=1)
mgmt_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, LIGHT_BLUE]),
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#AAAAAA")),
("BOX", (0,0), (-1,-1), 1, DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("FONTNAME", (0,1), (0,-1), "Helvetica-Bold"),
("ALIGN", (0,1), (0,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(mgmt_tbl)
story.append(Spacer(1, 0.4*cm))
# ============================================================
# CASE APPLICATION BOX
# ============================================================
case_text = (
"<b>Application to This Case (Durgi, 75F, 17-07-2026):</b> "
"HR 48 bpm | P duration 0 ms | PR 0 ms | QRS 75 ms (narrow) | QTc 423 ms — "
"<b>Junctional Escape Rhythm.</b> "
"No P waves, narrow QRS, regular rhythm at 48 bpm. No ST changes — no acute ischemia. "
"Priority: Rule out drug effects, hypothyroidism, and electrolyte imbalance. "
"Holter monitoring and cardiology referral recommended. If symptomatic and reversible "
"causes excluded — permanent pacemaker indicated."
)
case_data = [[Paragraph(case_text, S("Case", fontSize=9.5, fontName="Helvetica",
leading=14, textColor=DARK_BLUE, alignment=TA_JUSTIFY))]]
case_tbl = Table(case_data, colWidths=[17*cm])
case_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#EAF4FF")),
("BOX", (0,0), (-1,-1), 1.5, MED_BLUE),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
]))
story.append(case_tbl)
story.append(Spacer(1, 0.5*cm))
# ============================================================
# DISCLAIMER & FOOTER
# ============================================================
story.append(HRFlowable(width="100%", thickness=0.5, color=MID_GREY))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<b>Disclaimer:</b> This reference summary is for educational and clinical reference use only. "
"All management decisions must be made by a qualified clinician based on full clinical assessment. "
"Sources: Tintinalli's Emergency Medicine (Table 18-6), Fuster & Hurst's The Heart 15th Edition, "
"Harrison's Principles of Internal Medicine 22E, Frameworks for Internal Medicine (Lippincott).",
footer_style))
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph("Generated by Orris Medical AI — July 2026", footer_style))
# ── Build ────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved to: {OUTPUT}")
cd /tmp/workspace/ecg-summary && python build_pdf.py 2>&1
Junctional vs AV Block ECG Summary
PDF Document · PDF