~/peds-neuro-chart/generate_chart.py
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.colors import HexColor
# ── Color palette ────────────────────────────────────────────────────────────
NAVY = HexColor("#1A3557")
TEAL = HexColor("#1B7A78")
ORANGE = HexColor("#E07B39")
GOLD = HexColor("#F4C430")
LIGHT_BLUE = HexColor("#D6E8F7")
LIGHT_TEAL = HexColor("#D4EDEB")
LIGHT_ORG = HexColor("#FDEBD9")
LIGHT_GREY = HexColor("#F5F5F5")
MID_GREY = HexColor("#CCCCCC")
WHITE = colors.white
BLACK = colors.black
RED_LIGHT = HexColor("#FDE8E8")
GREEN_LIGHT= HexColor("#E8F5E8")
OUTPUT = "/home/daytona/workspace/peds-neuro-chart/Pediatric_Neuro_Exam_Quick_Reference.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
topMargin=1.2*cm, bottomMargin=1.0*cm,
leftMargin=1.2*cm, rightMargin=1.2*cm
)
styles = getSampleStyleSheet()
# ── Custom styles ─────────────────────────────────────────────────────────────
def S(name, parent='Normal', **kw):
return ParagraphStyle(name, parent=styles[parent], **kw)
title_style = S('MainTitle', 'Title',
fontSize=18, textColor=WHITE, alignment=TA_CENTER,
spaceAfter=2, spaceBefore=2, fontName='Helvetica-Bold')
subtitle_style = S('Subtitle', fontSize=9, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=2)
section_style = S('Section', fontSize=10, textColor=WHITE,
fontName='Helvetica-Bold', alignment=TA_CENTER)
col_head = S('ColHead', fontSize=8, textColor=WHITE,
fontName='Helvetica-Bold', alignment=TA_CENTER, leading=10)
cell_body = S('CellBody', fontSize=7.5, textColor=BLACK,
leading=10, spaceAfter=1)
cell_center = S('CellCenter', fontSize=7.5, textColor=BLACK,
leading=10, alignment=TA_CENTER)
cell_bold = S('CellBold', fontSize=7.5, textColor=NAVY,
fontName='Helvetica-Bold', leading=10)
cell_small = S('CellSmall', fontSize=6.8, textColor=BLACK, leading=9)
warn_style = S('Warn', fontSize=7, textColor=HexColor("#8B0000"),
fontName='Helvetica-Bold', leading=9)
note_style = S('Note', fontSize=6.5, textColor=HexColor("#444444"),
leading=9, alignment=TA_CENTER)
footer_style = S('Footer', fontSize=6.5, textColor=HexColor("#666666"),
alignment=TA_CENTER, leading=8)
# ── Helpers ───────────────────────────────────────────────────────────────────
def P(text, style=None):
if style is None:
style = cell_body
return Paragraph(text, style)
def section_banner(text, color=NAVY):
t = Table([[Paragraph(text, section_style)]], colWidths=[17.6*cm])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), color),
('TOPPADDING', (0,0),(-1,-1), 4),
('BOTTOMPADDING', (0,0),(-1,-1), 4),
('LEFTPADDING', (0,0),(-1,-1), 6),
('RIGHTPADDING', (0,0),(-1,-1), 6),
('ROUNDEDCORNERS', [4, 4, 4, 4]),
]))
return t
def std_table(header_row, data_rows, col_widths, header_color=NAVY,
alt_color=LIGHT_GREY):
header = [P(h, col_head) for h in header_row]
rows = [header]
for i, row in enumerate(data_rows):
rows.append([P(str(c), cell_body) if isinstance(c, str) else c
for c in row])
t = Table(rows, colWidths=col_widths, repeatRows=1)
style_cmds = [
('BACKGROUND', (0,0), (-1,0), header_color),
('GRID', (0,0), (-1,-1), 0.4, MID_GREY),
('ROWBACKGROUNDS',(0,1), (-1,-1), [WHITE, alt_color]),
('TOPPADDING', (0,0), (-1,-1), 3),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('LEFTPADDING', (0,0), (-1,-1), 4),
('RIGHTPADDING', (0,0), (-1,-1), 4),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
]
t.setStyle(TableStyle(style_cmds))
return t
# ── Story ──────────────────────────────────────────────────────────────────────
story = []
# ══════════════════════════════════════════════════════════════════════════════
# TITLE BANNER
# ══════════════════════════════════════════════════════════════════════════════
title_table = Table(
[[Paragraph("PEDIATRIC NEUROLOGICAL EXAMINATION", title_style)],
[Paragraph("Quick Reference Chart • Motor | Sensory | Reflexes | Cerebellar", subtitle_style)]],
colWidths=[17.6*cm]
)
title_table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), NAVY),
('TOPPADDING', (0,0),(-1,-1), 6),
('BOTTOMPADDING', (0,0),(-1,-1), 6),
]))
story.append(title_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 1: MOTOR EXAMINATION
# ══════════════════════════════════════════════════════════════════════════════
story.append(section_banner("1. MOTOR EXAMINATION", NAVY))
story.append(Spacer(1, 2*mm))
# 1A: Tone
story.append(P("<b>A. MUSCLE TONE</b> — passive limb movement (pronate/supinate forearm; flex/extend wrist; raise knees in supine)", cell_bold))
story.append(Spacer(1, 1*mm))
tone_data = [
["Normal", P("Mild resistance to passive movement; heels drag table when knees raised", cell_body)],
[P("<b>Spasticity</b>", cell_bold),
P("Velocity-dependent ↑ resistance → <b>corticospinal (UMN)</b> lesion", cell_body)],
[P("<b>Rigidity</b>", cell_bold),
P("Equal resistance at all angles → <b>extrapyramidal / basal ganglia</b> disease", cell_body)],
[P("<b>Cogwheel rigidity</b>", cell_bold),
P("Jerky interruptions in passive motion → <b>Parkinsonism</b>", cell_body)],
[P("<b>Paratonia</b>", cell_bold),
P("Fluctuating resistance → <b>frontal lobe</b> pathology", cell_body)],
[P("<b>Flaccidity</b>", cell_bold),
P("Absent tone → <b>LMN / peripheral nerve</b> disorder", cell_body)],
]
t = Table([[P(r[0], cell_body) if isinstance(r[0], str) else r[0], r[1]]
for r in tone_data],
colWidths=[4.0*cm, 13.6*cm])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (0,-1), LIGHT_BLUE),
('BACKGROUND', (1,0), (1,0), GREEN_LIGHT),
('GRID', (0,0),(-1,-1), 0.4, MID_GREY),
('TOPPADDING', (0,0),(-1,-1), 3),
('BOTTOMPADDING', (0,0),(-1,-1), 3),
('LEFTPADDING', (0,0),(-1,-1), 4),
('RIGHTPADDING', (0,0),(-1,-1), 4),
('VALIGN', (0,0),(-1,-1), 'TOP'),
]))
story.append(t)
story.append(Spacer(1, 2*mm))
# 1B: Strength grading
story.append(P("<b>B. MUSCLE STRENGTH — MRC Scale</b> (Pronator drift screen: arms extended, eyes closed 10s — pronation/flexion = UMN weakness)", cell_bold))
story.append(Spacer(1, 1*mm))
mrc_data = [
["5", "Full normal power"],
["4+ / 4 / 4−", "Movement against strong / moderate / mild resistance"],
["3", "Movement against gravity but NOT against resistance"],
["2", "Movement with gravity eliminated"],
["1", "Flicker/trace contraction — no joint movement"],
["0", "No contraction"],
]
t_mrc = std_table(
["Grade", "Description"],
mrc_data,
[2.5*cm, 15.1*cm],
header_color=TEAL
)
story.append(t_mrc)
story.append(Spacer(1, 2*mm))
# 1C: Patterns
story.append(P("<b>C. WEAKNESS PATTERNS</b>", cell_bold))
story.append(Spacer(1, 1*mm))
pat_data = [
["Unilateral UE extensors + LE flexors", "Pyramidal (UMN) tract"],
["Bilateral PROXIMAL weakness", "Myopathy"],
["Bilateral DISTAL weakness", "Peripheral neuropathy"],
["Fatigable weakness, normal/↓ tone, normal reflexes", "Neuromuscular junction disorder"],
]
t_pat = std_table(
["Pattern of Weakness", "Likely Localization"],
pat_data,
[9.0*cm, 8.6*cm],
header_color=TEAL
)
story.append(t_pat)
story.append(Spacer(1, 2*mm))
# 1D: Gait
story.append(P("<b>D. GAIT</b> — stride length, arm swing, posture, turning", cell_bold))
story.append(Spacer(1, 1*mm))
gait_data = [
["Hemiplegic", "Unilateral UMN damage — circumduction of leg"],
["Spastic / scissor", "Bilateral UMN — legs cross with each step"],
["Steppage", "Footdrop (LMN/peripheral nerve) — high knee lift"],
["Waddling", "Proximal muscle weakness (myopathy) — lateral trunk sway"],
["Parkinsonian", "Stooped, short shuffling steps, flexed arms, ↓ arm swing"],
["Ataxic / wide-based", "Cerebellar disease — variable stride, veering"],
]
t_gait = std_table(
["Gait Type", "Cause / Features"],
gait_data,
[4.5*cm, 13.1*cm],
header_color=TEAL
)
story.append(t_gait)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 2: SENSORY EXAMINATION
# ══════════════════════════════════════════════════════════════════════════════
story.append(section_banner("2. SENSORY EXAMINATION", TEAL))
story.append(Spacer(1, 2*mm))
story.append(P("Requires an alert, cooperative patient. Test major dermatomes; compare sides; observe for dermatomal loss or distal-to-proximal gradient.", cell_body))
story.append(Spacer(1, 1*mm))
# 2A Modalities
sens_data = [
[P("<b>Light Touch</b>", cell_bold),
"Cotton wisp on dermatomes of extremities + trunk",
"Dorsal column / medial lemniscus",
"Asymmetry, dermatomal or glove-stocking loss"],
[P("<b>Pin-prick (pain)</b>", cell_bold),
"Same areas with a pin",
"Spinothalamic tract",
"Dissociated loss (intact touch, lost pain) → central cord / Brown-Séquard"],
[P("<b>Temperature</b>", cell_bold),
"Cool object on skin (mirrors pain pathway)",
"Spinothalamic tract",
"Loss parallels pain loss"],
[P("<b>Vibration</b>", cell_bold),
"128-Hz tuning fork on bony prominences; patient says when vibration stops",
"Dorsal column",
"Loss → posterior column or peripheral neuropathy; test spinous processes for myelopathy level"],
[P("<b>Proprioception</b>", cell_bold),
"Grasp digit on sides; move up/down; even very small movements should be detected (great toe, distal thumb)",
"Dorsal column",
"Loss → posterior column, peripheral neuropathy, or tabes dorsalis"],
[P("<b>Stereognosis</b>", cell_bold),
"Identify small objects placed in hand without looking",
"Cortical integration (parietal)",
"Astereognosis → contralateral parietal lobe lesion"],
[P("<b>Graphesthesia</b>", cell_bold),
"Identify numbers written on palm",
"Cortical integration (parietal)",
"Loss → parietal lobe (if primary sensation intact)"],
]
t_sens = std_table(
["Modality", "Technique", "Pathway", "Abnormal Finding"],
sens_data,
[3.2*cm, 5.0*cm, 4.2*cm, 5.2*cm],
header_color=TEAL
)
story.append(t_sens)
story.append(Spacer(1, 2*mm))
# 2B Romberg
romberg_data = [
[P("<b>Romberg Test</b>", cell_bold),
"Patient stands feet together, then closes eyes",
P("Positive (falls eyes closed) → proprioceptive loss (peripheral or posterior column); also positive in vestibular/cerebellar disease", warn_style)],
]
t_rom = Table(romberg_data, colWidths=[3.2*cm, 7.0*cm, 7.4*cm])
t_rom.setStyle(TableStyle([
('BACKGROUND', (0,0),(0,-1), LIGHT_TEAL),
('BACKGROUND', (1,0),(-1,-1), LIGHT_GREY),
('GRID', (0,0),(-1,-1), 0.4, MID_GREY),
('TOPPADDING', (0,0),(-1,-1), 3),
('BOTTOMPADDING', (0,0),(-1,-1), 3),
('LEFTPADDING', (0,0),(-1,-1), 4),
('RIGHTPADDING', (0,0),(-1,-1), 4),
('VALIGN', (0,0),(-1,-1), 'TOP'),
]))
story.append(t_rom)
story.append(Spacer(1, 2*mm))
# 2C Dermatomes
story.append(P("<b>Key Dermatome Landmarks</b>", cell_bold))
story.append(Spacer(1, 1*mm))
derm_data = [
["C5–C6", "Lateral upper limb; C6 = thumb"],
["C8", "Ring and little fingers"],
["T4", "Nipple level"],
["T10", "Umbilicus"],
["L1", "Groin / inguinal region"],
["L4–5, S1", "Foot"],
["S2–4", "Perineum"],
]
t_derm = std_table(
["Root", "Region"],
derm_data,
[2.5*cm, 15.1*cm],
header_color=TEAL
)
story.append(t_derm)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 3: REFLEX EXAMINATION
# ══════════════════════════════════════════════════════════════════════════════
story.append(section_banner("3. REFLEX EXAMINATION", ORANGE))
story.append(Spacer(1, 2*mm))
# 3A Grading
story.append(P("<b>A. DTR GRADING SCALE</b> (Jendrassik maneuver to reinforce: teeth-clench for UL; hook fingers for Achilles)", cell_bold))
story.append(Spacer(1, 1*mm))
grade_data = [
["0", "Absent", P("<b>Abnormal</b> — LMN, peripheral nerve, or severe neuromuscular disease", warn_style)],
["1", "Present but diminished", "May be normal or LMN / peripheral neuropathy"],
["2", "Normoactive", P("<b>Normal</b>", cell_bold)],
["3", "Brisker than average, no clonus", "May be UMN or anxiety — check symmetry"],
["4", "Pathologically increased + clonus", P("<b>Abnormal</b> — UMN lesion", warn_style)],
]
t_grade = std_table(
["Grade", "Description", "Interpretation"],
grade_data,
[1.5*cm, 5.5*cm, 10.6*cm],
header_color=ORANGE
)
story.append(t_grade)
story.append(Spacer(1, 2*mm))
# 3B Deep Tendon Reflexes
story.append(P("<b>B. DEEP TENDON REFLEXES (DTRs)</b>", cell_bold))
story.append(Spacer(1, 1*mm))
dtr_data = [
["Biceps", "C5, C6", "Strike biceps tendon with patient's elbow partly flexed"],
["Brachioradialis", "C5, C6", "Strike radial styloid with wrist in neutral"],
["Triceps", "C6, C7", "Strike triceps tendon with elbow flexed to 90°"],
["Finger flexors", "C8, T1", "Flick examiner's fingers resting on patient's fingers"],
["Patellar (knee)", "L3, L4", "Strike patellar tendon below patella, knee flexed"],
["Achilles (ankle)", "S1, S2", "Strike Achilles tendon with foot slightly dorsiflexed"],
]
t_dtr = std_table(
["Reflex", "Root Level", "Technique"],
dtr_data,
[3.5*cm, 2.5*cm, 11.6*cm],
header_color=ORANGE
)
story.append(t_dtr)
story.append(Spacer(1, 2*mm))
# 3C Cutaneous reflexes
story.append(P("<b>C. CUTANEOUS (SUPERFICIAL) REFLEXES</b>", cell_bold))
story.append(Spacer(1, 1*mm))
cut_data = [
[P("<b>Plantar (Babinski)</b>", cell_bold),
"Stroke lateral sole heel → ball → great toe with blunt object",
P("<b>Normal:</b> Plantar flexion of toes", cell_body),
P("<b>Abnormal:</b> Extension of great toe + fanning (Babinski sign) → UMN above S1. ALWAYS abnormal after age 3 years", warn_style)],
[P("<b>Abdominal</b>", cell_bold),
"Stroke each quadrant diagonally toward umbilicus",
P("<b>Normal:</b> Umbilicus moves toward stimulus (T9–T12)", cell_body),
P("<b>Absent:</b> UMN lesion. Preserved upper (T9) only → lesion T9–T12", warn_style)],
[P("<b>Cremasteric</b>", cell_bold),
"Stroke medial thigh",
P("<b>Normal:</b> Ipsilateral testicular elevation (L1–L2)", cell_body),
P("<b>Absent:</b> L1–L2 lesion or UMN disease", warn_style)],
[P("<b>Anal</b>", cell_bold),
"Scratch perianal skin",
P("<b>Normal:</b> Anal sphincter contraction (S2–S4)", cell_body),
P("<b>Absent:</b> S2–S4 lesion — test in any suspected spinal cord / lumbosacral injury", warn_style)],
]
t_cut = std_table(
["Reflex", "Technique", "Normal Response", "Abnormal / Clinical Significance"],
cut_data,
[3.2*cm, 4.5*cm, 4.3*cm, 5.6*cm],
header_color=ORANGE
)
story.append(t_cut)
story.append(Spacer(1, 2*mm))
# 3D Primitive reflexes
story.append(P("<b>D. PRIMITIVE / FRONTAL RELEASE REFLEXES</b> (Normal in neonates; pathological if persisting/reappearing in older children)", cell_bold))
story.append(Spacer(1, 1*mm))
prim_data = [
["Suck reflex", "Touch center of lips with tongue blade",
"Lip pursing / sucking movement", "Normal neonate; abnormal if persistent → frontal lobe disease"],
["Rooting reflex", "Touch corner of lips",
"Head/lips turn toward stimulus", "Normal neonate; abnormal if persistent → frontal pathology"],
["Grasp reflex", "Touch palm between thumb and index finger",
"Forced grasp of examiner's hand", "Normal neonate; abnormal if persistent → frontal lobe disease"],
["Palmomental", "Scratch diagonally across palm",
"Ipsilateral chin (mentalis) contraction", "Frontal lobe dysfunction"],
]
t_prim = std_table(
["Reflex", "Technique", "Response", "Clinical Significance"],
prim_data,
[3.2*cm, 4.5*cm, 3.5*cm, 6.4*cm],
header_color=ORANGE
)
story.append(t_prim)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 4: CEREBELLAR EXAMINATION
# ══════════════════════════════════════════════════════════════════════════════
story.append(section_banner("4. CEREBELLAR EXAMINATION (SARA domains)", TEAL))
story.append(Spacer(1, 2*mm))
cer_data = [
[P("<b>Gait</b>", cell_bold),
"Walk normally; run or climb stairs for subtle deficits",
P("<b>Normal:</b> Smooth, narrow-based", cell_body),
P("Wide-based, veering, variable stride — develops as compensatory mechanism in moderate/severe ataxia", cell_body)],
[P("<b>Stance</b>", cell_bold),
"Stand feet together → tandem stance → single leg → hop",
P("<b>Normal:</b> Stable upright posture", cell_body),
P("Truncal sway; inability to tandem stance", cell_body)],
[P("<b>Sitting</b>", cell_bold),
"Observe sitting without back support",
P("<b>Normal:</b> Stable", cell_body),
P("Truncal sway while sitting", cell_body)],
[P("<b>Speech</b>", cell_bold),
"Listen during conversation; ask to repeat phrases",
P("<b>Normal:</b> Fluent, prosodic", cell_body),
P("<b>Scanning speech:</b> slow, irregular force, unnecessary syllable pauses (dysarthria)", cell_body)],
[P("<b>Finger-Nose Test</b>", cell_bold),
"Arm extended → touch nose → touch examiner's moving index finger; repeat",
P("<b>Normal:</b> Smooth, accurate", cell_body),
P("<b>Intention tremor</b> (oscillation ↑ near target); <b>dysmetria</b> (past-pointing)", cell_body)],
[P("<b>Finger Chase</b>", cell_bold),
"Patient's index finger follows examiner's moving finger",
P("<b>Normal:</b> Accurate tracking", cell_body),
P("<b>Hypermetria/hypometria</b> — overshoot or undershoot", cell_body)],
[P("<b>Rapid Alternating Movements</b>", cell_bold),
"Rapid pronation/supination; finger tapping",
P("<b>Normal:</b> Smooth, rhythmic", cell_body),
P("<b>Dysdiadochokinesia</b> — slow, irregular rhythm", cell_body)],
[P("<b>Heel-Knee-Shin</b>", cell_bold),
"Supine: lift leg, place heel on opposite knee, slide heel smoothly down shin",
P("<b>Normal:</b> Smooth, accurate", cell_body),
P("Heel falls off shin; irregular movement", cell_body)],
[P("<b>Eye Movements</b>", cell_bold),
"Assess fixation, smooth pursuit, and saccades",
P("<b>Normal:</b> Stable gaze, smooth pursuit, accurate saccades", cell_body),
P("End-gaze nystagmus; square-wave jerks (Friedreich); saccadic pursuit (SCA3); slow saccades (SCA2); hypo/hypermetric saccades", cell_body)],
]
t_cer = std_table(
["Domain", "Technique", "Normal", "Abnormal Finding"],
cer_data,
[3.0*cm, 5.0*cm, 3.8*cm, 5.8*cm],
header_color=TEAL
)
story.append(t_cer)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 5: LOCALIZATION SUMMARY TABLE
# ══════════════════════════════════════════════════════════════════════════════
story.append(section_banner("5. LOCALIZATION AT A GLANCE — UMN vs LMN vs CEREBELLAR", NAVY))
story.append(Spacer(1, 2*mm))
loc_data = [
[P("<b>Tone</b>", cell_bold),
P("↑ Spasticity", cell_body),
P("↓ Flaccidity", cell_body),
P("Normal or ↓", cell_body)],
[P("<b>Strength</b>", cell_bold),
P("Pyramidal pattern (UE ext, LE flex)", cell_body),
P("Focal / distal pattern", cell_body),
P("Normal power", cell_body)],
[P("<b>DTRs</b>", cell_bold),
P("Hyperreflexia (grade 3–4)", cell_body),
P("Hypo / Absent (grade 0–1)", cell_body),
P("Normal", cell_body)],
[P("<b>Babinski</b>", cell_bold),
P("<b>Positive</b> (extensor plantar)", warn_style),
P("Negative", cell_body),
P("Negative", cell_body)],
[P("<b>Fasciculations / Atrophy</b>", cell_bold),
P("Absent / mild disuse atrophy", cell_body),
P("Present (fasciculations + marked atrophy)", warn_style),
P("Absent", cell_body)],
[P("<b>Coordination</b>", cell_bold),
P("Impaired due to weakness", cell_body),
P("May be impaired", cell_body),
P("<b>Markedly impaired</b> (ipsilateral) — intention tremor, dysmetria, dysdiadochokinesia", warn_style)],
[P("<b>Gait</b>", cell_bold),
P("Spastic / hemiplegic", cell_body),
P("Steppage / waddling", cell_body),
P("Wide-based / ataxic", cell_body)],
[P("<b>Lesion site</b>", cell_bold),
P("Above anterior horn cell (brain, cord above lesion)", cell_body),
P("Anterior horn cell, nerve root, peripheral nerve", cell_body),
P("Cerebellum (ipsilateral hemisphere or vermis)", cell_body)],
]
t_loc = Table(
[[P("Feature", col_head), P("UMN Lesion", col_head),
P("LMN Lesion", col_head), P("Cerebellar Lesion", col_head)]] + loc_data,
colWidths=[3.5*cm, 4.7*cm, 4.7*cm, 4.7*cm],
repeatRows=1
)
t_loc.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), NAVY),
('BACKGROUND', (1,1), (1,-1), LIGHT_BLUE),
('BACKGROUND', (2,1), (2,-1), RED_LIGHT),
('BACKGROUND', (3,1), (3,-1), LIGHT_TEAL),
('BACKGROUND', (0,1), (0,-1), LIGHT_GREY),
('GRID', (0,0), (-1,-1), 0.4, MID_GREY),
('TOPPADDING', (0,0),(-1,-1), 3),
('BOTTOMPADDING', (0,0),(-1,-1), 3),
('LEFTPADDING', (0,0),(-1,-1), 4),
('RIGHTPADDING', (0,0),(-1,-1), 4),
('VALIGN', (0,0),(-1,-1), 'TOP'),
]))
story.append(t_loc)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════════
# FOOTER
# ══════════════════════════════════════════════════════════════════════════════
story.append(HRFlowable(width="100%", thickness=0.5, color=MID_GREY))
story.append(Spacer(1, 1*mm))
story.append(Paragraph(
"Sources: Textbook of Family Medicine 9e | Harrison's Principles of Internal Medicine 22E | "
"Bradley and Daroff's Neurology in Clinical Practice | Adams and Victor's Principles of Neurology 12E • "
"For clinical use as a quick reference only — always apply clinical judgment.",
footer_style
))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF created: {OUTPUT}")