~/dev-milestones/generate_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.pdfgen import canvas
OUTPUT = "/home/daytona/workspace/dev-milestones/Developmental_Milestones_Quick_Reference.pdf"
# ── Colour palette ──────────────────────────────────────────────
C_HEADER = colors.HexColor("#1B4F72") # deep navy – page header
C_AGE_BG = colors.HexColor("#2E86C1") # blue – age band header
C_AGE_FG = colors.white
C_MOTOR = colors.HexColor("#1A5276") # dark blue
C_LANG = colors.HexColor("#1E8449") # green
C_COG = colors.HexColor("#7D3C98") # purple
C_SOC = colors.HexColor("#CA6F1E") # amber
C_RED = colors.HexColor("#C0392B") # red flags
C_LIGHT1 = colors.HexColor("#D6EAF8") # motor row bg
C_LIGHT2 = colors.HexColor("#D5F5E3") # lang row bg
C_LIGHT3 = colors.HexColor("#E8DAEF") # cog row bg
C_LIGHT4 = colors.HexColor("#FDEBD0") # soc row bg
C_RED_LIGHT= colors.HexColor("#FADBD8") # red flag bg
C_RULE = colors.HexColor("#AED6F1")
C_FOOTER = colors.HexColor("#566573")
PAGE_W, PAGE_H = A4
MARGIN = 14 * mm
# ── Styles ───────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
title_style = S("Title2",
fontSize=20, textColor=C_HEADER, alignment=TA_CENTER,
fontName="Helvetica-Bold", spaceAfter=2)
subtitle_style = S("Sub",
fontSize=10, textColor=C_FOOTER, alignment=TA_CENTER,
fontName="Helvetica", spaceAfter=8)
section_style = S("Section",
fontSize=11, textColor=colors.white, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=14)
age_label_style = S("AgeLabel",
fontSize=12, textColor=colors.white, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=14)
domain_style = S("Domain",
fontSize=8.5, textColor=colors.white, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=11)
cell_style = S("Cell",
fontSize=8, textColor=colors.HexColor("#1C2833"),
fontName="Helvetica", leading=11, leftIndent=3, rightIndent=3)
red_head_style = S("RedHead",
fontSize=10, textColor=C_RED, fontName="Helvetica-Bold",
spaceAfter=4, spaceBefore=6)
red_cell_style = S("RedCell",
fontSize=8, textColor=colors.HexColor("#1C2833"),
fontName="Helvetica", leading=11, leftIndent=3, rightIndent=3)
tip_style = S("Tip",
fontSize=8.5, textColor=colors.HexColor("#1C2833"),
fontName="Helvetica-Oblique", leading=12, leftIndent=6)
footer_style = S("Footer",
fontSize=7.5, textColor=C_FOOTER, fontName="Helvetica",
alignment=TA_CENTER)
# ── Milestone data ───────────────────────────────────────────────
# Each entry: (age_label, motor, language, cognitive, social)
MILESTONES = [
("2 Months",
"Holds head up prone\nMoves all 4 limbs\nOpens hands briefly",
"Sounds beyond crying\nReacts to loud sounds",
"Watches faces move\nLooks at toy for seconds",
"Calms when picked up/spoken to\nSmiles in response to face"),
("4 Months",
"Head steady when held\nHolds toy placed in hand\nPushes onto elbows prone\nHands to mouth",
"Cooing ('ooo','aahh')\nTurns head toward voice\nVocalises back when talked to",
"Opens mouth seeing breast/bottle\nLooks at hands with interest",
"Spontaneous social smile\nChuckles when made to laugh\nMakes sounds to hold attention"),
("6 Months",
"Rolls tummy→back\nSits with support\nPushes up on straight arms\nReaches/grabs objects",
"Babbles; takes turns vocalising\nBlows 'raspberries'\nResponds to own name",
"Brings objects to mouth\nReaches for nearby toys\nLooks for dropped object",
"Recognises familiar people\nLaughs out loud\nLooks at self in mirror"),
("9 Months",
"Sits independently\nBegins crawling\nPulls to stand (some)\nRaking grasp→early pincer",
"Mama/dada (non-specific)\nBacks-and-forth sounds\nLifts arms to be picked up",
"Looks for dropped objects\nBangs two objects together\nPermanence: finds partly hidden toy",
"Stranger anxiety begins\nSeparation anxiety\nWaves bye-bye (some)"),
("12 Months",
"Pulls to stand; cruises furniture\nFirst independent steps (some)\nPincer grasp (thumb+index)\nDrinks from held cup",
"Mama/dada with intent\nUnderstands 'no'\n1–2 words (some)\nPoints to request",
"Object permanence – finds hidden toy\nPuts objects in containers",
"Plays pat-a-cake\nWaves bye-bye\nShows affection"),
("15 Months",
"Takes several steps alone\nFeeds self finger food",
"1–2 words beyond mama/dada\nPoints to ask/get help\nFollows directions + gesture",
"Uses objects correctly (cup, phone)\nStacks 2 blocks",
"Copies other children\nShows favourite objects\nClaps when excited; hugs toys"),
("18 Months",
"Walks independently\nClimbs on/off furniture\nScribbles\nTries to use spoon",
"3+ words beyond mama/dada\nFollows 1-step direction (no gesture)",
"Simple pretend play\nCopies simple chores (sweeping)",
"Moves from parent but checks back\nPoints to share interest\nHelps with dressing"),
("24 Months",
"Runs\nKicks a ball\n≥50 recognisable words in vocab",
"2-word phrases ('more milk')\nPoints to pictures when asked\nNames 2 body parts",
"Uses one hand while other holds\nSimple 2-piece puzzles\nUses switches/knobs on toys",
"Notices when others are upset\nLooks to parent for cues\nParallel play begins"),
("3 Years",
"Pedals tricycle\nRuns well; hops on preferred foot\nUndresses self",
"3-word sentences\n~75% intelligible to strangers\nAsks 'why?' frequently",
"Counts to 3\nDraws a circle\nKnows name, age, sex",
"Takes turns\nCooperative play begins\nExpresses complex emotions"),
("4 Years",
"Hops on one foot\nCatches bounced ball\nDresses/undresses with minimal help",
"Most speech understood by strangers\n4–5 word sentences\nTells simple stories",
"Names some colours/letters\nDraws person with 3 parts\nUnderstands same/different",
"Dramatic/pretend play\nConcern for others\nImproved sharing\n'Band-Aid phase'"),
("5 Years",
"Hops on one foot (sustained)\nButtons clothes\nComplex sports/motor tasks",
"Tells story with ≥2 events\nMaintains 3+ exchange conversation\nUses simple rhymes",
"Counts to 10; names numbers 1–5\nWrites some letters\nAttends 5–10 min task",
"Follows rules in games\nDoes simple chores\nPerforms for others (sings/dances)"),
("6–12 Years\n(School Age)",
"Writes fluently\nComplex sports (tennis, gymnastics)\nFine motor well-developed",
"Complex ideas; multiple elements\nAbstract thinking emerges (~10 yr)\nExpletives/sex words (typical)",
"Logical > fantasy thinking\nCan concentrate well (by age 9–10)\nMoral: rules = absolute until ~7 yr",
"Same-sex peer groups dominate\nBest friend relationship (~10 yr)\nEmpathy & compassion by 9–10 yr"),
]
# ── Red flags ────────────────────────────────────────────────────
RED_FLAGS = [
("Any age", "Loss of ANY previously acquired skill — refer immediately"),
("2 months", "Not responding to loud sounds; not watching moving things; not smiling"),
("4 months", "Not bringing objects to mouth; not pushing down with legs on hard surface"),
("6 months", "No babbling; cannot sit with support"),
("9 months", "No back-and-forth gestures; no pointing/waving"),
("12 months", "No babbling; no intentional words; no pointing/waving"),
("16 months", "No single words"),
("18 months", "No pointing; no single words"),
("24 months", "No 2-word meaningful phrases (not just imitating)"),
("Any age", "Regression: losing language or social skills previously gained"),
]
# ── Mnemonics / pearls ───────────────────────────────────────────
PEARLS = [
"Social smile by 6 weeks | Object permanence ~8–9 months | Pincer grasp ~9–10 months",
"Words by 12 months | 2-word phrases by 2 years | Sentences by 3 years",
"Walk independently by 18 months (if not, evaluate for motor delay)",
"Speech intelligibility: ~50% at 2 yr → ~75% at 3 yr → ~100% at 4 yr",
"Daytime continence ~2.5 yr | Night-time continence ~4 yr",
"Gender identity fixed: ~24–30 months (largely innate)",
"Stranger anxiety onset: ~9 months | Separation anxiety peak: ~12–18 months",
"AAP formal screening: 9, 18, and 30 months | Autism screen: 18 and 24 months",
]
# ── Screening tools ───────────────────────────────────────────────
SCREENS = [
["Tool", "Age Range", "By Whom", "Purpose"],
["Ages & Stages Q (ASQ)", "4–60 months", "Parent", "Cognitive & motor"],
["PEDS", "0–8 years", "Parent", "Developmental & behavioural"],
["Capute Scales (CLAMS+CAT)", "3–36 months", "Clinician", "Language & problem-solving DQ"],
["M-CHAT-R/F", "16–30 months", "Parent", "Autism spectrum"],
["CSBS DP Infant-Toddler Checklist", "6–24 months", "Parent", "Autism spectrum"],
]
# ── Domain colour map ─────────────────────────────────────────────
DOMAIN_COLORS = {
"Motor": (C_MOTOR, C_LIGHT1),
"Language": (C_LANG, C_LIGHT2),
"Cognitive": (C_COG, C_LIGHT3),
"Social/Emot.": (C_SOC, C_LIGHT4),
}
DOMAINS = list(DOMAIN_COLORS.keys())
# ── Helper: build one age block ───────────────────────────────────
def build_age_block(age, motor, lang, cog, soc):
"""Returns a KeepTogether containing the coloured age table."""
usable_w = PAGE_W - 2 * MARGIN
col_w = usable_w / 5 # age col + 4 domain cols
header_row = [
Paragraph(age, age_label_style),
Paragraph("Motor", domain_style),
Paragraph("Language", domain_style),
Paragraph("Cognitive", domain_style),
Paragraph("Social/Emot.", domain_style),
]
data_row = [
"",
Paragraph(motor, cell_style),
Paragraph(lang, cell_style),
Paragraph(cog, cell_style),
Paragraph(soc, cell_style),
]
t = Table(
[header_row, data_row],
colWidths=[col_w] * 5,
repeatRows=0,
)
# Flatten colour pairs for columns 1-4
col_fg = [fg for (fg, _) in DOMAIN_COLORS.values()]
col_bg = [bg for (_, bg) in DOMAIN_COLORS.values()]
ts = TableStyle([
# Age band – full width merge for header row col 0
("BACKGROUND", (0, 0), (0, 1), C_AGE_BG),
("TEXTCOLOR", (0, 0), (0, 1), C_AGE_FG),
("SPAN", (0, 0), (0, 1)),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("ALIGN", (1, 1), (-1, 1), "LEFT"),
("VALIGN", (1, 1), (-1, 1), "TOP"),
# Domain header colours
("BACKGROUND", (1, 0), (1, 0), col_fg[0]),
("BACKGROUND", (2, 0), (2, 0), col_fg[1]),
("BACKGROUND", (3, 0), (3, 0), col_fg[2]),
("BACKGROUND", (4, 0), (4, 0), col_fg[3]),
# Data row bg colours
("BACKGROUND", (1, 1), (1, 1), col_bg[0]),
("BACKGROUND", (2, 1), (2, 1), col_bg[1]),
("BACKGROUND", (3, 1), (3, 1), col_bg[2]),
("BACKGROUND", (4, 1), (4, 1), col_bg[3]),
("TEXTCOLOR", (1, 0), (-1, 0), colors.white),
("GRID", (0, 0), (-1, -1), 0.5, colors.white),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("ROUNDEDCORNERS", [3]),
])
t.setStyle(ts)
return KeepTogether([t, Spacer(1, 3)])
# ── Helper: red flags table ───────────────────────────────────────
def build_red_flags():
usable_w = PAGE_W - 2 * MARGIN
rows = [[Paragraph("<b>Age</b>", cell_style),
Paragraph("<b>Red Flag</b>", cell_style)]]
for age, flag in RED_FLAGS:
rows.append([
Paragraph(f"<b>{age}</b>", red_cell_style),
Paragraph(flag, red_cell_style),
])
t = Table(rows, colWidths=[30 * mm, usable_w - 30 * mm])
ts = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), C_RED),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("BACKGROUND", (0, 1), (-1, -1), C_RED_LIGHT),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [C_RED_LIGHT, colors.HexColor("#FDFEFE")]),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#E74C3C")),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
])
t.setStyle(ts)
return t
# ── Helper: screening table ───────────────────────────────────────
def build_screening():
usable_w = PAGE_W - 2 * MARGIN
col_ws = [55*mm, 30*mm, 25*mm, usable_w - 55*mm - 30*mm - 25*mm]
rows = []
for i, row in enumerate(SCREENS):
rows.append([Paragraph(cell, cell_style) for cell in row])
t = Table(rows, colWidths=col_ws)
ts = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), C_HEADER),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("ROWBACKGROUNDS", (0, 1), (-1, -1),
[colors.HexColor("#EBF5FB"), colors.HexColor("#FDFEFE")]),
("GRID", (0, 0), (-1, -1), 0.4, C_RULE),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
])
t.setStyle(ts)
return t
# ── Page template with header/footer ─────────────────────────────
class HeaderFooterCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_header_footer(num_pages)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
def draw_header_footer(self, total):
self.saveState()
w, h = A4
# Header bar
self.setFillColor(C_HEADER)
self.rect(0, h - 12 * mm, w, 12 * mm, fill=1, stroke=0)
self.setFillColor(colors.white)
self.setFont("Helvetica-Bold", 11)
self.drawCentredString(w / 2, h - 8 * mm,
"PAEDIATRIC DEVELOPMENTAL MILESTONES – QUICK REFERENCE")
# Footer
self.setFillColor(C_FOOTER)
self.setFont("Helvetica", 7)
page_num = self._saved_page_states.index(
{k: self.__dict__[k] for k in self.__dict__ if k in self._saved_page_states[0]}
) if False else self._pageNumber
self.drawCentredString(
w / 2, 7 * mm,
f"Source: Harriet Lane Handbook 23rd ed. | Kaplan & Sadock's Synopsis of Psychiatry | CDC Milestones 2022 Page {self._pageNumber} / {total}"
)
self.restoreState()
# ── Build document ────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=16 * mm, bottomMargin=14 * mm,
title="Paediatric Developmental Milestones",
author="Orris Medical Reference",
)
story = []
# Title block
story.append(Spacer(1, 2 * mm))
story.append(Paragraph("Paediatric Developmental Milestones", title_style))
story.append(Paragraph(
"Motor · Language · Cognitive · Social/Emotional | Birth to School Age",
subtitle_style))
story.append(HRFlowable(width="100%", thickness=1.5, color=C_RULE, spaceAfter=5))
# Legend row
legend_data = [[
Paragraph("<b><font color='#1A5276'>■ Motor</font></b>", cell_style),
Paragraph("<b><font color='#1E8449'>■ Language</font></b>", cell_style),
Paragraph("<b><font color='#7D3C98'>■ Cognitive</font></b>", cell_style),
Paragraph("<b><font color='#CA6F1E'>■ Social/Emotional</font></b>", cell_style),
Paragraph("<b><font color='#C0392B'>■ Red Flags</font></b>", cell_style),
]]
legend_t = Table(legend_data, colWidths=[(PAGE_W - 2*MARGIN)/5]*5)
legend_t.setStyle(TableStyle([
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
]))
story.append(legend_t)
story.append(HRFlowable(width="100%", thickness=0.5, color=C_RULE, spaceAfter=5))
# Milestone blocks
for entry in MILESTONES:
story.append(build_age_block(*entry))
story.append(Spacer(1, 4 * mm))
story.append(HRFlowable(width="100%", thickness=1.5, color=C_RED, spaceAfter=3))
# Red flags
story.append(Paragraph("⚠ Developmental Red Flags", red_head_style))
story.append(build_red_flags())
story.append(Spacer(1, 5 * mm))
# Mnemonics / pearls
story.append(HRFlowable(width="100%", thickness=1, color=C_RULE, spaceAfter=3))
story.append(Paragraph("Clinical Pearls & Mnemonics", ParagraphStyle(
"PearlHead", fontSize=10, textColor=C_HEADER,
fontName="Helvetica-Bold", spaceAfter=3)))
for pearl in PEARLS:
story.append(Paragraph(f"• {pearl}", tip_style))
story.append(Spacer(1, 5 * mm))
# Screening tools
story.append(HRFlowable(width="100%", thickness=1, color=C_RULE, spaceAfter=3))
story.append(Paragraph("Developmental Screening Tools (AAP-Recommended)", ParagraphStyle(
"ScreenHead", fontSize=10, textColor=C_HEADER,
fontName="Helvetica-Bold", spaceAfter=3)))
story.append(build_screening())
story.append(Spacer(1, 4*mm))
story.append(Paragraph(
"AAP guideline: formal developmental screening at 9, 18, and 30 months (or 24 months if 30-month visit not possible). "
"Autism-specific screening at 18 and 24 months.",
tip_style))
# Build
doc.build(story, canvasmaker=HeaderFooterCanvas)
print("PDF generated:", OUTPUT)