~/grbs-sheet/generate_grbs_sheet.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, HRFlowable
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import KeepTogether
OUTPUT = "/home/daytona/workspace/grbs-sheet/GRBS_4Point_Tracking_Sheet.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=1.5*cm,
rightMargin=1.5*cm,
topMargin=1.5*cm,
bottomMargin=1.5*cm,
)
W, H = A4
styles = getSampleStyleSheet()
# ── Custom styles ──────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1a3a5c")
TEAL = colors.HexColor("#1a7a6e")
AMBER = colors.HexColor("#e07b00")
LGRAY = colors.HexColor("#f0f4f8")
DGRAY = colors.HexColor("#4a5568")
WHITE = colors.white
RED = colors.HexColor("#c0392b")
GREEN = colors.HexColor("#27ae60")
title_style = ParagraphStyle(
"TitleStyle", parent=styles["Normal"],
fontSize=15, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER, spaceAfter=2
)
subtitle_style = ParagraphStyle(
"SubStyle", parent=styles["Normal"],
fontSize=9, fontName="Helvetica",
textColor=WHITE, alignment=TA_CENTER
)
section_style = ParagraphStyle(
"SectionStyle", parent=styles["Normal"],
fontSize=8.5, fontName="Helvetica-Bold",
textColor=NAVY, alignment=TA_LEFT
)
small_style = ParagraphStyle(
"SmallStyle", parent=styles["Normal"],
fontSize=7.5, fontName="Helvetica",
textColor=DGRAY, alignment=TA_LEFT, leading=11
)
note_style = ParagraphStyle(
"NoteStyle", parent=styles["Normal"],
fontSize=7, fontName="Helvetica-Oblique",
textColor=DGRAY, alignment=TA_LEFT, leading=10
)
center_small = ParagraphStyle(
"CenterSmall", parent=styles["Normal"],
fontSize=7.5, fontName="Helvetica",
textColor=DGRAY, alignment=TA_CENTER
)
label_bold = ParagraphStyle(
"LabelBold", parent=styles["Normal"],
fontSize=8, fontName="Helvetica-Bold",
textColor=NAVY, alignment=TA_CENTER
)
white_bold = ParagraphStyle(
"WhiteBold", parent=styles["Normal"],
fontSize=8, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER
)
tiny = ParagraphStyle(
"Tiny", parent=styles["Normal"],
fontSize=6.5, fontName="Helvetica",
textColor=DGRAY, alignment=TA_CENTER, leading=9
)
story = []
# ══════════════════════════════════════════════════════════════════════════
# HEADER BANNER
# ══════════════════════════════════════════════════════════════════════════
header_data = [[
Paragraph("4-POINT GRBS MONITORING CHART", title_style),
Paragraph("Glucometer Random Blood Sugar — Insulin Tracking Sheet", subtitle_style)
]]
# Stack title and subtitle in one cell spanning full width
header_data = [[
Paragraph(
"<b>4-POINT GRBS MONITORING CHART</b><br/>"
"<font size=9>Glucometer Random Blood Sugar & Insulin Dose Tracking — Ward Use</font>",
ParagraphStyle("H", parent=styles["Normal"], fontSize=14,
fontName="Helvetica-Bold", textColor=WHITE,
alignment=TA_CENTER, leading=20)
)
]]
header_table = Table(header_data, colWidths=[doc.width])
header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [4]),
]))
story.append(header_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# PATIENT INFO ROW
# ══════════════════════════════════════════════════════════════════════════
def field(label, width=3*cm):
return Paragraph(f"<b>{label}:</b> {'_' * 22}", small_style)
info_data = [[
field("Patient Name"),
field("Age/Sex"),
field("Ward/Bed No."),
field("IP No."),
]]
info_table = Table(info_data, colWidths=[doc.width/4]*4)
info_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LGRAY),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.5, NAVY),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#c0ccd8")),
]))
story.append(info_table)
info_data2 = [[
field("Diagnosis"),
field("Consultant"),
field("Insulin Regimen"),
Paragraph("<b>Sheet Period:</b> ______ to ______", small_style),
]]
info_table2 = Table(info_data2, colWidths=[doc.width/4]*4)
info_table2.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), WHITE),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.5, NAVY),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#c0ccd8")),
("LINEABOVE", (0,0), (-1,0), 0, WHITE),
]))
story.append(info_table2)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# QUICK REFERENCE BOX
# ══════════════════════════════════════════════════════════════════════════
ref_header = [[Paragraph("QUICK REFERENCE: Which insulin does each reading reflect?", white_bold)]]
ref_header_table = Table(ref_header, colWidths=[doc.width])
ref_header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), TEAL),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
story.append(ref_header_table)
ref_data = [
[
Paragraph("<b>Reading</b>", label_bold),
Paragraph("<b>Time</b>", label_bold),
Paragraph("<b>Reflects this insulin</b>", label_bold),
Paragraph("<b>Normal target</b>", label_bold),
Paragraph("<b>If HIGH → action</b>", label_bold),
Paragraph("<b>If LOW (<80) → action</b>", label_bold),
],
[
Paragraph("Fasting", center_small),
Paragraph("~7 AM\n(before breakfast)", tiny),
Paragraph("Night NPH\n(given ~10 PM)", tiny),
Paragraph("80–110 mg/dL", tiny),
Paragraph("↑ Night NPH by 2u", tiny),
Paragraph("↓ Night NPH by 2u", tiny),
],
[
Paragraph("Post-Breakfast", center_small),
Paragraph("~9:30 AM\n(2 hrs after BF)", tiny),
Paragraph("Morning HA\n(given ~7:30 AM)", tiny),
Paragraph("<140 mg/dL", tiny),
Paragraph("↑ Morning HA by 2u", tiny),
Paragraph("↓ Morning HA by 2u", tiny),
],
[
Paragraph("Pre-Dinner / Post-Lunch", center_small),
Paragraph("~7 PM\n(before dinner)", tiny),
Paragraph("Morning NPH\n(given ~7:30 AM)", tiny),
Paragraph("80–140 mg/dL", tiny),
Paragraph("↑ Morning NPH by 2u", tiny),
Paragraph("↓ Morning NPH by 2u", tiny),
],
[
Paragraph("Post-Dinner / Bedtime", center_small),
Paragraph("~9:30 PM\n(2 hrs after dinner)", tiny),
Paragraph("Evening HA\n(given ~7:30 PM)", tiny),
Paragraph("<140 mg/dL", tiny),
Paragraph("↑ Evening HA by 2u", tiny),
Paragraph("↓ Evening HA by 2u", tiny),
],
]
cw = [2.8*cm, 2.4*cm, 3.2*cm, 2.4*cm, 3.5*cm, 3.5*cm]
ref_table = Table(ref_data, colWidths=cw)
ref_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#dceef5")),
("BACKGROUND", (0,1), (-1,1), WHITE),
("BACKGROUND", (0,2), (-1,2), LGRAY),
("BACKGROUND", (0,3), (-1,3), WHITE),
("BACKGROUND", (0,4), (-1,4), LGRAY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("BOX", (0,0), (-1,-1), 0.7, TEAL),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#b0c8d8")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,0), (-1,-1), "CENTER"),
]))
story.append(ref_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# DAILY TRACKING TABLE (7 days)
# ══════════════════════════════════════════════════════════════════════════
track_header = [[Paragraph("DAILY BLOOD GLUCOSE LOG (7-Day Record)", white_bold)]]
track_header_table = Table(track_header, colWidths=[doc.width])
track_header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
story.append(track_header_table)
# Sub-header
sub_data = [[
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("FASTING\n~7 AM", tiny),
Paragraph("POST-BREAKFAST\n~9:30 AM", tiny),
Paragraph("PRE-DINNER\n~7 PM", tiny),
Paragraph("POST-DINNER\n~9:30 PM", tiny),
Paragraph("3 AM*\n(if needed)", tiny),
Paragraph("INSULIN GIVEN TODAY", label_bold),
Paragraph("NOTES /\nHypo episodes", tiny),
]]
span1_data = [[
Paragraph("<b>Date</b>", label_bold),
Paragraph("<b>Day</b>", label_bold),
Paragraph("<b>FASTING</b><br/><font size=6>~7 AM</font>", label_bold),
Paragraph("<b>POST-BF</b><br/><font size=6>~9:30 AM</font>", label_bold),
Paragraph("<b>PRE-DINNER</b><br/><font size=6>~7 PM</font>", label_bold),
Paragraph("<b>POST-DINNER</b><br/><font size=6>~9:30 PM</font>", label_bold),
Paragraph("<b>3 AM</b><br/><font size=6>if ordered</font>", label_bold),
Paragraph("<b>HA (units)</b><br/><font size=6>B – L – D</font>", label_bold),
Paragraph("<b>NPH (units)</b><br/><font size=6>AM – PM</font>", label_bold),
Paragraph("<b>Notes / Hypo</b>", label_bold),
]]
tcw = [1.6*cm, 1.0*cm, 1.8*cm, 1.8*cm, 1.9*cm, 2.0*cm, 1.5*cm, 2.0*cm, 1.8*cm, 2.4*cm]
track_rows = [span1_data[0]]
for i in range(1, 8):
row = [
Paragraph(f"Day {i}", center_small),
Paragraph("", center_small), # day name
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
Paragraph("", center_small),
]
track_rows.append(row)
track_table = Table(track_rows, colWidths=tcw, rowHeights=[1.0*cm] + [1.1*cm]*7)
ts = TableStyle([
# Header row
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#1a3a5c")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 7.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 3),
("BOX", (0,0), (-1,-1), 0.8, NAVY),
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#c0ccd8")),
# Alternate row shading
("BACKGROUND", (0,2), (-1,2), LGRAY),
("BACKGROUND", (0,4), (-1,4), LGRAY),
("BACKGROUND", (0,6), (-1,6), LGRAY),
# Highlight glucose columns
("BACKGROUND", (2,0), (2,0), colors.HexColor("#2e6da4")),
("BACKGROUND", (3,0), (3,0), colors.HexColor("#2e6da4")),
("BACKGROUND", (4,0), (4,0), colors.HexColor("#2e6da4")),
("BACKGROUND", (5,0), (5,0), colors.HexColor("#2e6da4")),
("BACKGROUND", (6,0), (6,0), colors.HexColor("#2e6da4")),
# Insulin columns
("BACKGROUND", (7,0), (8,0), colors.HexColor("#1a7a6e")),
])
track_table.setStyle(ts)
story.append(track_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# TITRATION DECISION GUIDE
# ══════════════════════════════════════════════════════════════════════════
tit_header = [[Paragraph("TITRATION DECISION GUIDE (use daily after reviewing glucose log)", white_bold)]]
tit_header_table = Table(tit_header, colWidths=[doc.width])
tit_header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), AMBER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
story.append(tit_header_table)
tit_data = [
[
Paragraph("<b>Fasting glucose</b>", label_bold),
Paragraph("<b>Action on Night NPH</b>", label_bold),
Paragraph("<b>Post-meal glucose</b>", label_bold),
Paragraph("<b>Action on that meal's HA</b>", label_bold),
],
[
Paragraph("< 80 mg/dL", center_small),
Paragraph("↓ Reduce night NPH by 2–3 units", center_small),
Paragraph("< 140 mg/dL (target)", center_small),
Paragraph("✓ No change", center_small),
],
[
Paragraph("80–110 mg/dL (ideal)", center_small),
Paragraph("✓ No change", center_small),
Paragraph("140–180 mg/dL (acceptable)", center_small),
Paragraph("Consider ↑ by 1 unit if consistent", center_small),
],
[
Paragraph("110–150 mg/dL", center_small),
Paragraph("↑ Increase night NPH by 1–2 units", center_small),
Paragraph("> 180 mg/dL (high)", center_small),
Paragraph("↑ Increase that HA by 2 units", center_small),
],
[
Paragraph("> 150 mg/dL", center_small),
Paragraph("↑ Increase night NPH by 2–3 units", center_small),
Paragraph("< 70 mg/dL (hypoglycemia!)", center_small),
Paragraph("↓ Decrease that HA by 2 units immediately", center_small),
],
]
tit_cw = [doc.width*0.22, doc.width*0.28, doc.width*0.25, doc.width*0.25]
tit_table = Table(tit_data, colWidths=tit_cw)
tit_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#fdebd0")),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("BACKGROUND", (0,2), (-1,2), colors.HexColor("#fef9f0")),
("BACKGROUND", (0,4), (-1,4), colors.HexColor("#fdeee0")),
("TEXTCOLOR", (0,3), (1,3), colors.HexColor("#c0392b")),
("TEXTCOLOR", (2,4), (3,4), colors.HexColor("#c0392b")),
("FONTNAME", (0,3), (1,3), "Helvetica-Bold"),
("FONTNAME", (2,4), (3,4), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("BOX", (0,0), (-1,-1), 0.8, AMBER),
("INNERGRID", (0,0), (-1,-1), 0.4, colors.HexColor("#f0d8b0")),
("LINEAFTER", (1,0), (1,-1), 1.0, AMBER),
]))
story.append(tit_table)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════
# SOMOGYI WARNING + NPO RULES
# ══════════════════════════════════════════════════════════════════════════
warn_data = [[
Paragraph(
"<b>⚠ SOMOGYI EFFECT (Rebound hyperglycemia):</b> "
"If fasting glucose is HIGH but 3 AM glucose was LOW — do NOT increase night NPH. "
"The body rebounded from nocturnal hypoglycemia. REDUCE night NPH by 2 units.",
ParagraphStyle("W", parent=styles["Normal"], fontSize=7.5, fontName="Helvetica",
textColor=colors.HexColor("#7b1a1a"), alignment=TA_LEFT, leading=11)
),
Paragraph(
"<b>NPO / No oral intake:</b> "
"HOLD all HA (no food = no prandial dose needed). "
"Give NPH at HALF the usual dose — do NOT omit entirely in T1DM.",
ParagraphStyle("W2", parent=styles["Normal"], fontSize=7.5, fontName="Helvetica",
textColor=colors.HexColor("#1a3a5c"), alignment=TA_LEFT, leading=11)
),
]]
warn_table = Table(warn_data, colWidths=[doc.width*0.52, doc.width*0.48])
warn_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), colors.HexColor("#fde8e8")),
("BACKGROUND", (1,0), (1,0), colors.HexColor("#e8f0f8")),
("BOX", (0,0), (0,0), 0.6, colors.HexColor("#c0392b")),
("BOX", (1,0), (1,0), 0.6, NAVY),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(warn_table)
story.append(Spacer(1, 3*mm))
# ══════════════════════════════════════════════════════════════════════════
# FOOTER
# ══════════════════════════════════════════════════════════════════════════
footer_data = [[
Paragraph(
"* Check 3 AM glucose if: fasting glucose is unexpectedly high despite adequate night NPH, "
"or patient reports night sweats / restless sleep (symptoms of nocturnal hypoglycemia). "
"Titrate in steps of 1–2 units. Wait 2–3 days between adjustments unless glucose is <70 or >300.",
note_style
)
]]
footer_table = Table(footer_data, colWidths=[doc.width])
footer_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#eef2f7")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("BOX", (0,0), (-1,-1), 0.5, colors.HexColor("#c0ccd8")),
]))
story.append(footer_table)
# ══════════════════════════════════════════════════════════════════════════
# BUILD
# ══════════════════════════════════════════════════════════════════════════
doc.build(story)
print(f"PDF generated: {OUTPUT}")