~/dev-milestones-card/generate_card.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import PageTemplate, Frame
from reportlab.pdfgen import canvas as pdf_canvas
OUTPUT = "/home/daytona/workspace/dev-milestones-card/Developmental_Milestones_QuickRef.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1B3A6B")
TEAL = colors.HexColor("#0D7B8A")
LIGHT_TEAL = colors.HexColor("#E0F4F6")
AMBER = colors.HexColor("#F5A623")
LIGHT_AMBER= colors.HexColor("#FFF8EC")
RED = colors.HexColor("#C0392B")
LIGHT_RED = colors.HexColor("#FDECEA")
GREEN = colors.HexColor("#1A7A3C")
LIGHT_GREEN= colors.HexColor("#EAF6EE")
GREY_BG = colors.HexColor("#F4F6FB")
GREY_LINE = colors.HexColor("#D0D7E6")
WHITE = colors.white
BLACK = colors.HexColor("#1A1A2E")
# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
title_style = S("CardTitle",
fontSize=22, leading=28, alignment=TA_CENTER,
textColor=WHITE, fontName="Helvetica-Bold", spaceAfter=2)
subtitle_style = S("CardSub",
fontSize=10, leading=14, alignment=TA_CENTER,
textColor=colors.HexColor("#BDE3EA"), fontName="Helvetica", spaceAfter=6)
section_head = S("SHead",
fontSize=9, leading=12, alignment=TA_CENTER,
textColor=WHITE, fontName="Helvetica-Bold")
cell_head = S("CellHead",
fontSize=7.5, leading=10, alignment=TA_CENTER,
textColor=WHITE, fontName="Helvetica-Bold")
cell_text = S("CellTxt",
fontSize=7, leading=9.5, alignment=TA_LEFT,
textColor=BLACK, fontName="Helvetica")
cell_bold = S("CellBold",
fontSize=7, leading=9.5, alignment=TA_LEFT,
textColor=BLACK, fontName="Helvetica-Bold")
age_style = S("AgeStyle",
fontSize=8.5, leading=11, alignment=TA_CENTER,
textColor=WHITE, fontName="Helvetica-Bold")
red_flag_head = S("RFHead",
fontSize=9, leading=12, alignment=TA_CENTER,
textColor=WHITE, fontName="Helvetica-Bold")
red_flag_text = S("RFText",
fontSize=7, leading=9.5, alignment=TA_LEFT,
textColor=BLACK, fontName="Helvetica")
footer_style = S("Footer",
fontSize=6.5, leading=9, alignment=TA_CENTER,
textColor=colors.HexColor("#888888"), fontName="Helvetica")
note_style = S("Note",
fontSize=7, leading=9.5, alignment=TA_LEFT,
textColor=colors.HexColor("#444444"), fontName="Helvetica-Oblique")
def P(text, style):
return Paragraph(text, style)
# ── Helper ────────────────────────────────────────────────────────────────────
def milestone_table():
"""Big milestone table: columns = domains, rows = age bands."""
col_headers = ["Age", "Gross Motor", "Fine Motor", "Language", "Cognitive", "Social / Emotional"]
col_widths = [19*mm, 29*mm, 27*mm, 31*mm, 29*mm, 31*mm]
hdr_row = [P(h, cell_head) for h in col_headers]
def row(age, gm, fm, lang, cog, soc, bg=WHITE):
return [
P(age, age_style),
P(gm, cell_text),
P(fm, cell_text),
P(lang, cell_text),
P(cog, cell_text),
P(soc, cell_text),
], bg
rows_data = [
row("Birth–\n4 wks",
"Moves all limbs; head lateral in prone",
"Grasp reflex; hand fist",
"Cries; differentiates sounds",
"Visual tracks midline; startles to sound",
"Responds to mother's voice; quiets when held"),
row("2\nmonths",
"Holds head up on tummy",
"Opens hands briefly; swipes at objects",
"Cooing; reacts to loud sounds",
"Watches movement; looks at toy briefly",
"Social smile; calms when spoken to", LIGHT_TEAL),
row("4\nmonths",
"Pushes onto elbows in prone; head steady",
"Holds toy placed in hand; brings hands to mouth",
"Cooing 'ooo/aah'; turns to voice",
"Opens mouth seeing breast/bottle",
"Chuckles; smiles to get attention"),
row("6\nmonths",
"Rolls tummy→back; sits leaning on hands",
"Reaches for toy; transfers object",
"Babbling starts; raspberries",
"Explores with mouth; closes lips to refuse food",
"Laughs; recognises familiar faces; mirror play", LIGHT_TEAL),
row("9\nmonths",
"Sits without support; begins to crawl",
"Raking grasp → early pincer",
"'Mama/baba/dada' (non-specific); lifts arms",
"Object permanence begins; bangs two objects",
"Stranger & separation anxiety; peek-a-boo"),
row("12\nmonths",
"Pulls to stand; cruises furniture; may take first steps",
"Pincer grasp (thumb + index)",
"'Mama/Dada' with meaning; waves bye-bye; understands 'no'",
"Puts block in cup; looks for hidden toy",
"Plays pat-a-cake; affectionate", LIGHT_TEAL),
row("15\nmonths",
"Walks independently; climbs",
"Stacks 2 blocks; scribbles",
"1–2 words (not mama/dada); points to ask",
"Uses objects correctly (cup, phone, book)",
"Copies other children; shows objects; hugs toys"),
row("18\nmonths",
"Runs (unsteadily); climbs stairs with help",
"Stacks 3–4 blocks; feeds self with spoon",
"~10 words; points to show interest",
"Begins pretend play",
"Parallel play; temper tantrums begin", LIGHT_TEAL),
row("2\nyears",
"Runs well; kicks ball",
"Stacks 6 blocks; turns pages",
"2-word phrases; 50+ vocabulary",
"Symbolic play; sorts shapes/colours",
"Imitates adults; plays alongside peers"),
row("3\nyears",
"Rides tricycle; alternates feet on stairs",
"Copies circle; uses scissors",
"3-word sentences; asks 'why'",
"Understands 'two'; counts to 3",
"Cooperative play; knows name & gender; dresses with help", LIGHT_TEAL),
row("4\nyears",
"Hops on one foot; catches bounced ball",
"Draws 3-part person; copies cross",
"Tells stories; 4–5 word sentences",
"Counts to 4; names colours",
"Rule-based games; best friends; imaginative play"),
row("5\nyears",
"Skips; pumps swing",
"Buttons clothes; draws 6-part person",
"Keeps 3+ back-and-forth conversation; simple rhymes",
"Counts to 10; writes some letters; 5–10 min attention",
"Follows rules; chores; cooperative songs/dance", LIGHT_TEAL),
]
table_data = [hdr_row]
row_bgs = [TEAL] # header bg
for rd, bg in rows_data:
table_data.append(rd)
row_bgs.append(bg)
tbl = Table(table_data, colWidths=col_widths, repeatRows=1)
style_cmds = [
("BACKGROUND", (0, 0), (-1, 0), TEAL),
("GRID", (0, 0), (-1, -1), 0.3, GREY_LINE),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_TEAL]),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 3),
("RIGHTPADDING", (0, 0), (-1, -1), 3),
("BACKGROUND", (0, 1), (0, -1), NAVY), # age column
("ALIGN", (0, 0), (0, -1), "CENTER"),
("VALIGN", (0, 0), (0, -1), "MIDDLE"),
]
tbl.setStyle(TableStyle(style_cmds))
return tbl
def red_flags_table():
headers = ["Age", "Red Flag - Refer Immediately"]
col_widths = [22*mm, 144*mm]
hdr = [P(h, cell_head) for h in headers]
flags = [
("ANY age", "Loss of previously acquired developmental skills (regression) — always urgent"),
("ANY age", "Parental / caregiver concern about development"),
("2 months", "No social smile; does not respond to sounds"),
("6 months", "No reaching or grasping; not laughing"),
("9 months", "No babbling; no object permanence"),
("12 months", "No single words; no pointing or waving; no response to name"),
("16 months", "No single words at all"),
("24 months", "No 2-word phrases; no functional play"),
("Any age", "No babbling + no pointing + social withdrawal → screen for AUTISM (M-CHAT-R/F 16–30 months)"),
]
rows = [hdr]
for i, (age, flag) in enumerate(flags):
bg = LIGHT_RED if "ANY" in age or "AUTISM" in flag else (LIGHT_AMBER if i % 2 == 0 else WHITE)
rows.append([P(age, cell_bold), P(flag, red_flag_text)])
tbl = Table(rows, colWidths=col_widths, repeatRows=1)
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), RED),
("GRID", (0, 0), (-1, -1), 0.3, GREY_LINE),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
# Highlight ANY-age rows
("BACKGROUND", (0, 1), (-1, 2), LIGHT_RED),
("BACKGROUND", (0, 9), (-1, 9), LIGHT_RED),
("ROWBACKGROUNDS", (0, 3), (-1, 8), [LIGHT_AMBER, WHITE]),
]))
return tbl
def erikson_table():
headers = ["Stage", "Age", "Crisis", "Virtue", "Nursing Tip"]
col_widths = [10*mm, 20*mm, 34*mm, 18*mm, 84*mm]
hdr = [P(h, cell_head) for h in headers]
stages = [
("1", "Birth–18 mo", "Trust vs Mistrust", "Hope",
"Consistent, prompt, warm caregiving; respond to infant cues reliably"),
("2", "18 mo–3 yr", "Autonomy vs Shame & Doubt", "Will",
"Offer safe choices; support toilet training without punishment"),
("3", "3–6 yr", "Initiative vs Guilt", "Purpose",
"Encourage imaginative play; avoid over-criticism of questions"),
("4", "6–12 yr", "Industry vs Inferiority", "Competence",
"Praise effort at school; support skill-building; avoid negative comparisons"),
("5", "12–18 yr", "Identity vs Role Confusion", "Fidelity",
"Respect autonomy; involve in care decisions; support peer relationships"),
]
rows = [hdr]
bgs = [LIGHT_TEAL, WHITE]
for i, s in enumerate(stages):
rows.append([P(t, cell_text) for t in s])
tbl = Table(rows, colWidths=col_widths, repeatRows=1)
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), TEAL),
("GRID", (0, 0), (-1, -1), 0.3, GREY_LINE),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [LIGHT_TEAL, WHITE]),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
]))
return tbl
def primitives_table():
headers = ["Reflex", "Stimulus", "Response", "Disappears"]
col_widths = [28*mm, 45*mm, 55*mm, 38*mm]
hdr = [P(h, cell_head) for h in headers]
refs = [
("Rooting", "Touch cheek/perioral skin", "Turns head, opens mouth, purses lips", "~4 months"),
("Moro (Startle)", "Sudden head drop backward", "Arms abduct & extend, then embrace", "~4 months"),
("Grasp", "Object placed in palm", "Fingers curl around object", "~4 months"),
("Tonic Neck (ATNR)", "Head turned to one side", "Arm extends same side, flexes opposite ('fencing')", "~4 months"),
("Babinski", "Stroke sole of foot", "Toes fan out (dorsiflexion of big toe)", "~12 months"),
("Stepping", "Held upright, sole touches surface", "Stepping movement", "~2 months"),
("Sucking", "Object in mouth", "Rhythmic sucking", "Gradually fades"),
]
rows = [hdr]
for r in refs:
rows.append([P(t, cell_text) for t in r])
tbl = Table(rows, colWidths=col_widths, repeatRows=1)
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), TEAL),
("GRID", (0, 0), (-1, -1), 0.3, GREY_LINE),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_TEAL]),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
]))
return tbl
def memory_table():
items = [
("Social smile", "6 weeks"),
("Stranger anxiety", "9 months"),
("First words with meaning", "12 months"),
("2-word phrases", "24 months"),
("Toilet trained (day)", "2½ years"),
("Toilet trained (night)", "4 years"),
("Counts to 10", "5 years"),
("Most primitive reflexes gone", "4 months"),
("Babinski reflex gone", "12 months"),
("Gender identity fixed", "24–30 months"),
]
col_widths = [50*mm, 30*mm] * 2
# split into 2 columns of pairs
left = items[:5]
right = items[5:]
rows = []
for i in range(5):
l = left[i]
r = right[i]
rows.append([
P(l[0], cell_text), P(l[1], cell_bold),
P(r[0], cell_text), P(r[1], cell_bold),
])
tbl = Table(rows, colWidths=col_widths)
tbl.setStyle(TableStyle([
("GRID", (0, 0), (-1, -1), 0.3, GREY_LINE),
("ROWBACKGROUNDS", (0, 0), (-1, -1), [LIGHT_GREEN, WHITE]),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("BACKGROUND", (1, 0), (1, -1), colors.HexColor("#D5EFE0")),
("BACKGROUND", (3, 0), (3, -1), colors.HexColor("#D5EFE0")),
("ALIGN", (1, 0), (1, -1), "CENTER"),
("ALIGN", (3, 0), (3, -1), "CENTER"),
("FONTNAME", (1, 0), (1, -1), "Helvetica-Bold"),
("FONTNAME", (3, 0), (3, -1), "Helvetica-Bold"),
("TEXTCOLOR", (1, 0), (1, -1), GREEN),
("TEXTCOLOR", (3, 0), (3, -1), GREEN),
]))
return tbl
# ── Section label helper ──────────────────────────────────────────────────────
def section_label(text, bg=NAVY):
data = [[P(text, section_head)]]
tbl = Table(data, colWidths=[166*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("ROUNDEDCORNERS", [3, 3, 3, 3]),
]))
return tbl
# ── Header banner (drawn as a table) ─────────────────────────────────────────
def header_banner():
title_text = "Developmental Milestones Quick-Reference Card"
sub_text = "Child Health Nursing | Undergraduate Nursing | Based on CDC / AAP / Harriet Lane Handbook"
data = [
[P(title_text, title_style)],
[P(sub_text, subtitle_style)],
]
tbl = Table(data, colWidths=[166*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("TOPPADDING", (0, 0), (0, 0), 10),
("BOTTOMPADDING", (0, 0), (0, 0), 2),
("TOPPADDING", (0, 1), (0, 1), 2),
("BOTTOMPADDING", (0, 1), (0, 1), 10),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
]))
return tbl
# ── Build document ────────────────────────────────────────────────────────────
def build():
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=12*mm, rightMargin=12*mm,
topMargin=12*mm, bottomMargin=14*mm,
title="Developmental Milestones Quick-Reference Card",
author="Child Health Nursing - Orris"
)
story = []
# Header
story.append(header_banner())
story.append(Spacer(1, 4*mm))
# ── Milestones table ──
story.append(section_label(" DEVELOPMENTAL MILESTONES BY AGE "))
story.append(Spacer(1, 1.5*mm))
story.append(milestone_table())
story.append(Spacer(1, 3*mm))
# ── Primitive reflexes ──
story.append(section_label(" PRIMITIVE REFLEXES "))
story.append(Spacer(1, 1.5*mm))
story.append(primitives_table())
story.append(Spacer(1, 3*mm))
# ── Red flags ──
story.append(section_label(" DEVELOPMENTAL RED FLAGS ", bg=RED))
story.append(Spacer(1, 1.5*mm))
story.append(red_flags_table())
story.append(Spacer(1, 3*mm))
# ── Erikson ──
story.append(section_label(" ERIKSON'S PSYCHOSOCIAL STAGES (CHILD-RELEVANT) "))
story.append(Spacer(1, 1.5*mm))
story.append(erikson_table())
story.append(Spacer(1, 3*mm))
# ── Memory aids ──
story.append(section_label(" KEY MEMORY FACTS ", bg=GREEN))
story.append(Spacer(1, 1.5*mm))
story.append(memory_table())
story.append(Spacer(1, 3*mm))
# ── Screening note ──
screening_data = [[
P("<b>Developmental Screening Schedule (AAP):</b> Formal screening at <b>9, 18, and 30 months</b> "
"(24 months if 30-month visit unavailable). Developmental surveillance at <b>every</b> well-child visit. "
"M-CHAT-R/F for autism at <b>16–30 months</b>.", note_style)
]]
screen_tbl = Table(screening_data, colWidths=[166*mm])
screen_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), LIGHT_AMBER),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("BOX", (0, 0), (-1, -1), 0.5, AMBER),
]))
story.append(screen_tbl)
story.append(Spacer(1, 2*mm))
# Footer
story.append(HRFlowable(width="100%", thickness=0.5, color=GREY_LINE))
story.append(Spacer(1, 1*mm))
story.append(P(
"Sources: Harriet Lane Handbook 23rd Ed. • CDC Developmental Milestones (2022) • "
"Kaplan & Sadock's Synopsis of Psychiatry • AAP Developmental Surveillance Guidelines | "
"Generated by Orris — For educational use only",
footer_style
))
doc.build(story)
print(f"PDF saved to: {OUTPUT}")
build()