~/clinical-lab-guide/generate_guide.py
"""
Clinical Laboratory Interpretation Guide with Flowcharts
Generates a comprehensive PDF using ReportLab + Matplotlib
"""
import os
import io
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
from reportlab.lib.pagesizes import A4
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, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, Image as RLImage, HRFlowable, KeepTogether
)
from reportlab.graphics.shapes import Drawing, Rect, String, Line, Polygon
from reportlab.graphics import renderPDF
OUTPUT_PATH = "/home/daytona/workspace/clinical-lab-guide/Clinical_Lab_Interpretation_Guide.pdf"
# ── Color palette ──────────────────────────────────────────────────────────────
NAVY = colors.HexColor('#1A2B5C')
TEAL = colors.HexColor('#006D77')
GREEN = colors.HexColor('#2D6A4F')
RED = colors.HexColor('#C62828')
ORANGE = colors.HexColor('#E65100')
GOLD = colors.HexColor('#F9A825')
LAVENDER = colors.HexColor('#E8EAF6')
LIGHT_BG = colors.HexColor('#F0F4F8')
MINT = colors.HexColor('#E0F2F1')
WHITE = colors.white
GRAY = colors.HexColor('#607D8B')
DARKGRAY = colors.HexColor('#37474F')
# ── Styles ─────────────────────────────────────────────────────────────────────
def build_styles():
styles = getSampleStyleSheet()
custom = {}
custom['CoverTitle'] = ParagraphStyle('CoverTitle',
fontName='Helvetica-Bold', fontSize=28, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=8, leading=34)
custom['CoverSub'] = ParagraphStyle('CoverSub',
fontName='Helvetica', fontSize=13, textColor=colors.HexColor('#BBDEFB'),
alignment=TA_CENTER, spaceAfter=4, leading=18)
custom['SectionHeader'] = ParagraphStyle('SectionHeader',
fontName='Helvetica-Bold', fontSize=15, textColor=WHITE,
alignment=TA_LEFT, spaceAfter=6, spaceBefore=4, leading=20,
leftIndent=10)
custom['SubHeader'] = ParagraphStyle('SubHeader',
fontName='Helvetica-Bold', fontSize=11, textColor=NAVY,
alignment=TA_LEFT, spaceAfter=4, spaceBefore=6, leading=14)
custom['Body'] = ParagraphStyle('Body',
fontName='Helvetica', fontSize=9, textColor=DARKGRAY,
alignment=TA_JUSTIFY, spaceAfter=4, leading=13, leftIndent=4)
custom['Bullet'] = ParagraphStyle('Bullet',
fontName='Helvetica', fontSize=9, textColor=DARKGRAY,
alignment=TA_LEFT, spaceAfter=2, leading=13, leftIndent=14,
firstLineIndent=-10)
custom['Caption'] = ParagraphStyle('Caption',
fontName='Helvetica-Oblique', fontSize=8, textColor=GRAY,
alignment=TA_CENTER, spaceAfter=2, leading=11)
custom['TableHeader'] = ParagraphStyle('TableHeader',
fontName='Helvetica-Bold', fontSize=8, textColor=WHITE,
alignment=TA_CENTER, leading=10)
custom['TableCell'] = ParagraphStyle('TableCell',
fontName='Helvetica', fontSize=8, textColor=DARKGRAY,
alignment=TA_LEFT, leading=11)
custom['NormalRange'] = ParagraphStyle('NormalRange',
fontName='Helvetica-Bold', fontSize=8, textColor=GREEN,
alignment=TA_CENTER, leading=11)
custom['Warning'] = ParagraphStyle('Warning',
fontName='Helvetica-BoldOblique', fontSize=8, textColor=RED,
alignment=TA_LEFT, leading=11, leftIndent=6)
return custom
# ── Flowchart builder (matplotlib) ─────────────────────────────────────────────
def make_flowchart(title, nodes, arrows, figsize=(8, 9), colors_map=None):
"""
nodes: list of dicts {id, label, x, y, shape, color, fontsize}
arrows: list of (from_id, to_id, label)
Returns PNG bytes
"""
fig, ax = plt.subplots(figsize=figsize)
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis('off')
# Title
ax.text(5, 9.6, title, ha='center', va='center', fontsize=13,
fontweight='bold', color='#1A2B5C',
bbox=dict(boxstyle='round,pad=0.4', facecolor='#E8EAF6', edgecolor='#1A2B5C', lw=1.5))
node_centers = {}
for n in nodes:
x, y = n['x'], n['y']
w = n.get('w', 3.2)
h = n.get('h', 0.65)
color = n.get('color', '#006D77')
shape = n.get('shape', 'round')
fc = n.get('fc', '#E0F2F1')
fs = n.get('fontsize', 8)
node_centers[n['id']] = (x, y)
if shape == 'diamond':
dx, dy = w/2, h*1.1
diamond = plt.Polygon(
[[x, y+dy], [x+dx*0.85, y], [x, y-dy], [x-dx*0.85, y]],
closed=True, facecolor=fc, edgecolor=color, lw=1.5)
ax.add_patch(diamond)
ax.text(x, y, n['label'], ha='center', va='center',
fontsize=fs, color=color, fontweight='bold',
wrap=True, multialignment='center')
elif shape == 'oval':
ellipse = mpatches.Ellipse((x, y), w, h*1.3,
facecolor=fc, edgecolor=color, lw=1.5)
ax.add_patch(ellipse)
ax.text(x, y, n['label'], ha='center', va='center',
fontsize=fs, color=color, fontweight='bold')
else: # rectangle / rounded
box = FancyBboxPatch((x - w/2, y - h/2), w, h,
boxstyle='round,pad=0.08',
facecolor=fc, edgecolor=color, lw=1.5)
ax.add_patch(box)
ax.text(x, y, n['label'], ha='center', va='center',
fontsize=fs, color=color, fontweight='bold',
multialignment='center')
for (src, dst, lbl) in arrows:
x1, y1 = node_centers[src]
x2, y2 = node_centers[dst]
ax.annotate('', xy=(x2, y2+0.35), xytext=(x1, y1-0.35),
arrowprops=dict(arrowstyle='->', color='#607D8B',
lw=1.4, connectionstyle='arc3,rad=0.0'))
if lbl:
mx = (x1+x2)/2 + 0.15
my = (y1+y2)/2
ax.text(mx, my, lbl, fontsize=7, color='#E65100',
fontstyle='italic', ha='left', va='center')
plt.tight_layout(pad=0.3)
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
plt.close(fig)
buf.seek(0)
return buf
# ── Individual flowcharts ───────────────────────────────────────────────────────
def fc_sodium():
nodes = [
{'id':'start','label':'Serum Sodium Result','x':5,'y':9.0,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'check','label':'Is Na⁺ < 135 or > 145?','x':5,'y':7.8,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.6,'h':0.8,'fontsize':8},
{'id':'normal','label':'NORMAL (135-145 mmol/L)\nNo action needed','x':8.2,'y':7.8,'shape':'round','color':'#2D6A4F','fc':'#C8E6C9','w':3.0,'h':0.7,'fontsize':7},
{'id':'hypo','label':'HYPONATREMIA\n< 135 mmol/L','x':2,'y':6.4,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.8,'h':0.7,'fontsize':8},
{'id':'hyper','label':'HYPERNATREMIA\n> 145 mmol/L','x':7.5,'y':6.4,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.8,'h':0.7,'fontsize':8},
{'id':'osm','label':'Check serum osmolality\n& urine osmolality','x':2,'y':5.1,'shape':'round','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.7,'fontsize':7},
{'id':'hyper_cause','label':'Water loss > Na loss?\nOr Na gain?','x':7.5,'y':5.1,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.75,'fontsize':7},
{'id':'dilutional','label':'Dilutional:\nSIADH / Heart failure\nCirrhosis / Nephrotic','x':0.8,'y':3.7,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.5,'h':0.85,'fontsize':7},
{'id':'depletional','label':'Depletional:\nDiuretics / Vomiting\nAdrenal insufficiency','x':3.2,'y':3.7,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.5,'h':0.85,'fontsize':7},
{'id':'water_loss','label':'Water loss:\nDiabetes insipidus\nInsensible losses','x':6.5,'y':3.7,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.5,'h':0.85,'fontsize':7},
{'id':'na_gain','label':'Na gain:\nConn\'s syndrome\nHypertonic saline','x':9.0,'y':3.7,'shape':'round','color':'#F57F17','fc':'#FFF9C4','w':2.4,'h':0.85,'fontsize':7},
{'id':'rx_hypo','label':'Rx: Fluid restrict / treat cause\nSevere: hypertonic saline\n(correct slowly: 0.5 mEq/hr)','x':2,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.2,'h':0.85,'fontsize':7},
{'id':'rx_hyper','label':'Rx: Free water replacement\nDI: Desmopressin\n(correct slowly: 0.5 mEq/hr)','x':7.5,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.2,'h':0.85,'fontsize':7},
]
arrows = [
('start','check',''),
('check','normal','Normal'),
('check','hypo','< 135'),
('check','hyper','> 145'),
('hypo','osm',''),
('hyper','hyper_cause',''),
('osm','dilutional','Low osm'),
('osm','depletional','Low osm + UNa<20'),
('hyper_cause','water_loss','Water loss'),
('hyper_cause','na_gain','Na gain'),
('dilutional','rx_hypo',''),
('depletional','rx_hypo',''),
('water_loss','rx_hyper',''),
('na_gain','rx_hyper',''),
]
return make_flowchart('Sodium (Na⁺) Interpretation Flowchart', nodes, arrows, figsize=(10,10))
def fc_potassium():
nodes = [
{'id':'start','label':'Serum Potassium Result','x':5,'y':9.2,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'check','label':'K⁺ < 3.5 or > 5.3?','x':5,'y':8.0,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.2,'h':0.8},
{'id':'normal','label':'NORMAL (3.5-5.3 mmol/L)','x':8.3,'y':8.0,'shape':'round','color':'#2D6A4F','fc':'#C8E6C9','w':2.8,'h':0.65,'fontsize':7},
{'id':'hypo','label':'HYPOKALEMIA < 3.5','x':2.2,'y':6.7,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.8,'h':0.65},
{'id':'hyper','label':'HYPERKALEMIA > 5.3','x':7.5,'y':6.7,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.8,'h':0.65},
{'id':'hypo_cause','label':'GI loss or renal loss?','x':2.2,'y':5.4,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':7},
{'id':'hyper_cause','label':'Real or spurious?','x':7.5,'y':5.4,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':7},
{'id':'gi_loss','label':'GI loss:\nVomiting / Diarrhea\nNG suction / Laxatives','x':1.0,'y':4.0,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.4,'h':0.9,'fontsize':7},
{'id':'renal_loss','label':'Renal loss:\nDiuretics / Aldosteronism\nMg deficiency / Alkalosis','x':3.5,'y':4.0,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.6,'h':0.9,'fontsize':7},
{'id':'spurious','label':'Spurious:\nHemolysis / High WBC\nDelayed sample processing','x':6.2,'y':4.0,'shape':'round','color':'#F57F17','fc':'#FFF9C4','w':2.6,'h':0.9,'fontsize':7},
{'id':'real_hyper','label':'Real Hyperkalemia:\nRenal failure / Addison\'s\nACEi/ARBs / Acidosis\nRhabdomyolysis','x':8.8,'y':4.0,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.4,'h':1.0,'fontsize':7},
{'id':'rx_hypo','label':'Rx: Oral/IV KCl replacement\nTreat cause; monitor ECG\nCorrect Mg deficiency','x':2.2,'y':2.4,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.0,'h':0.9,'fontsize':7},
{'id':'rx_hyper','label':'Rx: Calcium gluconate (cardioprotect)\nInsulin + glucose (shift K⁺ in)\nSalbutamol / Kayexalate\nDialysis if severe','x':7.5,'y':2.4,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.2,'h':1.0,'fontsize':7},
{'id':'ecg','label':'⚠ URGENT ECG:\nTall T-waves → Sine wave → VF','x':7.5,'y':0.9,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':3.2,'h':0.75,'fontsize':7},
]
arrows = [
('start','check',''),
('check','normal','Normal'),
('check','hypo','< 3.5'),
('check','hyper','> 5.3'),
('hypo','hypo_cause',''),
('hyper','hyper_cause',''),
('hypo_cause','gi_loss','GI'),
('hypo_cause','renal_loss','Renal'),
('hyper_cause','spurious','Spurious?'),
('hyper_cause','real_hyper','Real'),
('gi_loss','rx_hypo',''),
('renal_loss','rx_hypo',''),
('real_hyper','rx_hyper',''),
('rx_hyper','ecg',''),
]
return make_flowchart('Potassium (K⁺) Interpretation Flowchart', nodes, arrows, figsize=(10,10))
def fc_glucose():
nodes = [
{'id':'start','label':'Fasting Blood Glucose','x':5,'y':9.2,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'check','label':'Glucose level?','x':5,'y':8.0,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.0,'h':0.8},
{'id':'normal','label':'NORMAL: 70-100 mg/dL','x':5,'y':6.5,'shape':'round','color':'#2D6A4F','fc':'#C8E6C9','w':2.8,'h':0.65,'fontsize':7},
{'id':'low','label':'HYPOGLYCEMIA < 70','x':1.5,'y':6.5,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.6,'h':0.65},
{'id':'prediab','label':'PRE-DIABETES 100-125','x':5,'y':5.2,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.8,'h':0.65,'fontsize':7},
{'id':'dm','label':'DIABETES ≥ 126 mg/dL','x':8.3,'y':6.5,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.8,'h':0.65},
{'id':'low_cause','label':'Cause of hypoglycemia?','x':1.5,'y':5.2,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':7},
{'id':'dm_type','label':'Type 1 or Type 2 DM?','x':8.3,'y':5.2,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':7},
{'id':'low_drug','label':'Drug-induced:\nInsulin / Sulfonylurea\nAlcohol','x':0.4,'y':3.8,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.2,'h':0.9,'fontsize':7},
{'id':'low_other','label':'Non-drug:\nInsulinoma / Addison\'s\nLiver failure / Fasting','x':2.6,'y':3.8,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.4,'h':0.9,'fontsize':7},
{'id':'t1','label':'Type 1 DM:\nAutoimmune / Anti-GAD\nInsulin required','x':7.2,'y':3.8,'shape':'round','color':'#006D77','fc':'#E0F2F1','w':2.4,'h':0.9,'fontsize':7},
{'id':'t2','label':'Type 2 DM:\nInsulin resistance\nOral agents / Lifestyle','x':9.4,'y':3.8,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.2,'h':0.9,'fontsize':7},
{'id':'rx_low','label':'Rx: 15g fast carbs (Rule of 15)\nSevere: IV Dextrose 50% or Glucagon\nFind & treat underlying cause','x':1.5,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.0,'h':0.9,'fontsize':7},
{'id':'hba1c','label':'Check HbA1c (3 month glucose average)\nTarget: < 7.0% (most patients)\nCheck for complications','x':5,'y':3.8,'shape':'round','color':'#1A2B5C','fc':'#FFF9C4','w':2.8,'h':0.9,'fontsize':7},
{'id':'rx_dm','label':'Rx: Lifestyle + Metformin (T2)\nInsulin (T1 and advanced T2)\nMonitor HbA1c, renal function, lipids','x':8.3,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.0,'h':0.9,'fontsize':7},
]
arrows = [
('start','check',''),
('check','low','< 70'),
('check','normal','70-100'),
('check','dm','≥ 126'),
('normal','prediab','Recheck\n100-125'),
('low','low_cause',''),
('dm','dm_type',''),
('low_cause','low_drug','Drug'),
('low_cause','low_other','Other'),
('dm_type','t1','T1'),
('dm_type','t2','T2'),
('low_drug','rx_low',''),
('low_other','rx_low',''),
('prediab','hba1c',''),
('t1','rx_dm',''),
('t2','rx_dm',''),
]
return make_flowchart('Blood Glucose Interpretation Flowchart', nodes, arrows, figsize=(10,11))
def fc_liver():
nodes = [
{'id':'start','label':'Abnormal Liver Enzymes','x':5,'y':9.3,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'pattern','label':'Hepatocellular or Cholestatic?','x':5,'y':8.1,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.8,'h':0.85},
{'id':'hep','label':'HEPATOCELLULAR\nALT/AST elevated\n(ALT > AST typical)','x':2.2,'y':6.7,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.8,'h':0.85,'fontsize':7},
{'id':'chol','label':'CHOLESTATIC\nALP + GGT elevated\nBilirubin raised','x':7.8,'y':6.7,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.8,'h':0.85,'fontsize':7},
{'id':'ratio','label':'AST:ALT ratio?','x':2.2,'y':5.3,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':8},
{'id':'chol_cause','label':'Intrahepatic or\nExtrahepatic?','x':7.8,'y':5.3,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.8,'h':0.8,'fontsize':7},
{'id':'alcoholic','label':'AST:ALT > 2:1\nAlcoholic hepatitis\n+ GGT elevated','x':0.8,'y':3.9,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.4,'h':0.9,'fontsize':7},
{'id':'viral','label':'AST:ALT < 1:1\nViral hepatitis\nNAFLD / Drug-induced','x':3.5,'y':3.9,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.6,'h':0.9,'fontsize':7},
{'id':'intra','label':'Intrahepatic:\nPBC / PSC / Drugs\nInfiltrative disease','x':6.5,'y':3.9,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.4,'h':0.9,'fontsize':7},
{'id':'extra','label':'Extrahepatic:\nGallstones / Stricture\nPancreatic Ca / CBD','x':9.0,'y':3.9,'shape':'round','color':'#F57F17','fc':'#FFF9C4','w':2.4,'h':0.9,'fontsize':7},
{'id':'severity','label':'Check severity:\nBilirubin / PT / Albumin\n(Child-Pugh / MELD)','x':2.2,'y':2.4,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.0,'h':0.9,'fontsize':7},
{'id':'imaging','label':'Imaging: Ultrasound\nMRCP / ERCP\nTumour markers (CA19-9)','x':7.8,'y':2.4,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':3.0,'h':0.9,'fontsize':7},
{'id':'biopsy','label':'Consider: Liver biopsy\nViral serology / Autoimmune\nHaemochromatosis screen','x':2.2,'y':0.9,'shape':'round','color':'#37474F','fc':'#ECEFF1','w':3.2,'h':0.85,'fontsize':7},
]
arrows = [
('start','pattern',''),
('pattern','hep','ALT/AST ↑↑'),
('pattern','chol','ALP ↑↑'),
('hep','ratio',''),
('chol','chol_cause',''),
('ratio','alcoholic','> 2:1'),
('ratio','viral','< 1:1'),
('chol_cause','intra','Intra'),
('chol_cause','extra','Extra'),
('alcoholic','severity',''),
('viral','severity',''),
('intra','imaging',''),
('extra','imaging',''),
('severity','biopsy',''),
]
return make_flowchart('Liver Function Tests Interpretation Flowchart', nodes, arrows, figsize=(10,11))
def fc_renal():
nodes = [
{'id':'start','label':'Elevated Creatinine / BUN','x':5,'y':9.3,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'ratio','label':'BUN:Creatinine Ratio?','x':5,'y':8.1,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.3,'h':0.8},
{'id':'pre','label':'PRE-RENAL\nBUN:Cr > 20:1\nFractional Na < 1%','x':1.5,'y':6.7,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.8,'h':0.85,'fontsize':7},
{'id':'renal','label':'INTRINSIC RENAL\nBUN:Cr 10-20:1\nFractional Na > 2%','x':5,'y':6.7,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.8,'h':0.85,'fontsize':7},
{'id':'post','label':'POST-RENAL\nBUN:Cr > 20:1\nHydronephrosis on USS','x':8.5,'y':6.7,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.8,'h':0.85,'fontsize':7},
{'id':'pre_cause','label':'Cause?','x':1.5,'y':5.3,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.0,'h':0.75,'fontsize':8},
{'id':'renal_cause','label':'Glomerular or Tubular\nor Vascular?','x':5,'y':5.3,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.0,'h':0.8,'fontsize':7},
{'id':'post_cause','label':'Obstruction level?','x':8.5,'y':5.3,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.6,'h':0.75,'fontsize':7},
{'id':'vol_dep','label':'Volume depletion:\nDehydration\nHeart failure\nHemorrhage','x':0.4,'y':3.8,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.2,'h':1.0,'fontsize':7},
{'id':'glom','label':'Glomerular:\nGlomerulonephritis\nNephrotic syndrome\nLupus nephritis','x':3.5,'y':3.8,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.4,'h':1.0,'fontsize':7},
{'id':'tubular','label':'Tubular (ATN):\nIschaemia / Sepsis\nNephrotoxins (contrast\nAminoglycosides)','x':6.1,'y':3.8,'shape':'round','color':'#F57F17','fc':'#FFF9C4','w':2.4,'h':1.0,'fontsize':7},
{'id':'obstr','label':'Obstruction:\nBPH / Stones\nRetroperitoneal\nmalignancy','x':8.8,'y':3.8,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.4,'h':1.0,'fontsize':7},
{'id':'rx_pre','label':'IV fluids / Treat HF\nACEi/ARBs hold\nMonitor urine output','x':1.5,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':2.8,'h':0.9,'fontsize':7},
{'id':'rx_renal','label':'Biopsy if glomerular\nStop nephrotoxins\nCheck ANA/ANCA/Anti-GBM','x':5,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':2.8,'h':0.9,'fontsize':7},
{'id':'rx_post','label':'Urinary catheter / Stent\nUrology referral\nNephrostomy if needed','x':8.5,'y':2.2,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':2.8,'h':0.9,'fontsize':7},
]
arrows = [
('start','ratio',''),
('ratio','pre','> 20:1'),
('ratio','renal','10-20:1'),
('ratio','post','Obstruction'),
('pre','pre_cause',''),
('renal','renal_cause',''),
('post','post_cause',''),
('pre_cause','vol_dep',''),
('renal_cause','glom','Glom'),
('renal_cause','tubular','ATN'),
('post_cause','obstr',''),
('vol_dep','rx_pre',''),
('glom','rx_renal',''),
('tubular','rx_renal',''),
('obstr','rx_post',''),
]
return make_flowchart('Renal Function (Creatinine/BUN) Interpretation Flowchart', nodes, arrows, figsize=(10,11))
def fc_abg():
nodes = [
{'id':'start','label':'Arterial Blood Gas (ABG)','x':5,'y':9.3,'shape':'oval','color':'#1A2B5C','fc':'#E8EAF6','w':3.5,'h':0.7},
{'id':'ph','label':'Check pH first','x':5,'y':8.2,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':3.0,'h':0.8},
{'id':'acid','label':'ACIDOSIS\npH < 7.35','x':2,'y':6.9,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.6,'h':0.7},
{'id':'alk','label':'ALKALOSIS\npH > 7.45','x':8,'y':6.9,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.6,'h':0.7},
{'id':'normal_ph','label':'NORMAL pH 7.35-7.45\nCheck for compensation','x':5,'y':6.9,'shape':'round','color':'#2D6A4F','fc':'#C8E6C9','w':2.8,'h':0.7,'fontsize':7},
{'id':'acid_co2','label':'Check PaCO₂','x':2,'y':5.6,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.6,'h':0.75,'fontsize':8},
{'id':'alk_co2','label':'Check PaCO₂','x':8,'y':5.6,'shape':'diamond','color':'#006D77','fc':'#E0F2F1','w':2.6,'h':0.75,'fontsize':8},
{'id':'resp_acid','label':'RESPIRATORY ACIDOSIS\nPaCO₂ > 45\nCOPD / Opioids\nRespiratory failure','x':0.8,'y':4.2,'shape':'round','color':'#C62828','fc':'#FFCDD2','w':2.6,'h':1.0,'fontsize':7},
{'id':'met_acid','label':'METABOLIC ACIDOSIS\nHCO₃ < 22\nDKA / Lactic acidosis\nRenal failure / Diarrhea','x':3.3,'y':4.2,'shape':'round','color':'#7B1FA2','fc':'#F3E5F5','w':2.6,'h':1.0,'fontsize':7},
{'id':'resp_alk','label':'RESPIRATORY ALKALOSIS\nPaCO₂ < 35\nHyperventilation\nSepsis / Anxiety','x':6.5,'y':4.2,'shape':'round','color':'#E65100','fc':'#FFE0B2','w':2.6,'h':1.0,'fontsize':7},
{'id':'met_alk','label':'METABOLIC ALKALOSIS\nHCO₃ > 26\nVomiting / Diuretics\nCushing\'s / Conn\'s','x':9.2,'y':4.2,'shape':'round','color':'#F57F17','fc':'#FFF9C4','w':2.6,'h':1.0,'fontsize':7},
{'id':'anion','label':'ANION GAP = Na - (Cl + HCO₃)\nNormal: 7-16 mEq/L\nHigh AG: DKA, Lactic, Uremia,\nMethanol, Salicylates\nNormal AG: RTA, Diarrhea','x':2,'y':2.5,'shape':'round','color':'#1A2B5C','fc':'#FFFDE7','w':3.2,'h':1.1,'fontsize':7},
{'id':'rx_resp','label':'Rx resp acidosis:\nO₂ / Ventilation support\nBronchodilators / Naloxone','x':0.8,'y':2.5,'shape':'round','color':'#1A2B5C','fc':'#E8EAF6','w':2.4,'h':0.9,'fontsize':6},
{'id':'compensation','label':'Check compensation:\nMetabolic acidosis → PaCO₂ ↓\nMetabolic alkalosis → PaCO₂ ↑\nRespiratory → HCO₃ changes','x':7.5,'y':2.5,'shape':'round','color':'#37474F','fc':'#ECEFF1','w':3.2,'h':1.0,'fontsize':7},
]
arrows = [
('start','ph',''),
('ph','acid','< 7.35'),
('ph','alk','> 7.45'),
('ph','normal_ph','7.35-7.45'),
('acid','acid_co2',''),
('alk','alk_co2',''),
('acid_co2','resp_acid','↑ CO₂'),
('acid_co2','met_acid','↓ HCO₃'),
('alk_co2','resp_alk','↓ CO₂'),
('alk_co2','met_alk','↑ HCO₃'),
('met_acid','anion','AG check'),
('resp_acid','rx_resp',''),
('met_alk','compensation',''),
('resp_alk','compensation',''),
]
return make_flowchart('Arterial Blood Gas (ABG) Interpretation Flowchart', nodes, arrows, figsize=(10,11))
# ── Reference table helper ──────────────────────────────────────────────────────
def ref_table(styles, data, col_widths, header_color=NAVY):
th = styles['TableHeader']
tc = styles['TableCell']
formatted = []
for i, row in enumerate(data):
if i == 0:
formatted.append([Paragraph(str(c), th) for c in row])
else:
formatted.append([Paragraph(str(c), tc) for c in row])
t = Table(formatted, colWidths=col_widths)
style = TableStyle([
('BACKGROUND', (0,0), (-1,0), header_color),
('TEXTCOLOR', (0,0), (-1,0), colors.white),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BG, colors.white]),
('GRID', (0,0), (-1,-1), 0.4, colors.HexColor('#B0BEC5')),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING',(0,0),(-1,-1), 4),
('LEFTPADDING',(0,0), (-1,-1), 5),
])
t.setStyle(style)
return t
# ── Section banner ──────────────────────────────────────────────────────────────
def section_banner(text, color, styles):
banner_style = ParagraphStyle('Banner',
fontName='Helvetica-Bold', fontSize=13, textColor=WHITE,
alignment=TA_LEFT, leftIndent=12, leading=18,
spaceAfter=6, spaceBefore=10)
p = Paragraph(text, banner_style)
t = Table([[p]], colWidths=[17*cm])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), color),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING',(0,0),(-1,-1), 6),
('LEFTPADDING',(0,0), (-1,-1), 8),
('ROUNDEDCORNERS', [4]),
]))
return t
def info_box(text, styles, bg=MINT, border=TEAL):
p = Paragraph(text, styles['Body'])
t = Table([[p]], colWidths=[17*cm])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), bg),
('BOX', (0,0), (-1,-1), 1, border),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING',(0,0),(-1,-1), 6),
('LEFTPADDING',(0,0), (-1,-1), 8),
]))
return t
def img_from_buf(buf, width=17*cm):
img = RLImage(buf, width=width, height=width*1.05)
return img
# ── Main builder ────────────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.2*cm, bottomMargin=2.2*cm,
title='Clinical Laboratory Interpretation Guide',
author='Orris Clinical Reference',
)
styles = build_styles()
W = 17*cm
story = []
# ═══════════════════════════════════════════════════════
# COVER PAGE
# ═══════════════════════════════════════════════════════
cover_bg = Table(
[[Paragraph('', styles['Body'])]],
colWidths=[W], rowHeights=[25*cm]
)
cover_bg.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,-1), NAVY)]))
story.append(cover_bg)
story.append(Spacer(1, -25*cm))
story.append(Spacer(1, 3.5*cm))
story.append(Paragraph('🔬 Clinical Laboratory', styles['CoverTitle']))
story.append(Paragraph('Interpretation Guide', styles['CoverTitle']))
story.append(Spacer(1, 0.5*cm))
story.append(Paragraph('with Diagnostic Flowcharts', styles['CoverSub']))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph('Normal Values · Abnormal Patterns · Differential Diagnoses', styles['CoverSub']))
story.append(Spacer(1, 1.5*cm))
cover_info = Table([
[Paragraph('<b>Electrolytes</b><br/>Na · K · Cl · HCO₃ · Ca · Mg · Phosphate', styles['CoverSub'])],
[Paragraph('<b>Metabolic</b><br/>Blood Glucose · HbA1c · Lipids · Uric Acid', styles['CoverSub'])],
[Paragraph('<b>Organ Function</b><br/>Liver (LFTs) · Kidneys (Creatinine/BUN) · Thyroid', styles['CoverSub'])],
[Paragraph('<b>Critical Values</b><br/>ABG · Cardiac Biomarkers · Inflammatory Markers', styles['CoverSub'])],
], colWidths=[W*0.75])
cover_info.setStyle(TableStyle([
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('BOX', (0,0), (-1,0), 0.5, colors.HexColor('#90CAF9')),
('BOX', (0,1), (-1,1), 0.5, colors.HexColor('#90CAF9')),
('BOX', (0,2), (-1,2), 0.5, colors.HexColor('#90CAF9')),
('BOX', (0,3), (-1,3), 0.5, colors.HexColor('#90CAF9')),
('LEFTPADDING', (0,0), (-1,-1), 30),
]))
story.append(cover_info)
story.append(Spacer(1, 2.0*cm))
story.append(Paragraph('Sources: Guyton & Hall Physiology · Goldman-Cecil Medicine · Harrison\'s Principles 22e', styles['CoverSub']))
story.append(Paragraph('July 2026', styles['CoverSub']))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# PAGE 2: QUICK REFERENCE TABLE
# ═══════════════════════════════════════════════════════
story.append(section_banner('📊 Quick Reference: Normal Lab Values', NAVY, styles))
story.append(Spacer(1, 0.3*cm))
story.append(info_box(
'<b>Clinical note:</b> Reference ranges cover the middle 95% of healthy adults. '
'Always correlate with clinical context. Ranges may vary by laboratory method, '
'patient age, sex, and pregnancy status.',
styles))
story.append(Spacer(1, 0.4*cm))
qr_data = [
['Test', 'Normal Range', 'Low Suggests', 'High Suggests'],
['Sodium (Na⁺)', '135-145 mmol/L', 'SIADH, HF, cirrhosis, vomiting', 'Dehydration, DI, Conn\'s'],
['Potassium (K⁺)', '3.5-5.3 mmol/L', 'Diuretics, vomiting, alkalosis', 'Renal failure, Addison\'s, acidosis'],
['Chloride (Cl⁻)', '98-108 mmol/L', 'Metabolic alkalosis, vomiting', 'Hyperchloremic acidosis, RTA'],
['Bicarbonate (HCO₃⁻)', '22-26 mEq/L', 'Metabolic acidosis (DKA, diarrhea)', 'Metabolic alkalosis, Cushing\'s'],
['Calcium (total)', '8.5-10.5 mg/dL', 'Hypoparathyroid, Vit D def., CKD', 'Hyperparathyroid, malignancy'],
['Magnesium', '1.3-2.4 mEq/L', 'Alcoholism, diuretics, malabsorption', 'Renal failure, excess Mg intake'],
['Phosphate', '2.5-4.5 mg/dL', 'Hyperparathyroid, malnutrition, refeeding', 'Renal failure, hypoparathyroid'],
['Blood Glucose (fast)', '70-100 mg/dL', 'Insulinoma, Addison\'s, liver failure', 'Diabetes, Cushing\'s, stress'],
['HbA1c', '< 5.7%', 'Hemolytic anemia (falsely low)', 'Pre-DM 5.7-6.4%; DM ≥ 6.5%'],
['Creatinine (M)', '0.7-1.3 mg/dL', 'Low muscle mass, malnutrition', 'CKD, AKI, rhabdomyolysis'],
['Creatinine (F)', '0.6-1.1 mg/dL', 'Low muscle mass, malnutrition', 'CKD, AKI, rhabdomyolysis'],
['BUN', '8-25 mg/dL', 'Liver failure, overhydration', 'Pre-renal, AKI, CKD, GI bleed'],
['Uric acid (M)', '3.5-7.2 mg/dL', 'Allopurinol, Wilson\'s, SIADH', 'Gout, CKD, diuretics, leukemia'],
['ALT (SGPT)', '7-56 U/L', '—', 'Hepatitis, liver damage, NAFLD'],
['AST (SGOT)', '10-40 U/L', '—', 'Liver disease, MI, rhabdomyolysis'],
['ALP', '44-147 U/L', '—', 'Cholestasis, bone disease, pregnancy'],
['GGT', 'M:8-61; F:5-36 U/L', '—', 'Alcohol, cholestasis, enzyme induction'],
['Total Bilirubin', '0.2-1.2 mg/dL', '—', 'Jaundice > 2.5 mg/dL'],
['Albumin', '3.5-5.0 g/dL', 'Cirrhosis, malnutrition, nephrotic', 'Dehydration (relative)'],
['Total Protein', '6.4-8.3 g/dL', 'Malnutrition, liver disease', 'Myeloma, chronic infection'],
['Total Cholesterol', '< 200 mg/dL', 'Malnutrition, hyperthyroid', 'CVD risk, hypothyroid'],
['LDL Cholesterol', '< 100 mg/dL (optimal)', 'Over-treated, malnutrition', 'CVD risk, FH, nephrotic'],
['HDL Cholesterol', '≥ 60 mg/dL (protective)', 'CVD risk, obesity, DM', 'Exercise, statin therapy'],
['Triglycerides', '< 150 mg/dL', '—', 'DM, obesity, alcohol, hypothyroid'],
['TSH', '0.4-4.0 mIU/L', 'Hyperthyroid, pituitary failure', 'Hypothyroid (primary)'],
['Free T4', '0.8-1.8 ng/dL', 'Hypothyroid', 'Hyperthyroid'],
['Troponin I/T (hs)', '< 0.04 ng/mL', '—', 'MI, myocarditis, PE, sepsis'],
['BNP', '< 100 pg/mL', '—', 'Heart failure, pulmonary HTN'],
['CRP', '< 1.0 mg/dL', '—', 'Bacterial infection, inflammation'],
['Procalcitonin (PCT)', '< 0.1 ng/mL', '—', '> 0.5 = bacterial sepsis likely'],
['pH (arterial)', '7.35-7.45', 'Acidosis', 'Alkalosis'],
['PaCO₂', '35-45 mmHg', 'Resp. alkalosis / Compensation', 'Resp. acidosis / Compensation'],
['PaO₂', '75-100 mmHg', 'Hypoxemia / Type 1 resp failure', '—'],
]
story.append(ref_table(styles, qr_data, [3.5*cm, 3.2*cm, 5.2*cm, 5.1*cm]))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# ELECTROLYTES SECTION
# ═══════════════════════════════════════════════════════
story.append(section_banner('💧 Electrolytes: Sodium & Potassium', TEAL, styles))
story.append(Spacer(1, 0.3*cm))
# Sodium flowchart
story.append(Paragraph('Sodium (Na⁺) Interpretation Algorithm', styles['SubHeader']))
buf_na = fc_sodium()
story.append(img_from_buf(buf_na, width=W))
story.append(Paragraph('Fig 1. Systematic approach to hypo- and hypernatremia. Correction rate must not exceed 0.5 mEq/L/hr to prevent osmotic demyelination syndrome.', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
na_detail = [
['Condition', 'Key Lab Findings', 'Common Causes', 'Treatment'],
['Hyponatremia\n< 135 mmol/L', 'Low Na, check Uosm\nand UNa', 'SIADH, HF, cirrhosis,\ndiuretics, Addison\'s', 'Fluid restriction; hypertonic\nNaCl if symptomatic'],
['Hypernatremia\n> 145 mmol/L', 'High Na, check urine\nosmolality and volume', 'Dehydration, DI,\nConn\'s syndrome', 'Free water orally or IV\n5% dextrose; correct slowly'],
['SIADH', 'Low Na, Low osm,\nUrine Na > 20, Uosm > 100', 'Malignancy, CNS disease,\nPneumonia, drugs (SSRIs)', 'Fluid restrict, Tolvaptan,\nDemeclocycline'],
['Diabetes insipidus', 'High Na, dilute urine\n(Uosm < 300)', 'Central (ADH def) or\nNephrogenic (ADH resistance)', 'Central: Desmopressin\nNephrogenic: Thiazide, low Na diet'],
]
story.append(ref_table(styles, na_detail, [3.0*cm, 3.8*cm, 5.0*cm, 5.2*cm], TEAL))
story.append(PageBreak())
# Potassium flowchart
story.append(Paragraph('Potassium (K⁺) Interpretation Algorithm', styles['SubHeader']))
buf_k = fc_potassium()
story.append(img_from_buf(buf_k, width=W))
story.append(Paragraph('Fig 2. Potassium disorders. Hyperkalemia is a medical emergency - obtain ECG immediately. Classic ECG progression: peaked T → PR prolongation → wide QRS → sine wave → VF.', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
k_detail = [
['Condition', 'ECG Changes', 'Common Causes', 'Treatment Priority'],
['Hypokalemia\n< 3.5 mmol/L', 'Flat/inverted T, U waves,\nST depression', 'Diuretics, vomiting,\ndiarrhea, Cushing\'s', 'Oral KCl (mild)\nIV KCl (< 2.5 or symptomatic)'],
['Hyperkalemia\n> 5.3 mmol/L', 'Peaked T waves → wide QRS\n→ Sine wave → VF', 'CKD, Addison\'s, ACEi,\nRhabdomyolysis, acidosis', '1st: Ca gluconate (cardioprotect)\n2nd: Insulin+glucose\n3rd: Dialysis'],
['Hypokalemic\nperiodic paralysis', 'Flaccid paralysis,\nno ECG changes initially', 'Thyrotoxicosis, exercise,\nhigh carbs, familial', 'IV KCl + treat thyroid\nAvoid glucose/insulin'],
]
story.append(ref_table(styles, k_detail, [3.0*cm, 3.5*cm, 5.0*cm, 5.5*cm], TEAL))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# GLUCOSE SECTION
# ═══════════════════════════════════════════════════════
story.append(section_banner('🩸 Blood Glucose & Diabetes Interpretation', colors.HexColor('#AD1457'), styles))
story.append(Spacer(1, 0.3*cm))
buf_gluc = fc_glucose()
story.append(img_from_buf(buf_gluc, width=W))
story.append(Paragraph('Fig 3. Blood glucose interpretation. Rule of 15 for hypoglycemia: 15g carbs → wait 15 min → recheck. Repeat × 3 if still < 70 mg/dL. Then give meal/snack.', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
gluc_detail = [
['Condition', 'Glucose', 'HbA1c', 'Other Findings', 'Management'],
['Normal', '70-100 mg/dL', '< 5.7%', 'Normal insulin, no proteinuria', 'Healthy lifestyle'],
['Pre-diabetes (IFG)', '100-125 mg/dL', '5.7-6.4%', 'OGTT 2hr: 140-199 mg/dL', 'Lifestyle mod, monitor annually'],
['Type 2 DM', '≥ 126 mg/dL', '≥ 6.5%', 'Overweight, acanthosis nigricans,\nhigh insulin, insulin resistance', 'Metformin + GLP-1 RA or SGLT2i\nLifestyle, CVD risk reduction'],
['Type 1 DM', '≥ 126 mg/dL', '≥ 6.5%', 'Anti-GAD, IA-2, ZnT8 antibodies\nLow C-peptide, thin young patient', 'Insulin (basal-bolus regimen)\nCarb counting, CGM'],
['DKA', '> 250 mg/dL', 'High', 'pH < 7.3, HCO₃ < 15\nKetones ++, raised anion gap', 'IV fluids + Insulin infusion\nK⁺ replacement, monitor closely'],
['Hypoglycemia', '< 70 mg/dL', 'Low if recurrent', 'Whipple\'s triad: symptoms +\nlow glucose + relief with glucose', 'Oral dextrose/glucose tablets\nSevere: IV D50 or Glucagon 1mg IM'],
]
story.append(ref_table(styles, gluc_detail, [2.8*cm, 2.5*cm, 1.8*cm, 4.7*cm, 5.2*cm], colors.HexColor('#AD1457')))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# LIVER FUNCTION
# ═══════════════════════════════════════════════════════
story.append(section_banner('🫀 Liver Function Tests (LFTs)', colors.HexColor('#2E7D32'), styles))
story.append(Spacer(1, 0.3*cm))
buf_liver = fc_liver()
story.append(img_from_buf(buf_liver, width=W))
story.append(Paragraph('Fig 4. LFT interpretation algorithm. AST:ALT > 2:1 strongly suggests alcoholic hepatitis. Isolated ALP rise = bone or liver; confirm with GGT (elevated = liver source).', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
liver_detail = [
['Condition', 'ALT/AST', 'ALP', 'Bilirubin', 'Albumin/PT', 'Key Tests'],
['Viral hepatitis\n(acute)', 'Very high\n(> 500 U/L)', 'Normal/mild ↑', 'Elevated\n(conjugated)', 'Usually normal', 'HBsAg, HCV RNA,\nHAV IgM'],
['Alcoholic\nhepatitis', 'Moderate ↑\nAST:ALT > 2:1', 'Mild-moderate ↑', 'Elevated', 'Low albumin\nHigh PT', 'GGT very high\nMDF score'],
['NAFLD/NASH', 'Mild ↑\nALT > AST', 'Normal/mild ↑', 'Usually normal', 'Usually normal', 'Liver USS, BMI\nFibroscan'],
['Biliary\nobstruction', 'Mild ↑', 'Very high\n(> 3× ULN)', 'Conjugated ↑↑\nDark urine, pale stools', 'Low if chronic', 'USS/MRCP\nCA 19-9'],
['Cirrhosis\n(chronic)', 'Mild-moderate ↑\nor normal', 'Elevated', 'Elevated\n(late)', 'Low albumin\nHigh PT, low PLT', 'Child-Pugh, MELD\nVarices screen'],
['Drug-induced\n(e.g. paracetamol)', 'Very high\n(> 1000 if OD)', 'Normal/mild ↑', 'Elevated', 'High PT/INR', 'Drug history\nParacetamol level'],
['Gilbert\'s\nsyndrome', 'Normal', 'Normal', 'Unconjugated ↑\n(isolated)', 'Normal', 'No treatment needed\nBenign condition'],
]
story.append(ref_table(styles, liver_detail, [2.5*cm, 2.2*cm, 2.0*cm, 2.8*cm, 2.5*cm, 5.0*cm], colors.HexColor('#2E7D32')))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# RENAL FUNCTION
# ═══════════════════════════════════════════════════════
story.append(section_banner('🔵 Renal Function Tests', colors.HexColor('#1565C0'), styles))
story.append(Spacer(1, 0.3*cm))
buf_renal = fc_renal()
story.append(img_from_buf(buf_renal, width=W))
story.append(Paragraph('Fig 5. Acute kidney injury (AKI) classification using BUN:Creatinine ratio and fractional excretion of sodium (FENa). FENa < 1% = pre-renal; FENa > 2% = intrinsic renal.', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
ckd_stages = [
['CKD Stage', 'eGFR (mL/min/1.73m²)', 'Description', 'Key Actions'],
['G1', '≥ 90', 'Normal/High', 'Treat cause, CV risk reduction'],
['G2', '60-89', 'Mildly decreased', 'Monitor annually, control BP/DM'],
['G3a', '45-59', 'Mildly-moderately decreased', 'Nephrology review if needed'],
['G3b', '30-44', 'Moderately-severely decreased', 'Avoid nephrotoxins, adjust doses'],
['G4', '15-29', 'Severely decreased', 'Prepare for renal replacement therapy'],
['G5', '< 15', 'Kidney failure', 'Dialysis or transplantation'],
]
story.append(Paragraph('CKD Staging (KDIGO 2022)', styles['SubHeader']))
story.append(ref_table(styles, ckd_stages, [1.8*cm, 4.0*cm, 5.0*cm, 6.2*cm], colors.HexColor('#1565C0')))
story.append(Spacer(1, 0.4*cm))
aki_table = [
['AKI Stage', 'Creatinine Criteria', 'Urine Output Criteria', 'Management Focus'],
['Stage 1', '1.5-1.9× baseline\nor ≥ 0.3 mg/dL rise', '< 0.5 mL/kg/hr for 6-12 hrs', 'Remove nephrotoxins, optimize fluids'],
['Stage 2', '2.0-2.9× baseline', '< 0.5 mL/kg/hr for ≥ 12 hrs', 'ICU/HDU monitoring, renal consult'],
['Stage 3', '≥ 3× baseline\nor ≥ 4.0 mg/dL', '< 0.3 mL/kg/hr for ≥ 24 hrs\nor anuria ≥ 12 hrs', 'Dialysis if: fluid overload, severe\nhyperK, acidosis, uremia'],
]
story.append(Paragraph('AKI Staging (KDIGO Criteria)', styles['SubHeader']))
story.append(ref_table(styles, aki_table, [2.2*cm, 4.0*cm, 4.2*cm, 6.6*cm], colors.HexColor('#1565C0')))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# ABG SECTION
# ═══════════════════════════════════════════════════════
story.append(section_banner('💨 Arterial Blood Gas (ABG) Interpretation', colors.HexColor('#4A148C'), styles))
story.append(Spacer(1, 0.3*cm))
buf_abg = fc_abg()
story.append(img_from_buf(buf_abg, width=W))
story.append(Paragraph('Fig 6. ABG interpretation algorithm. Always: (1) Check pH, (2) Identify primary disorder, (3) Assess compensation, (4) Calculate anion gap if metabolic acidosis.', styles['Caption']))
story.append(Spacer(1, 0.3*cm))
abg_table = [
['Disorder', 'pH', 'PaCO₂', 'HCO₃', 'Expected Compensation', 'Common Causes'],
['Resp. Acidosis\n(Acute)', '↓', '↑ > 45', 'Normal\n(+1 per 10 CO₂)', 'HCO₃ ↑ by 1 mEq/L\nper 10 mmHg CO₂ ↑', 'COPD exac., opioid OD,\napnea, NM weakness'],
['Resp. Acidosis\n(Chronic)', '↓ (mild)', '↑ > 45', '↑ significantly\n(+3.5 per 10 CO₂)', 'HCO₃ ↑ by 3.5 mEq/L\nper 10 mmHg CO₂ ↑', 'Chronic COPD,\nobesity hypoventilation'],
['Resp. Alkalosis', '↑', '↓ < 35', 'Normal/↓', 'HCO₃ ↓ by 2 mEq/L\nper 10 mmHg CO₂ ↓', 'Anxiety, sepsis,\npregnancy, altitude'],
['Met. Acidosis', '↓', 'Normal/↓', '↓ < 22', 'CO₂ = 1.5×(HCO₃)+8±2\n(Winter\'s formula)', 'DKA, lactic acidosis,\nrenal failure, diarrhea'],
['Met. Alkalosis', '↑', 'Normal/↑', '↑ > 26', 'CO₂ ↑ by 0.7×\n(HCO₃ - 24)', 'Vomiting, diuretics,\nCushing\'s, Conn\'s'],
]
story.append(ref_table(styles, abg_table, [2.8*cm, 1.2*cm, 2.0*cm, 2.5*cm, 3.5*cm, 5.0*cm], colors.HexColor('#4A148C')))
story.append(Spacer(1, 0.4*cm))
anion_gap = [
['Anion Gap Status', 'Causes (MUDPILES mnemonic)'],
['High Anion Gap\nMetabolic Acidosis\n(AG > 16)', 'M - Methanol\nU - Uremia (renal failure)\nD - Diabetic ketoacidosis (DKA)\nP - Propylene glycol / Paracetamol\nI - Isoniazid / Iron toxicity\nL - Lactic acidosis\nE - Ethylene glycol\nS - Salicylates'],
['Normal Anion Gap\nMetabolic Acidosis\n(Hyperchloremic)', 'D - Diarrhea (bicarbonate loss)\nU - Ureteral diversion\nR - Renal tubular acidosis (RTA)\nA - Acetazolamide\nG - GI fistulas\nS - Saline infusion (excess)'],
]
story.append(Paragraph('Anion Gap in Metabolic Acidosis (Normal AG = 7-16 mEq/L)', styles['SubHeader']))
story.append(ref_table(styles, anion_gap, [4.5*cm, 12.5*cm], colors.HexColor('#4A148C')))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# LIPIDS & CARDIAC
# ═══════════════════════════════════════════════════════
story.append(section_banner('❤️ Lipid Profile & Cardiac Biomarkers', RED, styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph('Lipid Profile Interpretation', styles['SubHeader']))
lipid_data = [
['Parameter', 'Optimal', 'Borderline', 'High Risk', 'Very High / Critical'],
['Total Cholesterol', '< 200 mg/dL', '200-239 mg/dL', '≥ 240 mg/dL', '—'],
['LDL Cholesterol', '< 70 (very high CVD risk)\n< 100 (high risk)\n< 130 (moderate)', '130-159 mg/dL', '160-189 mg/dL', '≥ 190 (FH likely)'],
['HDL Cholesterol', '≥ 60 mg/dL\n(protective)', '40-59 mg/dL', '< 40 (men)\n< 50 (women)', '—'],
['Triglycerides', '< 150 mg/dL', '150-199 mg/dL', '200-499 mg/dL', '≥ 500 (pancreatitis risk)'],
['Non-HDL Cholesterol', '< 130 mg/dL', '130-159 mg/dL', '≥ 160 mg/dL', '—'],
]
story.append(ref_table(styles, lipid_data, [3.0*cm, 3.5*cm, 3.0*cm, 3.0*cm, 4.5*cm], RED))
story.append(Spacer(1, 0.4*cm))
story.append(info_box(
'<b>Causes of secondary dyslipidemia:</b> Hypothyroidism (↑LDL) · Nephrotic syndrome (↑LDL, ↑TG) · '
'Diabetes (↑TG, ↓HDL) · Chronic kidney disease · Alcoholism (↑TG) · Cushing\'s syndrome · '
'Drugs: corticosteroids, thiazides, beta-blockers, retinoids, antipsychotics.',
styles, bg=colors.HexColor('#FFEBEE'), border=RED))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph('Cardiac Biomarkers', styles['SubHeader']))
cardiac_data = [
['Biomarker', 'Normal', 'Rise After MI', 'Peak', 'Returns Normal', 'Key Uses'],
['Troponin I/T (hs)', '< 0.04 ng/mL', '3-6 hours', '12-24 hrs', '5-14 days', 'Diagnosis of MI, myocarditis,\nPE risk stratification'],
['CK-MB', '< 5 ng/mL', '3-6 hours', '12-24 hrs', '48-72 hrs', 'Reinfarction detection\n(returns to normal faster)'],
['Myoglobin', '< 90 ng/mL', '1-3 hours', '6-12 hrs', '24-36 hrs', 'Early marker (non-specific)\nAlso raised in rhabdomyolysis'],
['BNP', '< 100 pg/mL', 'Chronic ↑', 'Persistent', 'Improves with Rx', 'Heart failure diagnosis\nDyspnea differentiation'],
['NT-proBNP', '< 125 pg/mL', 'Chronic ↑', 'Persistent', 'Improves with Rx', 'HF diagnosis (adjust\ncutoff > 75 yrs: < 450)'],
['LDH', '140-280 U/L', '12-24 hours', '48-72 hrs', '7-10 days', 'Late MI marker, hemolysis,\ntumor lysis syndrome'],
]
story.append(ref_table(styles, cardiac_data, [2.8*cm, 2.2*cm, 2.0*cm, 1.8*cm, 2.0*cm, 6.2*cm], RED))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# THYROID & INFLAMMATORY MARKERS
# ═══════════════════════════════════════════════════════
story.append(section_banner('🦋 Thyroid Function & Inflammatory Markers', ORANGE, styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph('Thyroid Function Test Interpretation', styles['SubHeader']))
thyroid_data = [
['TSH', 'Free T4', 'Free T3', 'Diagnosis', 'Common Causes', 'Management'],
['High', 'Low', 'Low', 'Primary\nHypothyroidism', 'Hashimoto\'s, iodine deficiency,\npost-thyroidectomy, amiodarone, lithium', 'Levothyroxine\n(target TSH 0.5-2.5)'],
['Low', 'High', 'High', 'Primary\nHyperthyroidism', 'Graves\' disease, toxic MNG,\nsolitary toxic adenoma, thyroiditis', 'Carbimazole/PTU,\nbeta-blockers, radioiodine'],
['High', 'Normal', 'Normal', 'Subclinical\nHypothyroid', 'Early Hashimoto\'s,\npost-RAI treatment', 'Treat if TSH > 10\nor symptoms present'],
['Low', 'Normal', 'Normal', 'Subclinical\nHyperthyroid', 'Early Graves\', over-replacement,\nsolitary toxic adenoma', 'Monitor; treat if\nAF risk or osteoporosis'],
['Low', 'Low', 'Low', 'Central\nHypothyroid', 'Pituitary adenoma,\nSheehan\'s syndrome, pituitary surgery', 'Treat pituitary cause;\nLevothyroxine'],
['Normal', 'Low/Normal', 'Low', 'Non-thyroidal\nIllness (Sick euthyroid)', 'Severe illness, starvation,\nICU patients', 'Treat underlying illness;\ndo NOT give T4'],
]
story.append(ref_table(styles, thyroid_data, [1.5*cm, 1.5*cm, 1.5*cm, 2.5*cm, 4.5*cm, 5.5*cm], ORANGE))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph('Inflammatory & Acute Phase Markers', styles['SubHeader']))
inflam_data = [
['Marker', 'Normal', 'Mildly Elevated', 'Markedly Elevated', 'Clinical Use'],
['CRP (C-Reactive\nProtein)', '< 1.0 mg/dL\n(< 10 mg/L)', '1-10 mg/dL\n(mild inflammation)', '> 10 mg/dL\nBacterial infection', 'Monitor infection, inflammation\nhi-sCRP for CVD risk (< 3 mg/L)'],
['ESR (Erythrocyte\nSedimen. Rate)', 'M: < 15 mm/hr\nF: < 20 mm/hr', '20-50 mm/hr\n(non-specific)', '> 100 mm/hr\nMyeloma, TB, endocarditis', 'Non-specific; useful in myeloma,\npolymyalgia rheumatica'],
['Procalcitonin\n(PCT)', '< 0.1 ng/mL', '0.1-0.5 (viral/local\nbacterial possible)', '> 0.5 bacterial sepsis\n> 2 severe sepsis\n> 10 septic shock', 'Guide antibiotic initiation\nand de-escalation'],
['Ferritin', 'M: 12-300 ng/mL\nF: 12-150 ng/mL', '—', '> 500 in inflammation\n> 1000 HLH, ferritinemia', 'Iron deficiency (low)\nHemochromatosis (very high)'],
['D-dimer', '< 0.5 µg/mL FEU', '—', '> 0.5 = VTE possible\n(high sensitivity, low spec)', 'Rule out DVT/PE;\nnot useful to rule in'],
['Fibrinogen', '200-400 mg/dL', '—', '> 400 = acute phase\n< 100 = DIC/severe liver', 'DIC: low + prolonged PT/aPTT\n+ low platelets'],
]
story.append(ref_table(styles, inflam_data, [2.8*cm, 2.8*cm, 3.0*cm, 3.5*cm, 5.0*cm], ORANGE))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# CALCIUM, ANEMIA, CRITICAL VALUES
# ═══════════════════════════════════════════════════════
story.append(section_banner('⚡ Calcium Disorders, Anemia & Critical Lab Values', colors.HexColor('#00695C'), styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph('Calcium Disorder Differential', styles['SubHeader']))
calcium_data = [
['Condition', 'Ca', 'PTH', 'Phosphate', 'Vitamin D', 'Causes'],
['Primary\nHyperparathyroidism', '↑', '↑↑', '↓', 'Normal/↓', 'Parathyroid adenoma (85%),\nhyperplasia, carcinoma'],
['Malignancy\n(hypercalcemia)', '↑', '↓ (suppressed)', 'Normal/↑', 'Normal', 'PTHrP secretion,\nlytic bone mets, lymphoma (1,25-D3)'],
['Sarcoidosis', '↑', '↓', 'Normal', 'High 1,25-D3', 'Granuloma converts 25-D3\nto active 1,25-D3'],
['Primary\nHypoparathyroidism', '↓', '↓↓', '↑', 'Normal', 'Post-thyroid surgery,\nautoimmune, DiGeorge'],
['Vitamin D\nDeficiency', '↓ (mild)', '↑ (2° HPT)', '↓ or normal', 'Low 25-D3', 'Poor sunlight, malabsorption,\nchronic liver/renal disease'],
['CKD (2° HPT)', '↓ or normal', '↑↑', '↑', 'Low 1,25-D3', 'Impaired renal 1-alpha\nhydroxylase; phosphate retention'],
['Pseudohypo-\nparathyroidism', '↓', '↑↑', '↑', 'Normal', 'PTH resistance (Albright\nosteodystrophy), GNAS mutation'],
]
story.append(ref_table(styles, calcium_data, [2.8*cm, 1.0*cm, 1.2*cm, 2.0*cm, 2.2*cm, 7.8*cm], colors.HexColor('#00695C')))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph('Anemia - Biochemical Classification', styles['SubHeader']))
anemia_data = [
['Type', 'Hb', 'MCV', 'Serum Iron', 'Ferritin', 'TIBC', 'Other', 'Causes'],
['Iron Deficiency\nAnemia (IDA)', '↓', '↓\n(< 80 fL)', '↓', '↓↓', '↑', 'Low reticulocytes\nHypochromic RBCs', 'GI blood loss, menorrhagia,\nmalabsorption (celiac)'],
['Anemia of Chronic\nDisease (ACD)', '↓\n(mild)', 'Normal/↓', '↓', 'Normal/↑\n(key diff)', '↓', 'High CRP,\nhigh hepcidin', 'Chronic infection, malignancy,\nautoimmune diseases'],
['Megaloblastic\n(B12/Folate def.)', '↓', '↑\n(> 100 fL)', 'Normal', 'Normal', 'Normal', 'Hypersegmented\nneutrophils, oval macrocytes', 'B12: pernicious anemia,\nvegetarians, gastrectomy\nFolate: alcohol, pregnancy'],
['Haemolytic\nAnemia', '↓', 'Normal/↑', 'Normal/↑', '↑', 'Normal/↓', '↑LDH, ↑Indirect bili,\n↓Haptoglobin', 'AIHA, hereditary spherocytosis,\nG6PD def., sickle cell, TTP'],
['Aplastic Anemia', '↓↓', 'Normal', 'Normal/↑', '↑', 'Normal', 'Pancytopenia,\nhypocellular marrow', 'Idiopathic, drugs, radiation,\nparoxysmal nocturnal hemoglobinuria'],
]
story.append(ref_table(styles, anemia_data, [2.2*cm, 0.8*cm, 1.0*cm, 1.5*cm, 1.5*cm, 1.0*cm, 3.0*cm, 5.5*cm], colors.HexColor('#00695C')))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph('Critical (Panic) Laboratory Values - Require Immediate Action', styles['SubHeader']))
critical_data = [
['Test', 'Critical Low', 'Critical High', 'Immediate Action'],
['Sodium', '< 120 mmol/L', '> 160 mmol/L', 'Medical emergency - slow correction\nMax rate 0.5 mEq/L/hr'],
['Potassium', '< 2.5 mmol/L', '> 6.5 mmol/L', 'URGENT ECG; IV KCl (low)\nCalcium gluconate + insulin/glucose (high)'],
['Glucose', '< 40 mg/dL', '> 500 mg/dL', 'IV D50 or glucagon (low)\nInsulin drip + fluids (high - DKA/HHS)'],
['Calcium (total)', '< 6.5 mg/dL', '> 13.0 mg/dL', 'IV calcium gluconate (low)\nIV saline + bisphosphonates (high)'],
['Creatinine', '—', '> 10 mg/dL\n(new/acute)', 'Nephrology consult; consider dialysis\nCheck K⁺, fluid status'],
['Troponin', '—', 'Any elevation\n+ chest pain', 'STEMI protocol: ECG + cath lab\nNon-STEMI: ASA, anticoag, cath'],
['pH (arterial)', '< 7.20', '> 7.60', 'ICU admission; treat underlying cause\nConsider NaHCO₃ if pH < 7.10'],
['Hemoglobin', '< 7.0 g/dL', '—', 'Transfuse if symptomatic\nFind source of blood loss'],
['PT/INR', '—', '> 3.0 (not on\nwarfarin)', 'Vitamin K IV; FFP if bleeding\nCheck liver function'],
]
story.append(ref_table(styles, critical_data, [3.0*cm, 3.0*cm, 3.5*cm, 7.5*cm], colors.HexColor('#B71C1C')))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════
# FINAL PAGE: KEY CLINICAL PRINCIPLES
# ═══════════════════════════════════════════════════════
story.append(section_banner('📋 Key Clinical Principles & Interpretation Rules', DARKGRAY, styles))
story.append(Spacer(1, 0.3*cm))
principles = [
('Never interpret a lab in isolation',
'Always correlate with clinical history, symptoms, physical examination, and other labs. '
'A troponin of 0.1 ng/mL means very different things in a chest-pain patient vs. a CKD patient.'),
('Reference ranges are population-based, not diagnostic cutoffs',
'They represent the middle 95% of healthy adults. 5% of healthy people will have "abnormal" values. '
'Age, sex, ethnicity, medications, and laboratory methods all affect reference ranges.'),
('Trend matters as much as the value',
'A creatinine rising from 0.8 to 1.3 mg/dL is more significant than a stable 1.5 mg/dL. '
'Serial troponins (0h, 3h, 6h) are more informative than a single measurement.'),
('Confirm unexpected critical values before acting',
'Always repeat unexpected critical values (except troponin, potassium in symptomatic patients). '
'Check for sample hemolysis, contamination, and collection errors first.'),
('Understand compensatory changes',
'A low pH + low HCO₃ + low CO₂ = metabolic acidosis with appropriate respiratory compensation. '
'The compensation is expected - it does not mean two separate disorders unless it is excessive.'),
('Multiple abnormalities often cluster in syndromes',
'DKA: high glucose + metabolic acidosis + high anion gap + ketones + high K⁺ (apparent) + high BUN. '
'Nephrotic: low albumin + edema + high cholesterol + proteinuria + normal/low Na.'),
('Pseudo-abnormalities exist',
'Pseudohyponatremia (hyperlipidemia, hyperproteinemia) · Pseudohyperkalemia (hemolysis, thrombocytosis) · '
'Pseudohypoglycemia (high WBC count consuming glucose in tube) · Spurious low creatinine (low muscle mass).'),
]
for title_p, body_p in principles:
story.append(KeepTogether([
Paragraph(f'• {title_p}', styles['SubHeader']),
Paragraph(body_p, styles['Body']),
Spacer(1, 0.2*cm),
]))
story.append(Spacer(1, 0.5*cm))
story.append(HRFlowable(width=W, thickness=1, color=GRAY))
story.append(Spacer(1, 0.3*cm))
sources_text = (
'<b>Sources:</b> Guyton and Hall Textbook of Medical Physiology, 14th Ed (Elsevier) · '
'Goldman-Cecil Medicine, International Edition, 2-Volume Set (Elsevier) · '
'Harrison\'s Principles of Internal Medicine, 22nd Ed (McGraw Hill, 2025) · '
'KDIGO Clinical Practice Guidelines for Acute Kidney Injury (2012) · '
'ADA Standards of Medical Care in Diabetes (2024) · '
'ACC/AHA Guideline on Dyslipidemia Management (2019)'
)
story.append(Paragraph(sources_text, styles['Caption']))
# ── Build ──────────────────────────────────────────────────────────────────
def on_first_page(canvas, doc):
pass
def on_later_pages(canvas, doc):
canvas.saveState()
canvas.setFont('Helvetica', 8)
canvas.setFillColor(GRAY)
canvas.drawString(2*cm, 1.3*cm, 'Clinical Laboratory Interpretation Guide · Orris Clinical Reference')
canvas.drawRightString(A4[0]-2*cm, 1.3*cm, f'Page {doc.page}')
canvas.restoreState()
doc.build(story, onFirstPage=on_first_page, onLaterPages=on_later_pages)
print(f"PDF successfully created: {OUTPUT_PATH}")
if __name__ == '__main__':
build_pdf()