investigations done in general medicine
| Parameter | What it Measures | Clinical Implication |
|---|---|---|
| Hb (Haemoglobin) | Total Hb in blood | Low = anaemia |
| RCC (Red Cell Count) | Number of RBCs | Can be high even with low Hb (e.g. thalassaemia) |
| MCV (Mean Corpuscular Volume) | Size of RBC | Low = microcytic; High = macrocytic |
| MCH (Mean Corpuscular Haemoglobin) | Amount of Hb per cell | Low in iron deficiency |
| MCHC | Hb concentration inside cells | Low in iron deficiency, high in hereditary spherocytosis |
| Reticulocyte count | Young RBCs from bone marrow | High = marrow actively producing RBCs (haemolysis or blood loss recovery) |
| Cell | Normal Range | High (causes) | Low (causes) |
|---|---|---|---|
| Neutrophils | 2-7 x10⁹/L | Bacterial infection, steroids, stress, malignancy | Viral infection, drug toxicity, marrow failure |
| Lymphocytes | 1.5-4 x10⁹/L | Viral infection (EBV, CMV), CLL, TB | HIV, steroids, SLE |
| Eosinophils | 0.04-0.4 x10⁹/L | Parasites, allergy, asthma, Addison's | Steroids |
| Monocytes | 0.2-1.0 x10⁹/L | TB, malaria, inflammation | - |
| Basophils | 0-0.1 x10⁹/L | CML, allergic reactions | - |
| Test | Significance |
|---|---|
| ALT (Alanine Aminotransferase) | Most specific for hepatocyte damage; very high in viral/toxic hepatitis |
| AST (Aspartate Aminotransferase) | Also elevated in hepatitis; rises in myocardial infarction too |
| LDH | Rises in hepatocyte injury and necrosis |
ALT:AST ratio >2 suggests alcoholic liver disease; if AST dominates, think MI or muscle disease.
| Test | Significance |
|---|---|
| ALP (Alkaline Phosphatase) | Rises in biliary obstruction, bone disease, pregnancy |
| GGT (Gamma-Glutamyl Transferase) | Confirms ALP is of hepatic origin; very sensitive to alcohol |
| Bilirubin (total + direct) | High direct = conjugated/obstructive; High indirect = haemolysis or Gilbert's |
| Test | Significance |
|---|---|
| Albumin | Reflects chronic liver synthetic function; falls only when >80% liver is destroyed |
| Total Protein | Follows albumin trends |
| PT / INR | Clotting factors made by liver; prolonged in hepatic failure |
| Ammonia | Rises when >80% liver is destroyed; causes hepatic encephalopathy |
| Test | Normal Range | Interpretation |
|---|---|---|
| Urea (BUN) | 2.5-7.1 mmol/L | Raised in prerenal (dehydration, GI bleed), renal failure; low in liver disease |
| Creatinine | 60-110 µmol/L (male) | Rises when GFR drops significantly; affected by muscle mass |
| eGFR | >60 mL/min/1.73m² | Best routine measure of kidney function; used to stage CKD |
| Uric acid | 200-430 µmol/L | Raised in gout, myeloproliferative disease, renal failure |
100 (or >20 in US units) = Prerenal cause (dehydration, GI bleed, heart failure)
| Electrolyte | Normal | High (causes) | Low (causes) |
|---|---|---|---|
| Sodium (Na⁺) | 135-145 mmol/L | Dehydration, DI, excess NaCl | SIADH, hypothyroidism, heart failure, diarrhoea |
| Potassium (K⁺) | 3.5-5.0 mmol/L | Renal failure, ACEi/ARBs, Addison's, acidosis | Diarrhoea, diuretics, Cushing's, alkalosis |
| Chloride (Cl⁻) | 95-107 mmol/L | Dehydration, renal failure | Vomiting, metabolic alkalosis |
| Bicarbonate (HCO₃⁻) | 22-29 mmol/L | Metabolic alkalosis | Metabolic acidosis, diarrhoea |
| Calcium (Ca²⁺) | 2.2-2.6 mmol/L | Hyperparathyroidism, malignancy, sarcoid, Vit D toxicity | Hypoparathyroidism, Vit D deficiency, malabsorption |
| Phosphate | 0.8-1.5 mmol/L | Renal failure, hypoparathyroidism | Malnutrition, hyperparathyroidism, refeeding syndrome |
| Magnesium | 0.7-1.0 mmol/L | Renal failure | Diarrhoea, alcohol, diuretics, PPIs |
| Test | Interpretation |
|---|---|
| Fasting glucose <6.1 mmol/L | Normal |
| 6.1-6.9 mmol/L | Impaired fasting glucose (pre-diabetes) |
| ≥7.0 mmol/L (on 2 occasions) | Diabetes mellitus |
| Random glucose ≥11.1 mmol/L | Diabetes mellitus (with symptoms) |
| HbA1c ≥48 mmol/mol (6.5%) | Diabetes mellitus |
| HbA1c 42-47 mmol/mol | Pre-diabetes |
| Pattern | TSH | Free T4 | Free T3 | Diagnosis |
|---|---|---|---|---|
| Primary hypothyroidism | High | Low | Low | Hashimoto's, iodine deficiency |
| Subclinical hypothyroidism | High | Normal | Normal | Early Hashimoto's |
| Primary hyperthyroidism | Low/undetectable | High | High | Graves', toxic nodule |
| Subclinical hyperthyroidism | Low | Normal | Normal | Early Graves', excess thyroxine |
| Secondary hypothyroidism | Low/normal | Low | Low | Pituitary failure |
| Test | Uses | Limitations |
|---|---|---|
| CRP (C-Reactive Protein) | Rises within 6-12 hours of inflammation/infection; falls quickly on recovery | Non-specific |
| ESR (Erythrocyte Sedimentation Rate) | Rises more slowly; useful in PMR, myeloma, TB | Affected by age, sex, anaemia, pregnancy |
| WBC | Neutrophilia = bacterial; Lymphocytosis = viral | - |
| Procalcitonin | More specific for bacterial sepsis | Expensive, not universally available |
| Ferritin | Acute phase reactant; also measures iron stores | Very high in haemophagocytic syndrome, Still's disease |
| Test | Normal | Prolonged in |
|---|---|---|
| PT / INR | INR 0.9-1.2 | Warfarin, liver disease, Vit K deficiency, DIC |
| APTT | 26-37 seconds | Heparin, haemophilia A/B, lupus anticoagulant, DIC |
| Fibrinogen | 2-4 g/L | Decreased in DIC, liver failure (consumed) |
| D-dimer | <0.5 mg/L | Raised in VTE, DIC, malignancy, post-surgery (sensitive, not specific) |
Create a quick reference PDF with interpretation flowcharts for each test
ls /home/daytona/workspace/ 2>/dev/null && mkdir -p /home/daytona/workspace/blood-tests-pdf
~/blood-tests-pdf/generate_pdf.py
#!/usr/bin/env python3
"""
Quick Reference PDF: Blood Test Interpretation Flowcharts for General Medicine
Uses ReportLab for PDF generation with flowcharts drawn via canvas graphics.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable,
PageBreak, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus.flowables import Flowable
from reportlab.lib.colors import HexColor
import reportlab.lib.colors as rlcolors
# ── Colour Palette ──────────────────────────────────────────────────────────
DARK_BLUE = HexColor("#1a3a5c")
MED_BLUE = HexColor("#2e6da4")
LIGHT_BLUE = HexColor("#d0e4f5")
TEAL = HexColor("#1a7a6e")
LIGHT_TEAL = HexColor("#c8ece9")
RED = HexColor("#c0392b")
LIGHT_RED = HexColor("#fce8e6")
AMBER = HexColor("#d4850a")
LIGHT_AMBER = HexColor("#fef3e0")
GREEN = HexColor("#1e7e34")
LIGHT_GREEN = HexColor("#d4edda")
PURPLE = HexColor("#6c3483")
LIGHT_PURPLE= HexColor("#e8d5f5")
GREY = HexColor("#5a6a7a")
LIGHT_GREY = HexColor("#f0f4f8")
WHITE = colors.white
BLACK = colors.black
W, H = A4 # 595 x 842 pt
# ── Flowable: Section Banner ─────────────────────────────────────────────────
class SectionBanner(Flowable):
def __init__(self, title, subtitle="", bg=DARK_BLUE, fg=WHITE, width=None, height=28):
super().__init__()
self._title = title
self._subtitle = subtitle
self._bg = bg
self._fg = fg
self._w = width or (W - 40*mm)
self._h = height
self.width = self._w
self.height = self._h + (8 if subtitle else 0)
def draw(self):
c = self.canv
c.setFillColor(self._bg)
c.roundRect(0, 0, self._w, self.height, 4, fill=1, stroke=0)
c.setFillColor(self._fg)
c.setFont("Helvetica-Bold", 13)
y = self.height - 18
c.drawString(10, y, self._title)
if self._subtitle:
c.setFont("Helvetica", 9)
c.drawString(10, y - 13, self._subtitle)
# ── Flowable: Flowchart Box ──────────────────────────────────────────────────
class FlowchartBox(Flowable):
"""
Draws a simple top-down decision flowchart.
nodes = list of dicts:
{"label": str, "type": "start"|"decision"|"result"|"note",
"color": HexColor (optional)}
arrows between consecutive nodes are drawn automatically.
"""
BOX_W = 200
BOX_H = 28
DEC_H = 36
GAP = 18 # vertical gap between boxes
def __init__(self, nodes, width=None):
super().__init__()
self._nodes = nodes
self._cw = width or (W - 40*mm)
# compute total height
total = 0
for n in nodes:
total += (self.DEC_H if n["type"] == "decision" else self.BOX_H) + self.GAP
self.width = self._cw
self.height = total + 10
def _box_color(self, node):
if "color" in node:
return node["color"]
t = node["type"]
if t == "start": return MED_BLUE
if t == "decision": return AMBER
if t == "result": return TEAL
if t == "note": return LIGHT_GREY
return LIGHT_BLUE
def _text_color(self, node):
t = node["type"]
if t in ("start", "result"): return WHITE
if t == "decision": return WHITE
return BLACK
def draw(self):
c = self.canv
cx = self._cw / 2
bw = self.BOX_W
y = self.height - 10 # start from top
for i, node in enumerate(self._nodes):
bh = self.DEC_H if node["type"] == "decision" else self.BOX_H
y -= bh
x = cx - bw / 2
bg = self._box_color(node)
fg = self._text_color(node)
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
if node["type"] == "decision":
# Diamond
mid_x = cx
mid_y = y + bh / 2
c.beginPath()
c.moveTo(mid_x, y + bh) # top
c.lineTo(x + bw, mid_y) # right
c.lineTo(mid_x, y) # bottom
c.lineTo(x, mid_y) # left
c.closePath()
c.drawPath(c._code and c._code or None, fill=1, stroke=1)
# ReportLab path trick
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
pts = [
(mid_x, y + bh),
(x + bw, mid_y),
(mid_x, y),
(x, mid_y),
]
c.beginPath()
c.moveTo(*pts[0])
for pt in pts[1:]:
c.lineTo(*pt)
c.closePath()
c.drawPath(None, fill=1, stroke=1)
elif node["type"] == "start":
c.roundRect(x, y, bw, bh, bh/2, fill=1, stroke=1)
elif node["type"] == "note":
c.setDash(3, 2)
c.rect(x, y, bw, bh, fill=1, stroke=1)
c.setDash()
else:
c.rect(x, y, bw, bh, fill=1, stroke=1)
# Label text
c.setFillColor(fg)
label = node["label"]
font_size = 8.5 if len(label) > 35 else 9.5
c.setFont("Helvetica-Bold" if node["type"] in ("start","decision") else "Helvetica", font_size)
# Simple text wrap: split on newline
lines = label.split("\n")
line_h = font_size * 1.3
total_text_h = len(lines) * line_h
start_y = y + bh/2 + total_text_h/2 - line_h*0.7
for li, ln in enumerate(lines):
c.drawCentredString(cx, start_y - li*line_h, ln)
# Arrow down
if i < len(self._nodes) - 1:
ax = cx
ay_top = y
ay_bottom = y - self.GAP
c.setStrokeColor(GREY)
c.setLineWidth(1.2)
c.line(ax, ay_top, ax, ay_bottom + 5)
# arrowhead
c.setFillColor(GREY)
c.beginPath()
c.moveTo(ax, ay_bottom)
c.lineTo(ax - 4, ay_bottom + 7)
c.lineTo(ax + 4, ay_bottom + 7)
c.closePath()
c.drawPath(None, fill=1, stroke=0)
y -= self.GAP
# ── Flowable: Side-by-Side Flowcharts ────────────────────────────────────────
class DualFlowchart(Flowable):
"""Two narrow flowcharts side by side."""
BOX_W = 110
BOX_H = 26
DEC_H = 34
GAP = 16
MARGIN = 8
def __init__(self, left_nodes, right_nodes, left_title="", right_title="", width=None):
super().__init__()
self._ln = left_nodes
self._rn = right_nodes
self._lt = left_title
self._rt = right_title
self._cw = width or (W - 40*mm)
def calc_h(nodes):
t = 30 # title
for n in nodes:
t += (self.DEC_H if n["type"]=="decision" else self.BOX_H) + self.GAP
return t + 10
self.width = self._cw
self.height = max(calc_h(left_nodes), calc_h(right_nodes))
def _box_color(self, node):
if "color" in node: return node["color"]
t = node["type"]
if t == "start": return MED_BLUE
if t == "decision": return AMBER
if t == "result": return TEAL
if t == "note": return LIGHT_GREY
return LIGHT_BLUE
def _text_color(self, node):
t = node["type"]
if t in ("start", "result", "decision"): return WHITE
return BLACK
def _draw_column(self, c, nodes, cx, start_y):
bw = self.BOX_W
y = start_y
for i, node in enumerate(nodes):
bh = self.DEC_H if node["type"] == "decision" else self.BOX_H
y -= bh
x = cx - bw/2
bg = self._box_color(node)
fg = self._text_color(node)
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
if node["type"] == "decision":
mid_x = cx; mid_y = y + bh/2
c.setFillColor(bg)
c.beginPath()
c.moveTo(mid_x, y+bh); c.lineTo(x+bw, mid_y)
c.lineTo(mid_x, y); c.lineTo(x, mid_y)
c.closePath()
c.drawPath(None, fill=1, stroke=1)
elif node["type"] == "start":
c.roundRect(x, y, bw, bh, bh/2, fill=1, stroke=1)
elif node["type"] == "note":
c.setDash(3,2); c.rect(x,y,bw,bh,fill=1,stroke=1); c.setDash()
else:
c.rect(x, y, bw, bh, fill=1, stroke=1)
c.setFillColor(fg)
label = node["label"]
fs = 7.5 if len(label) > 30 else 8.5
c.setFont("Helvetica-Bold" if node["type"] in ("start","decision") else "Helvetica", fs)
lines = label.split("\n")
lh = fs * 1.3
tot = len(lines)*lh
sy = y + bh/2 + tot/2 - lh*0.7
for li, ln in enumerate(lines):
c.drawCentredString(cx, sy - li*lh, ln)
if i < len(nodes)-1:
c.setStrokeColor(GREY); c.setLineWidth(1)
c.line(cx, y, cx, y - self.GAP + 5)
c.setFillColor(GREY)
c.beginPath()
c.moveTo(cx, y-self.GAP)
c.lineTo(cx-3, y-self.GAP+6); c.lineTo(cx+3, y-self.GAP+6)
c.closePath(); c.drawPath(None, fill=1, stroke=0)
y -= self.GAP
def draw(self):
c = self.canv
half = self._cw / 2
lcx = half / 2
rcx = half + half / 2
# Column titles
c.setFont("Helvetica-Bold", 9)
c.setFillColor(DARK_BLUE)
if self._lt:
c.drawCentredString(lcx, self.height - 14, self._lt)
if self._rt:
c.drawCentredString(rcx, self.height - 14, self._rt)
# Divider
c.setStrokeColor(HexColor("#cccccc"))
c.setLineWidth(0.5)
c.line(half, 0, half, self.height)
self._draw_column(c, self._ln, lcx, self.height - 22)
self._draw_column(c, self._rn, rcx, self.height - 22)
# ── Helper to build coloured tables ─────────────────────────────────────────
def make_table(data, col_widths, header_bg=MED_BLUE, alt_bg=LIGHT_BLUE,
header_fg=WHITE, font_size=8.5):
ts = TableStyle([
("BACKGROUND", (0,0), (-1,0), header_bg),
("TEXTCOLOR", (0,0), (-1,0), header_fg),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), font_size),
("FONTNAME", (0,1), (-1,-1), "Helvetica"),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, alt_bg]),
("GRID", (0,0), (-1,-1), 0.35, HexColor("#aaaaaa")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING",(0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0), (-1,-1), 4),
])
t = Table(data, colWidths=col_widths)
t.setStyle(ts)
return t
# ── Styles ───────────────────────────────────────────────────────────────────
def get_styles():
ss = getSampleStyleSheet()
styles = {
"title": ParagraphStyle("title", parent=ss["Normal"],
fontSize=22, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER, spaceAfter=4),
"subtitle": ParagraphStyle("subtitle", parent=ss["Normal"],
fontSize=12, fontName="Helvetica",
textColor=LIGHT_BLUE, alignment=TA_CENTER, spaceAfter=2),
"h2": ParagraphStyle("h2", parent=ss["Normal"],
fontSize=11, fontName="Helvetica-Bold",
textColor=DARK_BLUE, spaceBefore=8, spaceAfter=4),
"body": ParagraphStyle("body", parent=ss["Normal"],
fontSize=9, fontName="Helvetica",
textColor=HexColor("#333333"), spaceAfter=4, leading=13),
"note": ParagraphStyle("note", parent=ss["Normal"],
fontSize=8, fontName="Helvetica-Oblique",
textColor=GREY, spaceAfter=3, leftIndent=6),
"small": ParagraphStyle("small", parent=ss["Normal"],
fontSize=7.5, fontName="Helvetica", textColor=GREY,
alignment=TA_CENTER),
"legend_title": ParagraphStyle("legend_title", parent=ss["Normal"],
fontSize=8.5, fontName="Helvetica-Bold", textColor=DARK_BLUE),
}
return styles
# ── Cover Page ───────────────────────────────────────────────────────────────
class CoverPage(Flowable):
def __init__(self, width=None, height=None):
super().__init__()
self.width = width or W - 40*mm
self.height = height or H - 40*mm
def draw(self):
c = self.canv
w = self.width; h = self.height
# Background gradient simulation (two rects)
c.setFillColor(DARK_BLUE)
c.rect(0, 0, w, h, fill=1, stroke=0)
c.setFillColor(MED_BLUE)
c.rect(0, h*0.55, w, h*0.45, fill=1, stroke=0)
# Decorative circles
c.setFillColor(HexColor("#ffffff15"))
c.circle(w*0.85, h*0.85, 90, fill=1, stroke=0)
c.circle(w*0.1, h*0.15, 60, fill=1, stroke=0)
c.setFillColor(HexColor("#ffffff08"))
c.circle(w*0.5, h*0.5, 140, fill=1, stroke=0)
# Title
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 28)
c.drawCentredString(w/2, h*0.72, "Blood Test Interpretation")
c.setFont("Helvetica-Bold", 22)
c.drawCentredString(w/2, h*0.65, "Quick Reference Guide")
# Subtitle bar
c.setFillColor(TEAL)
c.roundRect(w*0.1, h*0.56, w*0.8, 26, 4, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica", 11)
c.drawCentredString(w/2, h*0.564, "Interpretation Flowcharts • Normal Ranges • Clinical Patterns")
# Tests listed
tests = ["CBC / FBC", "Liver Function Tests", "Renal Function Tests",
"Electrolytes & Anion Gap", "Blood Glucose & HbA1c",
"Thyroid Function Tests", "Inflammatory Markers", "Coagulation Screen"]
c.setFont("Helvetica-Bold", 9)
c.setFillColor(LIGHT_BLUE)
c.drawCentredString(w/2, h*0.50, "TESTS COVERED")
c.setFont("Helvetica", 9)
c.setFillColor(WHITE)
for i, t in enumerate(tests):
col = 0 if i < 4 else 1
row = i % 4
cx = w*0.28 + col*w*0.44
c.drawCentredString(cx, h*0.465 - row*16, "✦ " + t)
# Footer
c.setFillColor(LIGHT_BLUE)
c.setFont("Helvetica", 8)
c.drawCentredString(w/2, 18, "For educational purposes | Always interpret in full clinical context | June 2026")
# ── Legend Flowable ──────────────────────────────────────────────────────────
class LegendBox(Flowable):
def __init__(self, width=None):
super().__init__()
self.width = width or (W - 40*mm)
self.height = 26
def draw(self):
c = self.canv; w = self.width
c.setFillColor(LIGHT_GREY)
c.roundRect(0, 0, w, self.height, 3, fill=1, stroke=0)
items = [
(MED_BLUE, WHITE, "Start / Test"),
(AMBER, WHITE, "Decision"),
(TEAL, WHITE, "Result / Diagnosis"),
(LIGHT_GREY, BLACK, "Note / Action"),
]
seg = w / len(items)
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(DARK_BLUE)
c.drawString(6, self.height - 11, "LEGEND:")
for i, (bg, fg, label) in enumerate(items):
sx = 75 + i * seg * 0.9
c.setFillColor(bg)
c.roundRect(sx, 6, 12, 12, 2, fill=1, stroke=0)
c.setFillColor(BLACK)
c.setFont("Helvetica", 7.5)
c.drawString(sx + 15, 10, label)
# ── Main PDF Builder ─────────────────────────────────────────────────────────
def build_pdf(path):
doc = SimpleDocTemplate(
path,
pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm,
topMargin=18*mm, bottomMargin=18*mm,
title="Blood Test Interpretation Quick Reference",
author="General Medicine Reference",
)
S = get_styles()
PW = W - 40*mm # printable width = 515 pt
story = []
# ── COVER ────────────────────────────────────────────────────────────────
story.append(CoverPage(PW, H - 36*mm))
story.append(PageBreak())
# ── PAGE HEADER helper ───────────────────────────────────────────────────
def section(title, subtitle="", color=DARK_BLUE):
story.append(SectionBanner(title, subtitle, bg=color, width=PW))
story.append(Spacer(1, 5))
def legend():
story.append(LegendBox(PW))
story.append(Spacer(1, 6))
# ════════════════════════════════════════════════════════════════════════
# 1. FULL BLOOD COUNT
# ════════════════════════════════════════════════════════════════════════
section("1. Full Blood Count (FBC / CBC)",
"Haemoglobin • Red Cell Indices • White Cell Differential • Platelets", MED_BLUE)
# --- Haemoglobin / Anaemia flowchart ---
story.append(Paragraph("1a. Haemoglobin — Anaemia Classification", S["h2"]))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Check Haemoglobin (Hb)"},
{"type":"decision", "label":"Hb Low?\n(M <130 g/L, F <120 g/L)"},
{"type":"decision", "label":"Check MCV"},
{"type":"note", "label":"Low MCV (<80 fL) → Microcytic\nNormal MCV (80-100 fL) → Normocytic\nHigh MCV (>100 fL) → Macrocytic"},
], width=PW))
story.append(Spacer(1, 6))
# Side-by-side: Microcytic vs Macrocytic
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Microcytic Anaemia\n(Low MCV <80 fL)"},
{"type":"decision","label":"Check serum ferritin"},
{"type":"result", "label":"Low ferritin → Iron Deficiency\nAnaemia", "color":RED},
{"type":"result", "label":"Normal/High ferritin →\nThalassaemia / ACD /\nSideroblastic anaemia", "color":TEAL},
{"type":"note", "label":"Iron deficiency: ↓Fe, ↓ferritin,\n↑TIBC. Check for blood loss."},
],
right_nodes=[
{"type":"start", "label":"Macrocytic Anaemia\n(High MCV >100 fL)"},
{"type":"decision","label":"Check B12 & Folate"},
{"type":"result", "label":"Low B12/Folate →\nMegaloblastic anaemia", "color":RED},
{"type":"result", "label":"Normal B12/Folate →\nAlcohol / Liver disease /\nHypothyroidism / Drugs", "color":TEAL},
{"type":"note", "label":"Drugs: methotrexate,\nhydroxyurea, azathioprine"},
],
left_title="MICROCYTIC", right_title="MACROCYTIC", width=PW,
))
story.append(Spacer(1, 6))
# Normocytic
story.append(Paragraph("Normocytic Anaemia (MCV 80-100 fL)", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Normocytic Anaemia"},
{"type":"decision", "label":"Check Reticulocyte Count"},
{"type":"note", "label":"High reticulocytes → Haemolysis or Acute blood loss\nLow reticulocytes → Marrow failure, CKD, ACD"},
{"type":"result", "label":"Haemolysis: ↑LDH, ↑indirect bilirubin,\n↓haptoglobin, +ve direct Coombs test"},
], width=PW))
story.append(Spacer(1, 8))
# WBC Table
story.append(Paragraph("1b. White Cell Count — Differential Interpretation", S["h2"]))
wbc_data = [
["Cell Type", "Normal Range", "Elevated (causes)", "Decreased (causes)"],
["Neutrophils", "2.0–7.0 ×10⁹/L", "Bacterial infection, steroids,\ntrauma, malignancy","Viral infection, drug toxicity,\napplastic anaemia"],
["Lymphocytes", "1.5–4.0 ×10⁹/L", "Viral infection (EBV, CMV),\nCLL, TB", "HIV, steroids, SLE,\npost-chemotherapy"],
["Eosinophils", "0.04–0.4 ×10⁹/L", "Parasites, allergy,\nasthma, malignancy", "Steroids, acute infection"],
["Monocytes", "0.2–1.0 ×10⁹/L", "TB, malaria, monocytic\nleukaemia, inflammation", "Hairy cell leukaemia"],
["Basophils", "0–0.1 ×10⁹/L", "CML, allergic reactions,\nmyeloproliferative", "Steroids, hyperthyroidism"],
]
story.append(make_table(wbc_data, [75, 70, 190, 180], font_size=8))
story.append(Spacer(1, 6))
# Platelets flowchart
story.append(Paragraph("1c. Platelet Count Interpretation", S["h2"]))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Check Platelet Count (normal 150–400 ×10⁹/L)"},
{"type":"decision", "label":"Low (<150 ×10⁹/L) or High (>400 ×10⁹/L)?"},
{"type":"note", "label":"LOW: ITP, heparin-induced (HIT), DIC, hypersplenism,\nbone marrow failure, drug-induced\nHIGH: Reactive (infection, iron deficiency, post-surgery, inflammation)\nor Essential Thrombocythaemia"},
{"type":"result", "label":"If <50×10⁹/L → Bleeding risk; <20×10⁹/L → Spontaneous bleeding risk\nIf HIT suspected: stop heparin immediately, use alternative anticoagulant"},
], width=PW))
story.append(Spacer(1, 6))
# Pancytopenia box
story.append(Paragraph("Pancytopenia (Low RBC + WBC + Platelets)", S["h2"]))
pan_data = [
["Mechanism", "Causes"],
["Reduced marrow\nproduction", "Aplastic anaemia, acute leukaemia, MDS,\nmyeloma, lymphoma infiltration, TB, megaloblastic anaemia"],
["Increased peripheral\ndestruction", "Hypersplenism, haemophagocytic syndrome,\nParoxysmal nocturnal haemoglobinuria (PNH)"],
]
story.append(make_table(pan_data, [120, 395], header_bg=RED, alt_bg=LIGHT_RED, font_size=8.5))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 2. LIVER FUNCTION TESTS
# ════════════════════════════════════════════════════════════════════════
section("2. Liver Function Tests (LFTs)", "ALT • AST • ALP • GGT • Bilirubin • Albumin • PT", TEAL)
legend()
story.append(FlowchartBox([
{"type":"start", "label":"LFT Results Abnormal"},
{"type":"decision", "label":"Pattern of abnormality?"},
{"type":"note", "label":"↑↑ ALT/AST (>10× ULN) → Hepatocellular pattern\n↑↑ ALP/GGT, ↑ Bilirubin → Cholestatic/Obstructive pattern\nBoth raised → Mixed pattern"},
], width=PW))
story.append(Spacer(1, 5))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hepatocellular Pattern\n(↑↑ ALT, AST)"},
{"type":"decision","label":"AST:ALT ratio?"},
{"type":"result", "label":"Ratio >2:1 →\nAlcoholic hepatitis\n(AST rarely >300)", "color":RED},
{"type":"result", "label":"Ratio <1 (ALT dominant) →\nViral / toxic hepatitis,\nNAFLD", "color":TEAL},
{"type":"note", "label":"Very high ALT (>1000) → Ischaemic\nhepatitis, viral, paracetamol OD"},
],
right_nodes=[
{"type":"start", "label":"Cholestatic Pattern\n(↑↑ ALP, GGT)"},
{"type":"decision","label":"Is ALP isolated?"},
{"type":"result", "label":"ALP ↑ + GGT normal →\nBone source (Paget's,\nbone mets, pregnancy)", "color":AMBER},
{"type":"result", "label":"ALP ↑ + GGT ↑ →\nBiliary obstruction / PBC /\nPSC / cholestasis", "color":TEAL},
{"type":"note", "label":"Biliary obstruction: USS first\nto check bile ducts"},
],
left_title="HEPATOCELLULAR", right_title="CHOLESTATIC", width=PW,
))
story.append(Spacer(1, 6))
# Bilirubin flowchart
story.append(Paragraph("2b. Bilirubin — Conjugated vs Unconjugated", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Raised Total Bilirubin (Jaundice >35 µmol/L)"},
{"type":"decision", "label":"Is the bilirubin mainly conjugated (direct) or unconjugated (indirect)?"},
{"type":"note", "label":"Mainly UNCONJUGATED (indirect) → Pre-hepatic:\n Haemolysis, ineffective erythropoiesis, Gilbert's syndrome\nMainly CONJUGATED (direct) → Hepatic or Post-hepatic:\n Hepatitis, cirrhosis, biliary obstruction, cholangitis"},
{"type":"result", "label":"Gilbert's syndrome: isolated mild unconjugated hyperbilirubinaemia,\nfasting/illness triggered, no treatment needed"},
], width=PW))
story.append(Spacer(1, 6))
# LFT summary table
story.append(Paragraph("2c. LFT Pattern Summary Table", S["h2"]))
lft_data = [
["Condition", "ALT/AST", "ALP", "GGT", "Bilirubin", "Albumin", "PT"],
["Acute hepatitis", "↑↑↑", "↑", "↑", "↑ direct", "Normal", "↑ if severe"],
["Alcoholic hepatitis","↑↑ AST>ALT","↑", "↑↑↑", "↑", "↓", "↑"],
["Biliary obstruction","↑", "↑↑↑", "↑↑", "↑↑ direct", "Normal", "Normal"],
["Cirrhosis", "↑/Normal", "↑", "↑", "↑ late", "↓↓", "↑↑"],
["NAFLD", "↑ ALT>AST", "↑", "↑", "Normal", "Normal", "Normal"],
["Haemolysis", "Normal", "Normal","Normal","↑ indirect", "Normal", "Normal"],
["Gilbert's syndrome","Normal", "Normal","Normal","↑ indirect", "Normal", "Normal"],
]
story.append(make_table(lft_data, [115, 55, 45, 45, 80, 60, 70], header_bg=TEAL, alt_bg=LIGHT_TEAL, font_size=8))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 3. RENAL FUNCTION TESTS
# ════════════════════════════════════════════════════════════════════════
section("3. Renal Function Tests (RFTs)", "Urea • Creatinine • eGFR • Urine Analysis", MED_BLUE)
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Raised Creatinine / Urea"},
{"type":"decision", "label":"Calculate Urea:Creatinine Ratio\n(Urea mmol/L ÷ Creatinine mmol/L)"},
{"type":"note", "label":"Ratio >100 (SI units) → Pre-renal AKI\n (Dehydration, heart failure, GI bleed, sepsis)\nRatio 40–100 → Intrinsic renal disease or post-renal\nRatio <40 → Low urea production (liver failure, low protein intake)"},
{"type":"decision", "label":"Is this acute (AKI) or chronic (CKD)?"},
{"type":"result", "label":"AKI: Acute rise in creatinine ≥26 µmol/L in 48h or\n≥1.5× baseline in 7 days. Check fluid status, urine output.\nCKD: eGFR <60 mL/min/1.73m² for >3 months. Stage 1–5."},
], width=PW))
story.append(Spacer(1, 6))
# CKD staging
story.append(Paragraph("3b. CKD Staging by eGFR", S["h2"]))
ckd_data = [
["CKD Stage", "eGFR (mL/min/1.73m²)", "Description", "Action"],
["Stage 1", "≥90 (with markers)", "Normal or high", "Treat risk factors"],
["Stage 2", "60–89", "Mildly reduced", "Monitor annually"],
["Stage 3a", "45–59", "Mildly-moderately ↓", "Monitor 6-monthly, nephrology referral if progressing"],
["Stage 3b", "30–44", "Moderately reduced", "Nephrology referral"],
["Stage 4", "15–29", "Severely reduced", "Prepare for RRT, nephrology"],
["Stage 5", "<15", "Kidney failure", "Renal replacement therapy (dialysis/transplant)"],
]
story.append(make_table(ckd_data, [60, 90, 120, 245], header_bg=MED_BLUE, alt_bg=LIGHT_BLUE, font_size=8))
story.append(Spacer(1, 6))
# AKI causes
story.append(Paragraph("3c. AKI Classification", S["h2"]))
aki_data = [
["Type", "Mechanism", "Common Causes", "Urine Na"],
["Pre-renal", "↓ renal perfusion", "Dehydration, haemorrhage, HF, sepsis", "<20 mmol/L"],
["Intrinsic", "Direct renal damage", "ATN (ischaemia, nephrotoxins), GN,\nvasculitis, myoglobinuria", ">40 mmol/L"],
["Post-renal", "Obstruction", "BPH, calculi, malignancy,\nretroperitoneal fibrosis", ">40 mmol/L"],
]
story.append(make_table(aki_data, [65, 100, 210, 70], header_bg=RED, alt_bg=LIGHT_RED, font_size=8))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 4. ELECTROLYTES
# ════════════════════════════════════════════════════════════════════════
section("4. Electrolytes & Anion Gap", "Na • K • Cl • HCO₃ • Ca • Mg • Phosphate", HexColor("#5b2c6f"))
legend()
# Sodium flowchart
story.append(Paragraph("4a. Sodium (Na⁺) — Hypo & Hypernatraemia", S["h2"]))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hyponatraemia\nNa⁺ <135 mmol/L"},
{"type":"decision","label":"Assess fluid status"},
{"type":"result", "label":"Hypovolaemic: vomiting,\ndiarrhoea, diuretics,\nAdrenal insufficiency", "color":RED},
{"type":"result", "label":"Euvolaemic: SIADH,\nhypothyroidism, psychogenic\npolydipsia", "color":TEAL},
{"type":"result", "label":"Hypervolaemic: HF, cirrhosis,\nnephrotic syndrome", "color":AMBER},
],
right_nodes=[
{"type":"start", "label":"Hypernatraemia\nNa⁺ >145 mmol/L"},
{"type":"decision","label":"Water loss or Na gain?"},
{"type":"result", "label":"Water loss: Dehydration,\nDiabetes insipidus,\nexcessive sweating", "color":RED},
{"type":"result", "label":"Na gain: Excess NaCl,\nhypertonic saline,\nCushing's syndrome", "color":TEAL},
{"type":"note", "label":"Correct slowly:\n≤10–12 mmol/L per 24h\nto avoid osmotic demyelination"},
],
left_title="HYPONATRAEMIA", right_title="HYPERNATRAEMIA", width=PW,
))
story.append(Spacer(1, 6))
# Potassium flowchart
story.append(Paragraph("4b. Potassium (K⁺)", S["h2"]))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hypokalaemia\nK⁺ <3.5 mmol/L"},
{"type":"result", "label":"GI loss: vomiting, diarrhoea\nRenal loss: diuretics, Cushing's\nAlkalosis, low Mg, insulin excess", "color":RED},
{"type":"note", "label":"ECG: U waves, flat T waves,\nlong QT. Risk of arrhythmia."},
{"type":"result", "label":"Replace K⁺ orally or IV.\nAlways replace Mg concurrently.", "color":GREEN},
],
right_nodes=[
{"type":"start", "label":"Hyperkalaemia\nK⁺ >5.0 mmol/L"},
{"type":"result", "label":"Renal failure, ACEi/ARBs/\nspironolactone, Addison's,\nacidosis, rhabdomyolysis", "color":RED},
{"type":"note", "label":"ECG: Tall T waves, widened QRS,\nsine wave. EMERGENCY if K>6.5"},
{"type":"result", "label":"URGENT: IV calcium gluconate\n(membrane stabilisation),\nthen insulin/dextrose", "color":GREEN},
],
left_title="HYPOKALAEMIA", right_title="HYPERKALAEMIA", width=PW,
))
story.append(Spacer(1, 6))
# Anion Gap
story.append(Paragraph("4c. Anion Gap & Metabolic Acidosis", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Metabolic Acidosis (↓HCO₃⁻, ↓pH)"},
{"type":"decision", "label":"Calculate Anion Gap = Na − (Cl + HCO₃) Normal: 8–12 mmol/L"},
{"type":"note", "label":"HIGH ANION GAP (>12): MUDPILES\n Methanol, Uraemia, DKA, Propylene glycol, Isoniazid/Iron,\n Lactic acidosis, Ethylene glycol, Salicylates\nNORMAL ANION GAP (Hyperchloraemic): HARD-UP\n Hyperalimentation, Addison's, RTA, Diarrhoea, Ureteroenteric fistula, Pancreatic fistula"},
{"type":"result", "label":"Confirm with ABG / VBG for pH, pCO₂, HCO₃, lactate"},
], width=PW))
story.append(Spacer(1, 6))
# Calcium table
story.append(Paragraph("4d. Calcium — Corrected Ca = Measured Ca + 0.02 × (40 − Albumin)", S["h2"]))
ca_data = [
["Condition", "Ca²⁺", "PTH", "PO₄", "Common Causes"],
["Primary hyperPTH", "↑", "↑↑", "↓", "Parathyroid adenoma (most common)"],
["Malignancy", "↑", "↓", "↑/N", "PTHrP secretion, bone mets, myeloma"],
["Sarcoidosis/Vit D","↑", "↓", "↑", "Excess Vit D, granulomatous disease"],
["HypoparaTH", "↓", "↓", "↑", "Post-surgery, autoimmune"],
["Vit D deficiency", "↓/N", "↑", "↓", "Malabsorption, lack of sunlight, CKD"],
["CKD", "↓", "↑", "↑", "Renal osteodystrophy"],
]
story.append(make_table(ca_data, [120, 40, 40, 40, 275], header_bg=PURPLE, alt_bg=LIGHT_PURPLE, font_size=8))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 5. BLOOD GLUCOSE & HbA1c
# ════════════════════════════════════════════════════════════════════════
section("5. Blood Glucose & HbA1c", "Fasting glucose • Random glucose • OGTT • HbA1c", HexColor("#1a5c3a"))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Assess Blood Glucose"},
{"type":"decision", "label":"Is patient symptomatic?\n(polyuria, polydipsia, unexplained weight loss)"},
{"type":"note", "label":"SYMPTOMATIC: ONE abnormal test is sufficient to diagnose diabetes\nASYMPTOMATIC: TWO abnormal results on separate occasions required"},
{"type":"decision", "label":"Which test result?"},
{"type":"note", "label":"Fasting glucose ≥7.0 mmol/L → Diabetes\nRandom glucose ≥11.1 mmol/L → Diabetes\nHbA1c ≥48 mmol/mol (6.5%) → Diabetes\nFasting glucose 6.1–6.9 mmol/L → Impaired fasting glucose (IFG)\nHbA1c 42–47 mmol/mol (6.0–6.4%) → Pre-diabetes"},
{"type":"result", "label":"2-hour OGTT (75g) ≥11.1 mmol/L → Diabetes\n2-hour OGTT 7.8–11.0 mmol/L → Impaired glucose tolerance (IGT)"},
], width=PW))
story.append(Spacer(1, 6))
story.append(Paragraph("HbA1c as monitoring tool (already diagnosed diabetic)", S["h2"]))
hba_data = [
["HbA1c", "mmol/mol", "Interpretation", "Action"],
["<48 (6.5%)", "<48", "Good glycaemic control", "Continue current regimen"],
["48–58 (6.5–7.5%)","48–58", "Target range (most T2DM)", "Reinforce lifestyle, review medication"],
["58–75 (7.5–9%)", "58–75", "Sub-optimal control", "Intensify therapy, dietitian referral"],
[">75 (>9%)", ">75", "Poor control, high complication risk", "Urgent review, consider insulin"],
]
story.append(make_table(hba_data, [80, 60, 200, 175], header_bg=GREEN, alt_bg=LIGHT_GREEN, font_size=8))
story.append(Spacer(1, 6))
story.append(Paragraph("Hypoglycaemia (<3.9 mmol/L) — Causes", S["h2"]))
hypo_data = [
["Category", "Common Causes"],
["Drugs", "Insulin, sulphonylureas (glibenclamide, gliclazide), alcohol, quinine"],
["Endocrine", "Addison's disease, hypopituitarism, insulinoma"],
["Hepatic", "Acute liver failure, alcohol toxicity, fatty liver (Reye's)"],
["Other", "Starvation, sepsis, critical illness, post-gastric bypass"],
]
story.append(make_table(hypo_data, [100, 415], header_bg=GREEN, alt_bg=LIGHT_GREEN, font_size=8.5))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 6. THYROID FUNCTION TESTS
# ════════════════════════════════════════════════════════════════════════
section("6. Thyroid Function Tests (TFTs)", "TSH • Free T4 • Free T3", HexColor("#17618a"))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Check TSH (best initial screening test)"},
{"type":"decision", "label":"TSH Level?"},
{"type":"note", "label":"TSH HIGH → Check Free T4\nTSH LOW → Check Free T4 & Free T3\nTSH NORMAL → Euthyroid (unless pituitary disease suspected)"},
], width=PW))
story.append(Spacer(1, 5))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"TSH High\n(>4.5 mU/L)"},
{"type":"decision","label":"Free T4?"},
{"type":"result", "label":"fT4 LOW → Primary\nHypothyroidism\n(Hashimoto's, iodine def.)", "color":RED},
{"type":"result", "label":"fT4 NORMAL → Subclinical\nHypothyroidism\n(early / borderline)", "color":AMBER},
{"type":"note", "label":"Check TPO antibodies.\nTreat if TSH>10 or symptomatic"},
],
right_nodes=[
{"type":"start", "label":"TSH Low\n(<0.3 mU/L)"},
{"type":"decision","label":"Free T4 / Free T3?"},
{"type":"result", "label":"fT4/fT3 HIGH → Primary\nHyperthyroidism\n(Graves', toxic nodule)", "color":RED},
{"type":"result", "label":"fT4/fT3 NORMAL → Subclinical\nHyperthyroidism\n(early Graves' or excess Levo)", "color":AMBER},
{"type":"note", "label":"Check TSH receptor antibodies\nfor Graves' disease"},
],
left_title="ELEVATED TSH", right_title="SUPPRESSED TSH", width=PW,
))
story.append(Spacer(1, 6))
tft_data = [
["Pattern", "TSH", "fT4", "fT3", "Diagnosis"],
["Primary Hypothyroid", "↑↑", "↓", "↓", "Hashimoto's, iodine deficiency, post-thyroidectomy"],
["Subclinical Hypothyroid","↑", "Normal", "Normal", "Early Hashimoto's, drug-induced"],
["Primary Hyperthyroid","↓↓", "↑", "↑", "Graves' disease, toxic nodule, toxic MNG"],
["Subclinical Hyperthyroid","↓", "Normal", "Normal", "Early Graves', excessive levothyroxine"],
["Secondary Hypothyroid","↓/N", "↓", "↓", "Pituitary failure, Sheehan's syndrome"],
["Sick Euthyroid", "Low/N", "↓", "↓↓", "Critical illness — low T3 syndrome; do not treat"],
]
story.append(make_table(tft_data, [115, 40, 40, 40, 280], header_bg=MED_BLUE, alt_bg=LIGHT_BLUE, font_size=8))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 7. INFLAMMATORY MARKERS
# ════════════════════════════════════════════════════════════════════════
section("7. Inflammatory Markers", "CRP • ESR • WBC • Procalcitonin • Ferritin", HexColor("#922b21"))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Raised Inflammatory Markers"},
{"type":"decision", "label":"CRP level?"},
{"type":"note", "label":"CRP 10–100 mg/L → Mild infection, autoimmune disease\nCRP 100–500 mg/L → Significant bacterial infection, surgery, trauma\nCRP >500 mg/L → Severe sepsis, burns, major surgery"},
{"type":"decision", "label":"Is ESR also raised?"},
{"type":"note", "label":"High ESR + normal CRP → Paraproteinaemia (myeloma), SLE, anaemia\nHigh ESR + high CRP → Bacterial infection, vasculitis, PMR, RA\nHigh CRP + normal ESR → Viral infection early, mild inflammation"},
{"type":"result", "label":"If CRP >100 and rising despite antibiotics → Consider resistant organism,\nloculated collection, fungal infection or non-infectious cause"},
], width=PW))
story.append(Spacer(1, 6))
inf_data = [
["Marker", "Normal", "Kinetics", "Key Uses", "Limitations"],
["CRP", "<5 mg/L", "Rises 6–12h,\nfalls fast","Monitors infection & treatment response","Non-specific"],
["ESR", "Age/sex dep.","Rises slowly\n(days)", "PMR, myeloma, TB screening", "Affected by anaemia,\npregnancy, age"],
["Procalcitonin", "<0.1 µg/L", "Rises 3–4h", "Bacterial sepsis (>0.5 = likely;\n>2 = probable)","Expensive, not universal"],
["Ferritin", "20–300 µg/L","Acute phase\nreactant", "Very high (>10,000) → HLH, Still's\ndisease, macrophage activation","Iron stores unreliable\nin inflammation"],
["Fibrinogen", "2–4 g/L", "Acute phase", "Rises in inflammation,\ndecreases in DIC","Affected by liver disease"],
]
story.append(make_table(inf_data, [75, 65, 70, 175, 130], header_bg=RED, alt_bg=LIGHT_RED, font_size=8))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# 8. COAGULATION SCREEN
# ════════════════════════════════════════════════════════════════════════
section("8. Coagulation Screen", "PT/INR • APTT • Fibrinogen • D-Dimer • Thrombin Time", HexColor("#1c4587"))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Abnormal Coagulation Screening"},
{"type":"decision", "label":"Is PT (INR) prolonged or APTT prolonged or both?"},
{"type":"note", "label":"Only PT↑ (INR↑) → Extrinsic pathway: Warfarin, Vit K deficiency,\n liver disease (early), Factor VII deficiency\nOnly APTT↑ → Intrinsic pathway: Heparin, Haemophilia A (↓FVIII),\n Haemophilia B (↓FIX), Lupus anticoagulant, vWD\nBoth PT↑ + APTT↑ → DIC, severe liver disease, massive transfusion,\n direct thrombin inhibitors (dabigatran), common pathway defect"},
{"type":"decision", "label":"If both prolonged: check fibrinogen & D-dimer"},
{"type":"result", "label":"Low fibrinogen + ↑D-dimer + thrombocytopenia → DIC (diagnose & treat cause urgently)\nNormal fibrinogen + ↑PT/APTT → Liver disease or anticoagulant effect"},
], width=PW))
story.append(Spacer(1, 6))
coag_data = [
["Test", "Normal Range", "Prolonged in", "Shortened in"],
["PT / INR", "INR 0.9–1.2\n11–14 sec","Warfarin, Vit K deficiency,\nliver disease, DIC, Factor VII def.","Hypercoagulable states\n(not clinically useful)"],
["APTT", "26–37 sec", "Heparin, haemophilia A/B,\nlupus anticoagulant, DIC","—"],
["Fibrinogen", "2.0–4.0 g/L", "Consumed in DIC, severe liver\nfailure, massive transfusion","Acute phase reactant\n(elevated in inflammation)"],
["D-Dimer", "<0.5 mg/L FEU", "DVT/PE, DIC, malignancy,\npost-surgery, sepsis","Not applicable\n(use for exclusion)"],
["Thrombin Time", "14–16 sec", "Heparin effect, hypofibrinogenaemia,\ndabigatran, paraproteinaemia","—"],
]
story.append(make_table(coag_data, [75, 75, 210, 155], header_bg=DARK_BLUE, alt_bg=LIGHT_BLUE, font_size=8))
story.append(Spacer(1, 6))
story.append(Paragraph("DIC Diagnostic Criteria (ISTH Score)", S["h2"]))
dic_data = [
["Parameter", "Score 2", "Score 1", "Score 0"],
["Platelet count", "<50 ×10⁹/L", "50–100 ×10⁹/L", ">100 ×10⁹/L"],
["D-Dimer / FDP", "Strong increase", "Moderate increase", "No increase"],
["Prolonged PT", ">6 sec", "3–6 sec", "<3 sec"],
["Fibrinogen", "—", "<1.0 g/L", ">1.0 g/L"],
]
story.append(make_table(dic_data, [110, 135, 135, 135], header_bg=RED, alt_bg=LIGHT_RED, font_size=8.5))
story.append(Paragraph("Score ≥5 = Overt DIC. Score <5 = Suspect DIC — repeat in 24h.", S["note"]))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════
# QUICK REFERENCE SUMMARY PAGE
# ════════════════════════════════════════════════════════════════════════
section("Quick Reference: Normal Values", "Adult reference ranges for common blood tests", DARK_BLUE)
summary_data = [
["Test", "Normal Range", "Test", "Normal Range"],
["Haemoglobin (M)", "130–175 g/L", "Sodium (Na⁺)", "135–145 mmol/L"],
["Haemoglobin (F)", "115–160 g/L", "Potassium (K⁺)", "3.5–5.0 mmol/L"],
["MCV", "80–100 fL", "Chloride (Cl⁻)", "95–107 mmol/L"],
["WBC", "4.0–11.0 ×10⁹/L", "Bicarbonate (HCO₃⁻)", "22–29 mmol/L"],
["Neutrophils", "2.0–7.0 ×10⁹/L", "Calcium (corrected)", "2.2–2.6 mmol/L"],
["Lymphocytes", "1.5–4.0 ×10⁹/L", "Phosphate", "0.8–1.5 mmol/L"],
["Platelets", "150–400 ×10⁹/L", "Magnesium", "0.7–1.0 mmol/L"],
["Reticulocytes", "0.5–2.5%", "Urea", "2.5–7.1 mmol/L"],
["ALT", "5–40 U/L", "Creatinine (M)", "60–110 µmol/L"],
["AST", "5–40 U/L", "Creatinine (F)", "45–90 µmol/L"],
["ALP", "30–130 U/L", "eGFR", ">60 mL/min/1.73m²"],
["GGT", "5–55 U/L", "Fasting glucose", "3.9–6.0 mmol/L"],
["Bilirubin (total)", "<21 µmol/L", "HbA1c (normal)", "<42 mmol/mol (<6%)"],
["Albumin", "35–50 g/L", "TSH", "0.3–4.5 mU/L"],
["Total Protein", "60–80 g/L", "Free T4", "9–21 pmol/L"],
["CRP", "<5 mg/L", "Free T3", "3.5–7.8 pmol/L"],
["ESR", "M: <15; F: <20 mm/h", "INR", "0.9–1.2"],
["Fibrinogen", "2.0–4.0 g/L", "APTT", "26–37 sec"],
["D-Dimer", "<0.5 mg/L FEU", "Anion Gap", "8–12 mmol/L"],
]
story.append(make_table(summary_data, [120, 100, 120, 100], header_bg=DARK_BLUE, alt_bg=LIGHT_BLUE, font_size=8.5))
story.append(Spacer(1, 8))
story.append(Paragraph(
"KEY CLINICAL PRINCIPLES: (1) Always interpret in full clinical context. "
"(2) Trends matter more than single values. "
"(3) Correct for confounders: albumin-corrected calcium, haemolysis-affected results. "
"(4) Clotted, haemolysed, or delayed samples give spurious results — repeat if in doubt.",
ParagraphStyle("box", parent=S["note"],
borderPad=6, borderColor=DARK_BLUE, borderWidth=1,
backColor=LIGHT_BLUE, fontSize=8.5, leftIndent=8, rightIndent=8,
spaceBefore=4, spaceAfter=4)
))
# ── Build ─────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF created at: {path}")
if __name__ == "__main__":
build_pdf("/home/daytona/workspace/blood-tests-pdf/Blood_Test_Interpretation_QuickRef.pdf")
cd /home/daytona/workspace/blood-tests-pdf && python generate_pdf.py 2>&1
~/blood-tests-pdf/generate_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
~/blood-tests-pdf/generate_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
~/blood-tests-pdf/generate_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
cd /home/daytona/workspace/blood-tests-pdf && python generate_pdf.py 2>&1
~/blood-tests-pdf/generate_pdf.py
#!/usr/bin/env python3
"""
Quick Reference PDF: Blood Test Interpretation Flowcharts for General Medicine
Uses ReportLab for PDF generation.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak,
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus.flowables import Flowable
from reportlab.lib.colors import HexColor
from reportlab.graphics.shapes import (
Drawing, Rect, String, Polygon, Line, Circle, Path,
Group,
)
from reportlab.graphics import renderPDF
from reportlab.lib import colors as rl_colors
# ── Palette ──────────────────────────────────────────────────────────────────
DARK_BLUE = HexColor("#1a3a5c")
MED_BLUE = HexColor("#2e6da4")
LIGHT_BLUE = HexColor("#d0e4f5")
TEAL = HexColor("#1a7a6e")
LIGHT_TEAL = HexColor("#c8ece9")
RED = HexColor("#c0392b")
LIGHT_RED = HexColor("#fce8e6")
AMBER = HexColor("#d4850a")
LIGHT_AMBER = HexColor("#fef3e0")
GREEN = HexColor("#1e7e34")
LIGHT_GREEN = HexColor("#d4edda")
PURPLE = HexColor("#6c3483")
LIGHT_PURPLE = HexColor("#e8d5f5")
GREY = HexColor("#5a6a7a")
LIGHT_GREY = HexColor("#f0f4f8")
WHITE = colors.white
BLACK = colors.black
W, H = A4 # 595.28 x 841.89 pt
# ─────────────────────────────────────────────────────────────────────────────
# Helper: draw filled+stroked polygon via canvas (safe API)
# ─────────────────────────────────────────────────────────────────────────────
def draw_polygon(c, points, fill_color, stroke_color=None, line_width=0.5):
"""Draw a filled polygon on a ReportLab canvas using moveTo/lineTo."""
if stroke_color is None:
stroke_color = HexColor("#aaaaaa")
c.setFillColor(fill_color)
c.setStrokeColor(stroke_color)
c.setLineWidth(line_width)
p = c.beginPath()
p.moveTo(*points[0])
for pt in points[1:]:
p.lineTo(*pt)
p.close()
c.drawPath(p, fill=1, stroke=1)
def draw_arrow(c, ax, ay_top, ay_bottom):
"""Draw a downward arrow from ay_top to ay_bottom."""
c.setStrokeColor(GREY)
c.setLineWidth(1.0)
c.line(ax, ay_top, ax, ay_bottom + 6)
# arrowhead triangle
pts = [(ax, ay_bottom), (ax - 4, ay_bottom + 7), (ax + 4, ay_bottom + 7)]
c.setFillColor(GREY)
p = c.beginPath()
p.moveTo(*pts[0])
for pt in pts[1:]:
p.lineTo(*pt)
p.close()
c.drawPath(p, fill=1, stroke=0)
# ─────────────────────────────────────────────────────────────────────────────
# Flowable: Section Banner
# ─────────────────────────────────────────────────────────────────────────────
class SectionBanner(Flowable):
def __init__(self, title, subtitle="", bg=DARK_BLUE, fg=WHITE, width=None):
super().__init__()
self._title = title
self._subtitle = subtitle
self._bg = bg
self._fg = fg
self.width = width or (W - 40*mm)
self.height = 36 if subtitle else 26
def draw(self):
c = self.canv
c.setFillColor(self._bg)
c.roundRect(0, 0, self.width, self.height, 4, fill=1, stroke=0)
c.setFillColor(self._fg)
c.setFont("Helvetica-Bold", 13)
c.drawString(10, self.height - 17, self._title)
if self._subtitle:
c.setFont("Helvetica", 8.5)
c.drawString(10, 7, self._subtitle)
# ─────────────────────────────────────────────────────────────────────────────
# Flowable: Vertical Flowchart
# ─────────────────────────────────────────────────────────────────────────────
class FlowchartBox(Flowable):
BOX_W = 340
BOX_H = 30
DEC_H = 40
GAP = 16
def __init__(self, nodes, width=None):
super().__init__()
self._nodes = nodes
self.width = width or (W - 40*mm)
h = 0
for n in nodes:
h += (self.DEC_H if n["type"] == "decision" else self.BOX_H) + self.GAP
self.height = h + 6
def _colors(self, node):
t = node.get("color", None)
if t:
return t, WHITE
typ = node["type"]
if typ == "start": return MED_BLUE, WHITE
if typ == "decision": return AMBER, WHITE
if typ == "result": return TEAL, WHITE
return LIGHT_GREY, BLACK
def draw(self):
c = self.canv
cx = self.width / 2
bw = self.BOX_W
y = self.height - 4
for i, node in enumerate(self._nodes):
bh = self.DEC_H if node["type"] == "decision" else self.BOX_H
y -= bh
x = cx - bw / 2
bg, fg = self._colors(node)
if node["type"] == "decision":
mx, my = cx, y + bh / 2
pts = [(mx, y+bh), (x+bw, my), (mx, y), (x, my)]
draw_polygon(c, pts, bg)
elif node["type"] == "start":
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.roundRect(x, y, bw, bh, bh/2, fill=1, stroke=1)
elif node["type"] == "note":
c.setFillColor(LIGHT_GREY)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.setDash(3, 2)
c.rect(x, y, bw, bh, fill=1, stroke=1)
c.setDash()
else:
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.rect(x, y, bw, bh, fill=1, stroke=1)
# Label
c.setFillColor(fg)
label = node["label"]
fs = 8 if len(label) > 60 else (9 if len(label) > 35 else 9.5)
bold = node["type"] in ("start", "decision")
c.setFont("Helvetica-Bold" if bold else "Helvetica", fs)
lines = label.split("\n")
lh = fs * 1.35
tot = len(lines) * lh
sy = y + bh/2 + tot/2 - lh * 0.75
for li, ln in enumerate(lines):
c.drawCentredString(cx, sy - li*lh, ln)
if i < len(self._nodes) - 1:
draw_arrow(c, cx, y, y - self.GAP)
y -= self.GAP
# ─────────────────────────────────────────────────────────────────────────────
# Flowable: Dual (side-by-side) Flowchart
# ─────────────────────────────────────────────────────────────────────────────
class DualFlowchart(Flowable):
BOX_W = 170
BOX_H = 28
DEC_H = 38
GAP = 14
TITLE_H = 18
def __init__(self, left_nodes, right_nodes, left_title="", right_title="", width=None):
super().__init__()
self._ln = left_nodes
self._rn = right_nodes
self._lt = left_title
self._rt = right_title
self.width = width or (W - 40*mm)
def col_h(nodes):
h = self.TITLE_H
for n in nodes:
h += (self.DEC_H if n["type"] == "decision" else self.BOX_H) + self.GAP
return h + 6
self.height = max(col_h(left_nodes), col_h(right_nodes))
def _colors(self, node):
t = node.get("color", None)
if t:
return t, WHITE
typ = node["type"]
if typ == "start": return MED_BLUE, WHITE
if typ == "decision": return AMBER, WHITE
if typ == "result": return TEAL, WHITE
return LIGHT_GREY, BLACK
def _draw_col(self, c, nodes, cx, top_y, title):
bw = self.BOX_W
y = top_y - self.TITLE_H
# Title
c.setFont("Helvetica-Bold", 9)
c.setFillColor(DARK_BLUE)
c.drawCentredString(cx, top_y - 12, title)
for i, node in enumerate(nodes):
bh = self.DEC_H if node["type"] == "decision" else self.BOX_H
y -= bh
x = cx - bw / 2
bg, fg = self._colors(node)
if node["type"] == "decision":
mx, my = cx, y + bh/2
pts = [(mx, y+bh), (x+bw, my), (mx, y), (x, my)]
draw_polygon(c, pts, bg)
elif node["type"] == "start":
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.roundRect(x, y, bw, bh, bh/2, fill=1, stroke=1)
elif node["type"] == "note":
c.setFillColor(LIGHT_GREY)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.setDash(3, 2)
c.rect(x, y, bw, bh, fill=1, stroke=1)
c.setDash()
else:
c.setFillColor(bg)
c.setStrokeColor(HexColor("#aaaaaa"))
c.setLineWidth(0.5)
c.rect(x, y, bw, bh, fill=1, stroke=1)
c.setFillColor(fg)
label = node["label"]
fs = 7.5 if len(label) > 45 else (8 if len(label) > 30 else 8.5)
bold = node["type"] in ("start", "decision")
c.setFont("Helvetica-Bold" if bold else "Helvetica", fs)
lines = label.split("\n")
lh = fs * 1.35
tot = len(lines) * lh
sy = y + bh/2 + tot/2 - lh*0.75
for li, ln in enumerate(lines):
c.drawCentredString(cx, sy - li*lh, ln)
if i < len(nodes) - 1:
draw_arrow(c, cx, y, y - self.GAP)
y -= self.GAP
def draw(self):
c = self.canv
half = self.width / 2
lcx = half / 2
rcx = half + half / 2
top = self.height
# Vertical divider
c.setStrokeColor(HexColor("#cccccc"))
c.setLineWidth(0.5)
c.line(half, 0, half, self.height)
self._draw_col(c, self._ln, lcx, top, self._lt)
self._draw_col(c, self._rn, rcx, top, self._rt)
# ─────────────────────────────────────────────────────────────────────────────
# Flowable: Legend
# ─────────────────────────────────────────────────────────────────────────────
class LegendBox(Flowable):
def __init__(self, width=None):
super().__init__()
self.width = width or (W - 40*mm)
self.height = 22
def draw(self):
c = self.canv
c.setFillColor(LIGHT_GREY)
c.roundRect(0, 0, self.width, self.height, 3, fill=1, stroke=0)
items = [
(MED_BLUE, "Start/Test"),
(AMBER, "Decision"),
(TEAL, "Result/Diagnosis"),
(LIGHT_GREY, "Note/Action"),
]
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(DARK_BLUE)
c.drawString(6, 7, "KEY:")
for i, (bg, label) in enumerate(items):
sx = 40 + i * 118
c.setFillColor(bg)
c.setStrokeColor(HexColor("#888888"))
c.setLineWidth(0.4)
c.roundRect(sx, 5, 11, 11, 2, fill=1, stroke=1)
c.setFillColor(BLACK)
c.setFont("Helvetica", 7.5)
c.drawString(sx + 14, 7, label)
# ─────────────────────────────────────────────────────────────────────────────
# Cover Page Flowable
# ─────────────────────────────────────────────────────────────────────────────
class CoverPage(Flowable):
def __init__(self, width, height):
super().__init__()
self.width = width
self.height = height
def draw(self):
c = self.canv
w = self.width
h = self.height
# Background
c.setFillColor(DARK_BLUE)
c.rect(0, 0, w, h, fill=1, stroke=0)
c.setFillColor(MED_BLUE)
c.rect(0, h*0.6, w, h*0.4, fill=1, stroke=0)
# Decorative circles
c.setFillColor(HexColor("#ffffff12"))
c.circle(w*0.85, h*0.85, 80, fill=1, stroke=0)
c.circle(w*0.12, h*0.18, 55, fill=1, stroke=0)
# Title
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 26)
c.drawCentredString(w/2, h*0.73, "Blood Test Interpretation")
c.setFont("Helvetica-Bold", 20)
c.drawCentredString(w/2, h*0.66, "Quick Reference Guide")
# Subtitle bar
c.setFillColor(TEAL)
c.roundRect(w*0.08, h*0.58, w*0.84, 24, 4, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica", 10)
c.drawCentredString(w/2, h*0.585, "Interpretation Flowcharts | Normal Ranges | Clinical Patterns")
# Tests list
tests = [
"1. CBC / FBC",
"2. Liver Function Tests (LFTs)",
"3. Renal Function Tests (RFTs)",
"4. Electrolytes & Anion Gap",
"5. Blood Glucose & HbA1c",
"6. Thyroid Function Tests (TFTs)",
"7. Inflammatory Markers",
"8. Coagulation Screen",
]
c.setFont("Helvetica-Bold", 9)
c.setFillColor(LIGHT_BLUE)
c.drawCentredString(w/2, h*0.535, "TESTS COVERED IN THIS GUIDE")
c.setFont("Helvetica", 9)
c.setFillColor(WHITE)
for i, t in enumerate(tests):
col = i // 4
row = i % 4
px = w*0.22 + col * w*0.5
py = h*0.505 - row*17
c.drawCentredString(px, py, t)
# Footer
c.setFillColor(LIGHT_BLUE)
c.setFont("Helvetica", 8)
c.drawCentredString(w/2, 18,
"For educational/reference use only | Always interpret in full clinical context | 2026")
# ─────────────────────────────────────────────────────────────────────────────
# Table helper
# ─────────────────────────────────────────────────────────────────────────────
def make_table(data, col_widths, header_bg=MED_BLUE, alt_bg=LIGHT_BLUE,
header_fg=WHITE, font_size=8.5):
ts = TableStyle([
("BACKGROUND", (0,0), (-1,0), header_bg),
("TEXTCOLOR", (0,0), (-1,0), header_fg),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), font_size),
("FONTNAME", (0,1), (-1,-1), "Helvetica"),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, alt_bg]),
("GRID", (0,0), (-1,-1), 0.35, HexColor("#aaaaaa")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
])
t = Table(data, colWidths=col_widths)
t.setStyle(ts)
return t
# ─────────────────────────────────────────────────────────────────────────────
# Styles
# ─────────────────────────────────────────────────────────────────────────────
def get_styles():
ss = getSampleStyleSheet()
return {
"h2": ParagraphStyle("h2", parent=ss["Normal"],
fontSize=10.5, fontName="Helvetica-Bold",
textColor=DARK_BLUE, spaceBefore=7, spaceAfter=3),
"body": ParagraphStyle("body", parent=ss["Normal"],
fontSize=9, fontName="Helvetica",
textColor=HexColor("#333333"), spaceAfter=4, leading=13),
"note": ParagraphStyle("note", parent=ss["Normal"],
fontSize=8.5, fontName="Helvetica-Oblique",
textColor=GREY, spaceAfter=3, leftIndent=6),
"callout": ParagraphStyle("callout", parent=ss["Normal"],
fontSize=8.5, fontName="Helvetica",
textColor=DARK_BLUE, spaceAfter=4, leftIndent=8, rightIndent=8,
leading=13, borderPad=6, backColor=LIGHT_BLUE),
}
# ─────────────────────────────────────────────────────────────────────────────
# BUILD PDF
# ─────────────────────────────────────────────────────────────────────────────
def build_pdf(path):
doc = SimpleDocTemplate(
path, pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm,
topMargin=18*mm, bottomMargin=18*mm,
title="Blood Test Interpretation Quick Reference",
)
S = get_styles()
PW = W - 40*mm # printable width ~515 pt
story = []
# ── COVER ────────────────────────────────────────────────────────────────
story.append(CoverPage(PW, H - 40*mm - 8))
story.append(PageBreak())
# ── Helpers ───────────────────────────────────────────────────────────────
def section(title, subtitle="", color=DARK_BLUE):
story.append(SectionBanner(title, subtitle, bg=color, width=PW))
story.append(Spacer(1, 5))
def legend():
story.append(LegendBox(PW))
story.append(Spacer(1, 5))
# ══════════════════════════════════════════════════════════════════════════
# 1. FULL BLOOD COUNT
# ══════════════════════════════════════════════════════════════════════════
section("1. Full Blood Count (FBC / CBC)",
"Haemoglobin • Red Cell Indices • White Cell Differential • Platelets", MED_BLUE)
story.append(Paragraph("1a. Anaemia — Classification by MCV", S["h2"]))
legend()
story.append(FlowchartBox([
{"type":"start", "label":"Check Haemoglobin (Hb)\nMale <130 g/L | Female <120 g/L = Anaemia"},
{"type":"decision", "label":"What is the MCV?"},
{"type":"note", "label":"MCV <80 fL → MICROCYTIC\nMCV 80-100 fL → NORMOCYTIC\nMCV >100 fL → MACROCYTIC"},
], width=PW))
story.append(Spacer(1, 5))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Microcytic (<80 fL)"},
{"type":"decision", "label":"Serum ferritin?"},
{"type":"result", "label":"Low ferritin\n→ Iron Deficiency Anaemia\n(check for blood loss)", "color":RED},
{"type":"result", "label":"Normal/High ferritin\n→ Thalassaemia\n→ Anaemia of chronic disease\n→ Sideroblastic anaemia", "color":TEAL},
],
right_nodes=[
{"type":"start", "label":"Macrocytic (>100 fL)"},
{"type":"decision", "label":"Serum B12 & Folate?"},
{"type":"result", "label":"Low B12 or Folate\n→ Megaloblastic anaemia\n(pernicious anaemia, malabsorption)", "color":RED},
{"type":"result", "label":"Normal B12 & Folate\n→ Alcohol / Liver disease\n→ Hypothyroidism\n→ Drugs (methotrexate, hydroxyurea)", "color":TEAL},
],
left_title="MICROCYTIC", right_title="MACROCYTIC", width=PW,
))
story.append(Spacer(1, 5))
story.append(FlowchartBox([
{"type":"start", "label":"Normocytic Anaemia (MCV 80–100 fL)"},
{"type":"decision","label":"Reticulocyte count?"},
{"type":"note", "label":"HIGH reticulocytes → Haemolysis or Acute blood loss (marrow responding)\n Confirm haemolysis: ↑LDH, ↑indirect bilirubin, ↓haptoglobin, +ve direct Coombs\nLOW reticulocytes → Bone marrow failure, CKD, Anaemia of chronic disease"},
], width=PW))
story.append(Spacer(1, 6))
story.append(Paragraph("1b. White Cell Count — 5-Part Differential", S["h2"]))
wbc_data = [
["Cell", "Normal Range", "Raised (causes)", "Low (causes)"],
["Neutrophils", "2.0–7.0 ×10⁹/L", "Bacterial infection, steroids, stress,\ntrauma, CML", "Viral infections, drug toxicity,\naplastic anaemia, B12/folate def."],
["Lymphocytes", "1.5–4.0 ×10⁹/L", "Viral infection (EBV, CMV, HIV),\nCLL, TB", "HIV/AIDS, steroids, SLE,\npost-chemotherapy"],
["Eosinophils", "0.04–0.4 ×10⁹/L", "Parasitic infection, allergy, asthma,\nHodgkin's, Addison's", "Corticosteroids,\nacute bacterial infection"],
["Monocytes", "0.2–1.0 ×10⁹/L", "TB, malaria, monocytic leukaemia,\nchronic inflammation", "Hairy cell leukaemia"],
["Basophils", "0–0.1 ×10⁹/L", "CML, myeloproliferative disease,\nallergic reactions", "Steroids,\nhyperthyroidism"],
]
story.append(make_table(wbc_data, [72, 70, 200, 178], font_size=8))
story.append(Spacer(1, 5))
story.append(Paragraph("1c. Platelet Count & Pancytopenia", S["h2"]))
plt_data = [
["Result", "Threshold", "Common Causes", "Action"],
["Thrombocytopenia", "<150 ×10⁹/L", "ITP, HIT (heparin), DIC, hypersplenism,\ndrug-induced, marrow failure", "If <50: bleeding risk; if HIT suspected: stop heparin"],
["Thrombocytosis", ">400 ×10⁹/L", "Reactive (infection, iron deficiency,\npost-surgery) or Essential Thrombocythaemia", "Exclude reactive cause; if persistent,\nhaematology referral"],
["Pancytopenia\n(all 3 low)", "See values", "Aplastic anaemia, leukaemia, MDS, myeloma,\ninfiltration (TB/lymphoma), megaloblastic", "Urgent bone marrow biopsy"],
]
story.append(make_table(plt_data, [85, 75, 210, 145], header_bg=RED, alt_bg=LIGHT_RED, font_size=8))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 2. LIVER FUNCTION TESTS
# ══════════════════════════════════════════════════════════════════════════
section("2. Liver Function Tests (LFTs)",
"ALT • AST • ALP • GGT • Bilirubin • Albumin • PT", TEAL)
legend()
story.append(Paragraph("2a. LFT Pattern Flowchart", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Abnormal LFT Result"},
{"type":"decision", "label":"Which markers are elevated?"},
{"type":"note", "label":"↑↑↑ ALT / AST (>5× ULN) → Hepatocellular pattern (hepatitis, ischaemia, toxin)\n↑↑ ALP + GGT → Cholestatic / Obstructive pattern (obstruction, PBC, drugs)\n↑ Bilirubin only (unconjugated) → Haemolysis or Gilbert's syndrome\nLow Albumin + ↑ PT → Liver synthetic failure (cirrhosis, fulminant failure)"},
], width=PW))
story.append(Spacer(1, 5))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hepatocellular Pattern\n(↑↑ ALT, AST)"},
{"type":"decision", "label":"AST : ALT ratio?"},
{"type":"result", "label":"> 2:1 (AST dominant)\n→ Alcoholic hepatitis\n(GGT also ↑↑↑)", "color":RED},
{"type":"result", "label":"< 1:1 (ALT dominant)\n→ Viral hepatitis / NAFLD /\nDrug-induced (paracetamol)", "color":TEAL},
{"type":"note", "label":"ALT >1000 U/L: ischaemic\nhepatitis, viral, OD"},
],
right_nodes=[
{"type":"start", "label":"Cholestatic Pattern\n(↑↑ ALP, GGT)"},
{"type":"decision", "label":"Is GGT elevated?"},
{"type":"result", "label":"ALP ↑, GGT NORMAL\n→ Bone source: Paget's,\nbone mets, pregnancy", "color":AMBER},
{"type":"result", "label":"ALP ↑ + GGT ↑\n→ Biliary obstruction\n→ PBC / PSC / Drugs", "color":TEAL},
{"type":"note", "label":"First line: USS abdomen\n(check bile ducts)"},
],
left_title="HEPATOCELLULAR", right_title="CHOLESTATIC", width=PW,
))
story.append(Spacer(1, 5))
story.append(Paragraph("2b. Bilirubin — Conjugated vs Unconjugated", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Raised Bilirubin (Jaundice visible when total bilirubin >35 µmol/L)"},
{"type":"decision", "label":"Mainly unconjugated (indirect) or conjugated (direct)?"},
{"type":"note", "label":"Mainly UNCONJUGATED → Pre-hepatic:\n Haemolysis (+ ↑LDH, ↑reticulocytes, ↓haptoglobin), Gilbert's syndrome, neonatal jaundice\nMainly CONJUGATED → Hepatic or Post-hepatic:\n Hepatitis, cirrhosis, biliary obstruction, cholangitis, drugs"},
{"type":"result", "label":"Gilbert's: isolated mild unconjugated hyperbilirubinaemia triggered by fasting/stress. Benign — no treatment needed."},
], width=PW))
story.append(Spacer(1, 5))
story.append(Paragraph("2c. LFT Pattern Summary", S["h2"]))
lft_data = [
["Condition", "ALT/AST", "ALP", "GGT", "Bilirubin", "Albumin", "PT/INR"],
["Acute viral hepatitis","↑↑↑", "↑", "↑", "↑ direct", "Normal", "↑ if severe"],
["Alcoholic hepatitis", "↑↑ AST>ALT", "↑", "↑↑↑", "↑", "↓", "↑"],
["Biliary obstruction", "↑", "↑↑↑", "↑↑", "↑↑ direct", "Normal", "Normal"],
["Cirrhosis", "↑/Normal", "↑", "↑", "↑ (late)", "↓↓", "↑↑"],
["NAFLD/NASH", "↑ (ALT>AST)", "↑", "↑", "Normal", "Normal", "Normal"],
["Haemolysis", "Normal", "Normal", "Normal", "↑ indirect", "Normal", "Normal"],
["Gilbert's syndrome", "Normal", "Normal", "Normal", "↑ indirect", "Normal", "Normal"],
["Metastases/infiltr.","Normal/↑", "↑↑", "↑↑", "↑ (late)", "↓", "Normal/↑"],
]
story.append(make_table(lft_data,
[105, 58, 40, 40, 75, 60, 62],
header_bg=TEAL, alt_bg=LIGHT_TEAL, font_size=8))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 3. RENAL FUNCTION TESTS
# ══════════════════════════════════════════════════════════════════════════
section("3. Renal Function Tests (RFTs)",
"Urea • Creatinine • eGFR • AKI Staging • CKD Staging", MED_BLUE)
legend()
story.append(Paragraph("3a. Raised Creatinine / AKI Flowchart", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Raised Creatinine or Urea"},
{"type":"decision", "label":"Calculate Urea:Creatinine Ratio (Urea mmol/L ÷ Creatinine mmol/L)"},
{"type":"note", "label":"> 100 → PRE-RENAL: Dehydration, GI bleeding, heart failure, sepsis\n40–100 → INTRINSIC RENAL or POST-RENAL\n< 40 → Low urea production (liver failure, low protein diet)"},
{"type":"decision", "label":"Is rise acute or chronic?"},
{"type":"result", "label":"AKI: Creatinine rise ≥26 µmol/L in 48h OR ≥1.5× baseline in 7 days\nCKD: eGFR <60 mL/min/1.73m² persisting >3 months"},
], width=PW))
story.append(Spacer(1, 5))
story.append(Paragraph("3b. AKI Classification", S["h2"]))
aki_data = [
["AKI Type", "Mechanism", "Common Causes", "Urine Na", "FENa"],
["Pre-renal", "↓ Renal perfusion", "Dehydration, haemorrhage,\nHF, sepsis, NSAIDs", "<20 mmol/L", "<1%"],
["Intrinsic", "Direct renal damage", "ATN (ischaemia, nephrotoxins),\nGN, vasculitis, myoglobinuria", ">40 mmol/L", ">2%"],
["Post-renal", "Obstruction", "BPH, calculi, pelvic malignancy,\nretroperitoneal fibrosis", ">40 mmol/L", ">2%"],
]
story.append(make_table(aki_data, [60, 80, 190, 80, 45],
header_bg=RED, alt_bg=LIGHT_RED, font_size=8))
story.append(Spacer(1, 5))
story.append(Paragraph("3c. CKD Staging by eGFR", S["h2"]))
ckd_data = [
["Stage", "eGFR (mL/min/1.73m²)", "Description", "Management"],
["G1", "≥90 (+ markers)", "Normal/High", "Treat risk factors (HTN, DM)"],
["G2", "60–89", "Mildly reduced", "Annual monitoring, lifestyle"],
["G3a", "45–59", "Mildly-moderately ↓", "Monitor 6-monthly; nephrology if progressing"],
["G3b", "30–44", "Moderately reduced", "Nephrology referral; manage complications"],
["G4", "15–29", "Severely reduced", "Prepare for RRT; dietitian, anaemia management"],
["G5", "<15 (or dialysis)", "Kidney failure", "Renal replacement therapy (dialysis / transplant)"],
]
story.append(make_table(ckd_data, [35, 90, 115, 275],
header_bg=MED_BLUE, alt_bg=LIGHT_BLUE, font_size=8.5))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 4. ELECTROLYTES
# ══════════════════════════════════════════════════════════════════════════
section("4. Electrolytes & Anion Gap",
"Na • K • Cl • HCO₃ • Ca • Mg • Phosphate • Anion Gap", PURPLE)
legend()
story.append(Paragraph("4a. Sodium", S["h2"]))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hyponatraemia Na⁺ <135 mmol/L"},
{"type":"decision", "label":"Assess volume status"},
{"type":"result", "label":"Hypovolaemic\n→ Vomiting, diarrhoea, diuretics,\nAdrenal insufficiency", "color":RED},
{"type":"result", "label":"Euvolaemic\n→ SIADH, hypothyroidism,\npsychogenic polydipsia", "color":TEAL},
{"type":"result", "label":"Hypervolaemic\n→ Heart failure, cirrhosis,\nnephrotic syndrome", "color":AMBER},
],
right_nodes=[
{"type":"start", "label":"Hypernatraemia Na⁺ >145 mmol/L"},
{"type":"decision", "label":"Water loss or Na gain?"},
{"type":"result", "label":"Water loss\n→ Dehydration, Diabetes Insipidus,\nexcessive sweating/diarrhoea", "color":RED},
{"type":"result", "label":"Sodium gain\n→ Excess saline infusion,\nCushing's, Conn's syndrome", "color":TEAL},
{"type":"note", "label":"Correct slowly!\nMax 10–12 mmol/L / 24h\nto avoid osmotic demyelination"},
],
left_title="HYPONATRAEMIA", right_title="HYPERNATRAEMIA", width=PW,
))
story.append(Spacer(1, 5))
story.append(Paragraph("4b. Potassium", S["h2"]))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"Hypokalaemia K⁺ <3.5 mmol/L"},
{"type":"result", "label":"GI loss: vomiting, diarrhoea\nRenal loss: diuretics (thiazide/loop)\nCushing's / Conn's, alkalosis\nInsulin excess, low Mg", "color":RED},
{"type":"note", "label":"ECG: U waves, flat T waves,\nlong QT → arrhythmia risk"},
{"type":"result", "label":"Replace K⁺ (oral or IV)\nAlways correct Mg concurrently", "color":GREEN},
],
right_nodes=[
{"type":"start", "label":"Hyperkalaemia K⁺ >5.0 mmol/L"},
{"type":"result", "label":"Renal failure (AKI/CKD)\nACEi / ARBs / Spironolactone\nAddison's, acidosis\nRhabdomyolysis, haemolysis", "color":RED},
{"type":"note", "label":"ECG: Tall T waves, wide QRS,\nsine wave → EMERGENCY if >6.5"},
{"type":"result", "label":"URGENT: IV Calcium gluconate\n(membrane stabilisation)\nThen insulin/dextrose, salbutamol", "color":GREEN},
],
left_title="HYPOKALAEMIA", right_title="HYPERKALAEMIA", width=PW,
))
story.append(Spacer(1, 5))
story.append(Paragraph("4c. Anion Gap & Metabolic Acidosis", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Metabolic Acidosis (↓HCO₃⁻, ↓pH on ABG/VBG)"},
{"type":"decision", "label":"Anion Gap = Na − (Cl + HCO₃) Normal: 8–12 mmol/L"},
{"type":"note", "label":"HIGH ANION GAP (>12 mmol/L) → MUDPILES:\n Methanol | Uraemia | DKA | Propylene glycol | Isoniazid / Iron\n Lactic acidosis | Ethylene glycol | Salicylates\n\nNORMAL ANION GAP (Hyperchloraemic acidosis):\n Diarrhoea | Renal tubular acidosis (RTA) | Acetazolamide | Addison's"},
{"type":"result", "label":"Always confirm with ABG/VBG: pH, pCO₂, HCO₃, base excess, lactate"},
], width=PW))
story.append(Spacer(1, 5))
story.append(Paragraph("4d. Calcium (correct for albumin: Ca + 0.02 × [40 − Albumin g/L])", S["h2"]))
ca_data = [
["Condition", "Ca²⁺", "PTH", "PO₄", "ALP", "Common Causes"],
["Primary hyperPTH", "↑", "↑↑", "↓", "↑", "Parathyroid adenoma (80%), hyperplasia"],
["Malignancy hypercalc.","↑", "↓", "↑/N", "↑", "PTHrP (solid tumours), bone mets, myeloma"],
["Sarcoidosis/Vit D OD","↑", "↓", "↑", "N", "Granulomatous disease, excess Vit D"],
["Hypoparathyroidism", "↓", "↓", "↑", "N", "Post-thyroid/parathyroid surgery, autoimmune"],
["Vit D deficiency", "↓/N", "↑↑", "↓", "↑", "Malabsorption, lack of sunlight, CKD"],
["CKD-MBD", "↓", "↑↑", "↑", "↑", "Renal osteodystrophy, secondary hyperPTH"],
]
story.append(make_table(ca_data, [110, 35, 35, 35, 40, 260],
header_bg=PURPLE, alt_bg=LIGHT_PURPLE, font_size=8))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 5. BLOOD GLUCOSE & HbA1c
# ══════════════════════════════════════════════════════════════════════════
section("5. Blood Glucose & HbA1c",
"Fasting glucose • Random glucose • OGTT • HbA1c • Hypoglycaemia", GREEN)
legend()
story.append(Paragraph("5a. Diabetes Diagnosis Flowchart", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Assess Blood Glucose"},
{"type":"decision", "label":"Is the patient symptomatic?\n(polyuria, polydipsia, unexplained weight loss, recurrent infections)"},
{"type":"note", "label":"SYMPTOMATIC: ONE abnormal result is sufficient to diagnose diabetes\nASYMPTOMATIC: TWO abnormal results on separate occasions required"},
{"type":"decision", "label":"Diagnostic thresholds"},
{"type":"note", "label":"Fasting glucose ≥7.0 mmol/L → Diabetes\nRandom glucose ≥11.1 mmol/L → Diabetes\nHbA1c ≥48 mmol/mol (6.5%) → Diabetes\n2h OGTT (75g) ≥11.1 mmol/L → Diabetes\n\nFasting glucose 6.1–6.9 mmol/L → Impaired Fasting Glucose (IFG)\n2h OGTT 7.8–11.0 mmol/L → Impaired Glucose Tolerance (IGT)\nHbA1c 42–47 mmol/mol → Pre-diabetes"},
{"type":"result", "label":"Both IFG and IGT = high risk for type 2 DM and cardiovascular disease\nIntervene: lifestyle modification, consider metformin"},
], width=PW))
story.append(Spacer(1, 5))
story.append(Paragraph("5b. HbA1c as Monitoring Tool (diagnosed diabetic)", S["h2"]))
hba_data = [
["HbA1c (%)", "HbA1c (mmol/mol)", "Interpretation", "Action"],
["<6.5%", "<48", "Good glycaemic control", "Continue current regimen"],
["6.5–7.5%", "48–58", "Acceptable target (most T2DM)", "Reinforce lifestyle, review medications"],
["7.5–9.0%", "58–75", "Sub-optimal control", "Intensify therapy; consider adding agent or insulin"],
[">9.0%", ">75", "Poor control — high complication risk", "Urgent review; likely insulin required"],
]
story.append(make_table(hba_data, [65, 75, 200, 175],
header_bg=GREEN, alt_bg=LIGHT_GREEN, font_size=8.5))
story.append(Spacer(1, 5))
story.append(Paragraph("5c. Hypoglycaemia (<3.9 mmol/L) — Causes", S["h2"]))
hypo_data = [
["Category", "Causes"],
["Drugs", "Insulin overdose, sulphonylureas (glibenclamide, gliclazide), alcohol, quinine, pentamidine"],
["Endocrine", "Insulinoma, Addison's disease, hypopituitarism, non-islet cell tumour hypoglycaemia"],
["Hepatic", "Acute liver failure, alcohol toxicity, Reye's syndrome"],
["Other", "Starvation, critical illness (sepsis, post-cardiac surgery), post-gastric bypass dumping"],
]
story.append(make_table(hypo_data, [80, 435],
header_bg=GREEN, alt_bg=LIGHT_GREEN, font_size=8.5))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 6. THYROID FUNCTION TESTS
# ══════════════════════════════════════════════════════════════════════════
section("6. Thyroid Function Tests (TFTs)",
"TSH • Free T4 • Free T3 • TPO Antibodies • TSH receptor antibodies", MED_BLUE)
legend()
story.append(Paragraph("6a. TFT Interpretation Flowchart", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Check TSH first (best initial screening test)"},
{"type":"decision", "label":"TSH level?"},
{"type":"note", "label":"TSH NORMAL (0.3–4.5 mU/L) → Likely euthyroid. Consider pituitary disease if clinical suspicion.\nTSH HIGH (>4.5 mU/L) → Check Free T4 (hypothyroidism or subclinical)\nTSH LOW (<0.3 mU/L) → Check Free T4 and Free T3 (hyperthyroidism or subclinical)"},
], width=PW))
story.append(Spacer(1, 5))
story.append(DualFlowchart(
left_nodes=[
{"type":"start", "label":"TSH HIGH (>4.5 mU/L)"},
{"type":"decision", "label":"Free T4?"},
{"type":"result", "label":"fT4 LOW\n→ Primary Hypothyroidism\n(Hashimoto's, iodine deficiency,\npost-thyroidectomy)", "color":RED},
{"type":"result", "label":"fT4 NORMAL\n→ Subclinical Hypothyroidism\n(early / borderline)", "color":AMBER},
{"type":"note", "label":"Check TPO antibodies\nTreat if TSH >10 or symptomatic"},
],
right_nodes=[
{"type":"start", "label":"TSH LOW (<0.3 mU/L)"},
{"type":"decision", "label":"Free T4 / Free T3?"},
{"type":"result", "label":"fT4 / fT3 HIGH\n→ Primary Hyperthyroidism\n(Graves', toxic MNG,\ntoxic adenoma)", "color":RED},
{"type":"result", "label":"fT4 / fT3 NORMAL\n→ Subclinical Hyperthyroidism\n(early Graves', excess\nlevothyroxine)", "color":AMBER},
{"type":"note", "label":"TSH receptor Ab (TRAb)\nconfirms Graves' disease"},
],
left_title="HIGH TSH", right_title="LOW TSH", width=PW,
))
story.append(Spacer(1, 5))
tft_data = [
["Pattern", "TSH", "fT4", "fT3", "Diagnosis"],
["Primary hypothyroid", "↑↑", "↓", "↓", "Hashimoto's thyroiditis, iodine deficiency, post-RAI"],
["Subclinical hypothyroid","↑", "Normal", "Normal", "Early Hashimoto's, drug-induced (lithium, amiodarone)"],
["Primary hyperthyroid", "↓↓", "↑", "↑", "Graves' disease, toxic nodule, toxic MNG"],
["Subclinical hyperthyroid","↓", "Normal", "Normal", "Early Graves', excess levothyroxine dose"],
["Secondary hypothyroid", "↓/Normal","↓", "↓", "Pituitary failure, Sheehan's syndrome, craniopharyngioma"],
["Sick euthyroid (NTI)", "Low/N", "↓", "↓↓", "Critical illness — low T3 syndrome. Do NOT treat."],
["Thyroid storm", "↓↓", "↑↑", "↑↑", "Emergency: fever, tachycardia, altered consciousness"],
]
story.append(make_table(tft_data, [118, 42, 40, 40, 275],
header_bg=MED_BLUE, alt_bg=LIGHT_BLUE, font_size=8))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 7. INFLAMMATORY MARKERS
# ══════════════════════════════════════════════════════════════════════════
section("7. Inflammatory Markers",
"CRP • ESR • WBC • Procalcitonin • Ferritin • Fibrinogen", RED)
legend()
story.append(Paragraph("7a. CRP + ESR Interpretation Flowchart", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Raised Inflammatory Markers (CRP / ESR / WBC)"},
{"type":"decision", "label":"CRP Level?"},
{"type":"note", "label":"CRP 10–100 mg/L → Mild infection, autoimmune flare, post-surgery\nCRP 100–500 mg/L → Significant bacterial infection, major trauma\nCRP >500 mg/L → Severe sepsis, major burns, necrotising infection"},
{"type":"decision", "label":"CRP raised — Is ESR also raised?"},
{"type":"note", "label":"High ESR + High CRP → Bacterial infection, vasculitis, PMR, RA, malignancy\nHigh ESR + Normal CRP → Myeloma (paraprotein), SLE, anaemia, chronic disease\nHigh CRP + Normal/Low ESR → Early viral infection, mild inflammation"},
{"type":"result", "label":"CRP >100 not responding to antibiotics → Resistant organism, collection, fungal infection,\nnon-infective cause (still's disease, malignancy, PE, MI)"},
], width=PW))
story.append(Spacer(1, 5))
inf_data = [
["Marker", "Normal", "Kinetics", "Key Clinical Uses", "Important Notes"],
["CRP", "<5 mg/L", "Rises 6–12h,\nfalls quickly on recovery","Best acute marker; monitors treatment response","Non-specific; rises with any inflammation"],
["ESR", "M <15; F <20\nmm/h (age-adjusted)","Rises slowly\nover days","PMR screening, myeloma, TB,\nchronic inflammatory disease","Affected by anaemia, pregnancy,\nhypergammaglobulinaemia, age"],
["Procalcitonin","<0.1 µg/L", "Rises 3–4h", ">0.5 = possible bacterial infection\n>2.0 = probable bacterial sepsis","More specific for bacterial infection;\nguides antibiotic de-escalation"],
["Ferritin", "M: 20–300\nF: 10–200 µg/L","Acute phase\nreactant","Iron stores (when not inflamed)\nVery high (>5000) → HLH, Macrophage\nActivation Syndrome, Still's disease","Unreliable for iron deficiency\nin active inflammation"],
["Fibrinogen", "2.0–4.0 g/L", "Acute phase", "Rises in inflammation;\nconsider in DIC workup","Falls with liver disease and DIC\n(consumed in clotting cascade)"],
]
story.append(make_table(inf_data, [72, 65, 65, 175, 138],
header_bg=RED, alt_bg=LIGHT_RED, font_size=8))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# 8. COAGULATION SCREEN
# ══════════════════════════════════════════════════════════════════════════
section("8. Coagulation Screen",
"PT/INR • APTT • Fibrinogen • D-Dimer • Thrombin Time • DIC", DARK_BLUE)
legend()
story.append(Paragraph("8a. Coagulation Pathway Interpretation", S["h2"]))
story.append(FlowchartBox([
{"type":"start", "label":"Abnormal Coagulation Screening Result"},
{"type":"decision", "label":"Which tests are prolonged?"},
{"type":"note", "label":"PT↑ (INR↑) ONLY → Extrinsic pathway affected:\n Warfarin, Vitamin K deficiency, liver disease (early), Factor VII deficiency\n\nAPTT↑ ONLY → Intrinsic pathway affected:\n Heparin (UFH), Haemophilia A (↓FactorVIII), Haemophilia B (↓FactorIX)\n Lupus anticoagulant, von Willebrand disease, Factor XI/XII deficiency\n\nBOTH PT↑ + APTT↑ → Common pathway or multiple defects:\n DIC, Severe liver disease, Massive transfusion, Supratherapeutic anticoagulation\n Direct thrombin inhibitors (dabigatran), Factor X/V/II deficiency"},
{"type":"decision", "label":"If both prolonged → check Fibrinogen + D-Dimer + Platelet count"},
{"type":"result", "label":"Fibrinogen ↓ + D-dimer ↑ + Platelets ↓ → DIC (diagnose & treat the underlying cause urgently)\nFibrinogen Normal + PT/APTT ↑ → Liver disease or anticoagulant effect"},
], width=PW))
story.append(Spacer(1, 5))
coag_data = [
["Test", "Normal Range", "Prolonged / Raised in", "Notes"],
["PT / INR", "INR 0.9–1.2\n(11–14 sec)","Warfarin, Vit K def., liver disease,\nDIC, Factor II/V/VII/X deficiency","INR >1.5 = significant coagulopathy;\n>3 = high bleeding risk"],
["APTT", "26–37 sec", "UFH (unfractionated heparin),\nHaemophilia A/B, lupus anticoagulant, DIC","Mixing study: corrects with normal plasma\n→ Factor deficiency; doesn't correct → Inhibitor"],
["Fibrinogen", "2.0–4.0 g/L", "Low in DIC, severe liver failure,\nmassive transfusion, fibrinolysis","Rises as acute phase reactant in\ninflammation (can mask depletion in DIC)"],
["D-Dimer", "<0.5 mg/L FEU", "VTE (DVT/PE), DIC, malignancy,\npregnancy, post-surgery, sepsis","High sensitivity, low specificity.\nNegative D-dimer excludes PE/DVT (low pre-test probability)"],
["Thrombin Time","14–16 sec", "Heparin, hypofibrinogenaemia,\ndabigatran, paraproteinaemia","Prolonged by even small amounts of heparin"],
]
story.append(make_table(coag_data, [70, 80, 200, 165],
header_bg=DARK_BLUE, alt_bg=LIGHT_BLUE, font_size=8))
story.append(Spacer(1, 5))
story.append(Paragraph("8b. ISTH DIC Diagnostic Score (overt DIC = score ≥5)", S["h2"]))
dic_data = [
["Parameter", "Score 2", "Score 1", "Score 0"],
["Platelet count", "<50 ×10⁹/L", "50–100 ×10⁹/L", ">100 ×10⁹/L"],
["D-Dimer / FDP", "Strong increase (>5mg/L)", "Moderate increase", "No increase"],
["Prolonged PT", ">6 seconds", "3–6 seconds", "<3 seconds"],
["Fibrinogen", "—", "<1.0 g/L", ">1.0 g/L"],
]
story.append(make_table(dic_data, [105, 140, 135, 135],
header_bg=RED, alt_bg=LIGHT_RED, font_size=8.5))
story.append(Paragraph(
"Score ≥5 = Overt DIC. Score <5 = Non-overt / suspected DIC — repeat in 24 hours.",
S["note"]))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# QUICK REFERENCE SUMMARY PAGE
# ══════════════════════════════════════════════════════════════════════════
section("Quick Reference: Adult Normal Ranges", "", DARK_BLUE)
summary_data = [
["Test", "Normal Range", "Test", "Normal Range"],
["Haemoglobin (male)", "130–175 g/L", "Sodium (Na⁺)", "135–145 mmol/L"],
["Haemoglobin (female)", "115–160 g/L", "Potassium (K⁺)", "3.5–5.0 mmol/L"],
["MCV", "80–100 fL", "Chloride (Cl⁻)", "95–107 mmol/L"],
["MCH", "27–33 pg", "Bicarbonate (HCO₃⁻)", "22–29 mmol/L"],
["WBC", "4.0–11.0 ×10⁹/L", "Calcium (corrected)", "2.2–2.6 mmol/L"],
["Neutrophils", "2.0–7.0 ×10⁹/L", "Phosphate", "0.8–1.5 mmol/L"],
["Lymphocytes", "1.5–4.0 ×10⁹/L", "Magnesium", "0.7–1.0 mmol/L"],
["Platelets", "150–400 ×10⁹/L", "Urea", "2.5–7.1 mmol/L"],
["Reticulocytes", "0.5–2.5%", "Creatinine (male)", "60–110 µmol/L"],
["ALT", "5–40 U/L", "Creatinine (female)", "45–90 µmol/L"],
["AST", "5–40 U/L", "eGFR (normal)", ">60 mL/min/1.73m²"],
["ALP", "30–130 U/L", "Fasting glucose", "3.9–6.0 mmol/L"],
["GGT", "5–55 U/L", "HbA1c (normal)", "<42 mmol/mol (<6%)"],
["Bilirubin (total)", "<21 µmol/L", "TSH", "0.3–4.5 mU/L"],
["Albumin", "35–50 g/L", "Free T4", "9–21 pmol/L"],
["Total Protein", "60–80 g/L", "Free T3", "3.5–7.8 pmol/L"],
["CRP", "<5 mg/L", "INR", "0.9–1.2"],
["ESR (male)", "<15 mm/h", "APTT", "26–37 sec"],
["ESR (female)", "<20 mm/h", "Fibrinogen", "2.0–4.0 g/L"],
["Procalcitonin", "<0.1 µg/L", "D-Dimer", "<0.5 mg/L FEU"],
["Ferritin (male)", "20–300 µg/L", "Anion Gap", "8–12 mmol/L"],
["Ferritin (female)", "10–200 µg/L", "Uric acid", "200–430 µmol/L"],
]
story.append(make_table(summary_data, [120, 100, 120, 100],
header_bg=DARK_BLUE, alt_bg=LIGHT_BLUE, font_size=8.5))
story.append(Spacer(1, 8))
story.append(Paragraph(
"IMPORTANT: Reference ranges vary between laboratories. Always use your local laboratory reference ranges. "
"Interpret results in the full clinical context — a 'normal' result may still be significant for a particular patient. "
"Trends over time matter more than single values. Haemolysed, clotted, or delayed samples give spurious results.",
ParagraphStyle("warn", parent=S["note"],
borderPad=6, backColor=LIGHT_AMBER,
fontSize=8.5, leftIndent=6, rightIndent=6, spaceBefore=4)
))
# ── Build ─────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved: {path}")
if __name__ == "__main__":
build_pdf("/home/daytona/workspace/blood-tests-pdf/Blood_Test_Interpretation_QuickRef.pdf")
try again
ls /home/daytona/workspace/blood-tests-pdf/
cat > /home/daytona/workspace/blood-tests-pdf/generate_pdf.py << 'PYEOF' #!/usr/bin/env python3 """ Blood Test Interpretation Quick Reference PDF Clean ReportLab implementation - no drawPath(None) calls """ from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import mm from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, ) from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER from reportlab.platypus.flowables import Flowable from reportlab.lib.colors import HexColor # ── Palette ──────────────────────────────────────────────────────────────── C = { "dark_blue": HexColor("#1a3a5c"), "med_blue": HexColor("#2e6da4"), "light_blue": HexColor("#d0e4f5"), "teal": HexColor("#1a7a6e"), "light_teal": HexColor("#c8ece9"), "red": HexColor("#c0392b"), "light_red": HexColor("#fce8e6"), "amber": HexColor("#d4850a"), "light_amber": HexColor("#fef3e0"), "green": HexColor("#1e7e34"), "light_green": HexColor("#d4edda"), "purple": HexColor("#6c3483"), "light_purple": HexColor("#e8d5f5"), "grey": HexColor("#5a6a7a"), "light_grey": HexColor("#f0f4f8"), "white": colors.white, "black": colors.black, } W, H = A4 def _fill_polygon(c, points, fill, stroke=None): """Safe polygon fill using beginPath/moveTo/lineTo/closePath/drawPath.""" if stroke is None: stroke = HexColor("#aaaaaa") c.setFillColor(fill) c.setStrokeColor(stroke) c.setLineWidth(0.5) path = c.beginPath() path.moveTo(*points[0]) for pt in points[1:]: path.lineTo(*pt) path.close() c.drawPath(path, fill=1, stroke=1) def _draw_arrow(c, cx, y_top, y_bot): c.setStrokeColor(C["grey"]) c.setLineWidth(0.9) c.line(cx, y_top, cx, y_bot + 6) pts = [(cx, y_bot), (cx - 4, y_bot + 7), (cx + 4, y_bot + 7)] c.setFillColor(C["grey"]) path = c.beginPath() path.moveTo(*pts[0]) path.lineTo(*pts[1]) path.lineTo(*pts[2]) path.close() c.drawPath(path, fill=1, stroke=0) # ── Node colour map ──────────────────────────────────────────────────────── _NODE_COLORS = { "start": (C["med_blue"], C["white"]), "decision": (C["amber"], C["white"]), "result": (C["teal"], C["white"]), "note": (C["light_grey"], C["black"]), } def _node_colors(node): if "color" in node: return node["color"], C["white"] return _NODE_COLORS.get(node["type"], (C["light_blue"], C["black"])) # ── Flowable: Cover ──────────────────────────────────────────────────────── class Cover(Flowable): def __init__(self, pw, ph): super().__init__() self.width = pw self.height = ph def draw(self): c, w, h = self.canv, self.width, self.height # bg c.setFillColor(C["dark_blue"]) c.rect(0, 0, w, h, fill=1, stroke=0) c.setFillColor(C["med_blue"]) c.rect(0, h * 0.6, w, h * 0.4, fill=1, stroke=0) # deco circles c.setFillColor(HexColor("#ffffff10")) c.circle(w * 0.85, h * 0.82, 80, fill=1, stroke=0) c.circle(w * 0.12, h * 0.15, 55, fill=1, stroke=0) # title c.setFillColor(C["white"]) c.setFont("Helvetica-Bold", 26) c.drawCentredString(w / 2, h * 0.73, "Blood Test Interpretation") c.setFont("Helvetica-Bold", 20) c.drawCentredString(w / 2, h * 0.66, "Quick Reference Guide") # subtitle bar c.setFillColor(C["teal"]) c.roundRect(w * 0.08, h * 0.58, w * 0.84, 22, 4, fill=1, stroke=0) c.setFillColor(C["white"]) c.setFont("Helvetica", 10) c.drawCentredString(w / 2, h * 0.585, "Interpretation Flowcharts | Normal Ranges | Clinical Patterns") # tests list tests = [ "1. CBC / FBC", "5. Blood Glucose & HbA1c", "2. Liver Function Tests", "6. Thyroid Function Tests", "3. Renal Function Tests", "7. Inflammatory Markers", "4. Electrolytes & Anion Gap", "8. Coagulation Screen", ] c.setFont("Helvetica-Bold", 9) c.setFillColor(C["light_blue"]) c.drawCentredString(w / 2, h * 0.535, "TESTS COVERED IN THIS GUIDE") c.setFont("Helvetica", 9) c.setFillColor(C["white"]) for i, t in enumerate(tests): col = i // 4 row = i % 4 c.drawCentredString(w * 0.26 + col * w * 0.48, h * 0.505 - row * 16, t) # footer c.setFillColor(C["light_blue"]) c.setFont("Helvetica", 7.5) c.drawCentredString(w / 2, 14, "For educational/reference use only | Always interpret in full clinical context | 2026") # ── Flowable: Section Banner ─────────────────────────────────────────────── class Banner(Flowable): def __init__(self, title, sub="", bg=None, width=None): super().__init__() self._title = title self._sub = sub self._bg = bg or C["dark_blue"] self.width = width or (W - 40 * mm) self.height = 36 if sub else 26 def draw(self): c = self.canv c.setFillColor(self._bg) c.roundRect(0, 0, self.width, self.height, 4, fill=1, stroke=0) c.setFillColor(C["white"]) c.setFont("Helvetica-Bold", 12) c.drawString(10, self.height - 17, self._title) if self._sub: c.setFont("Helvetica", 8) c.drawString(10, 7, self._sub) # ── Flowable: Legend ─────────────────────────────────────────────────────── class Legend(Flowable): def __init__(self, width=None): super().__init__() self.width = width or (W - 40 * mm) self.height = 20 def draw(self): c = self.canv c.setFillColor(C["light_grey"]) c.roundRect(0, 0, self.width, self.height, 3, fill=1, stroke=0) items = [ (C["med_blue"], "Start / Test"), (C["amber"], "Decision"), (C["teal"], "Result / Diagnosis"), (C["light_grey"], "Note / Action"), ] c.setFont("Helvetica-Bold", 7) c.setFillColor(C["dark_blue"]) c.drawString(6, 6, "KEY:") for i, (bg, lbl) in enumerate(items): sx = 36 + i * 118 c.setFillColor(bg) c.setStrokeColor(HexColor("#999999")) c.setLineWidth(0.4) c.roundRect(sx, 4, 11, 11, 2, fill=1, stroke=1) c.setFillColor(C["black"]) c.setFont("Helvetica", 7) c.drawString(sx + 14, 6, lbl) # ── Flowable: Single-Column Flowchart ───────────────────────────────────── class Flow(Flowable): BW = 340; BH = 30; DH = 40; GAP = 15 def __init__(self, nodes, width=None): super().__init__() self._nodes = nodes self.width = width or (W - 40 * mm) h = 0 for n in nodes: h += (self.DH if n["type"] == "decision" else self.BH) + self.GAP self.height = h + 4 def draw(self): c = self.canv cx = self.width / 2 bw = self.BW y = self.height - 2 for i, node in enumerate(self._nodes): bh = self.DH if node["type"] == "decision" else self.BH y -= bh x = cx - bw / 2 bg, fg = _node_colors(node) if node["type"] == "decision": mx, my = cx, y + bh / 2 _fill_polygon(c, [(mx, y+bh), (x+bw, my), (mx, y), (x, my)], bg) elif node["type"] == "start": c.setFillColor(bg); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.roundRect(x, y, bw, bh, bh / 2, fill=1, stroke=1) elif node["type"] == "note": c.setFillColor(C["light_grey"]); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.setDash(3, 2); c.rect(x, y, bw, bh, fill=1, stroke=1); c.setDash() else: c.setFillColor(bg); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.rect(x, y, bw, bh, fill=1, stroke=1) label = node["label"] fs = 7.8 if len(label) > 70 else (8.5 if len(label) > 40 else 9.5) bold = node["type"] in ("start", "decision") c.setFont("Helvetica-Bold" if bold else "Helvetica", fs) c.setFillColor(fg) lines = label.split("\n") lh = fs * 1.35 sy = y + bh / 2 + len(lines) * lh / 2 - lh * 0.75 for j, ln in enumerate(lines): c.drawCentredString(cx, sy - j * lh, ln) if i < len(self._nodes) - 1: _draw_arrow(c, cx, y, y - self.GAP) y -= self.GAP # ── Flowable: Dual-Column Flowchart ─────────────────────────────────────── class Dual(Flowable): BW = 172; BH = 28; DH = 38; GAP = 13; TH = 18 def __init__(self, ln, rn, lt="", rt="", width=None): super().__init__() self._ln = ln; self._rn = rn self._lt = lt; self._rt = rt self.width = width or (W - 40 * mm) def col_h(ns): h = self.TH for n in ns: h += (self.DH if n["type"] == "decision" else self.BH) + self.GAP return h + 4 self.height = max(col_h(ln), col_h(rn)) def _col(self, c, nodes, cx, top, title): bw = self.BW y = top - self.TH c.setFont("Helvetica-Bold", 9); c.setFillColor(C["dark_blue"]) c.drawCentredString(cx, top - 13, title) for i, node in enumerate(nodes): bh = self.DH if node["type"] == "decision" else self.BH y -= bh x = cx - bw / 2 bg, fg = _node_colors(node) if node["type"] == "decision": mx, my = cx, y + bh / 2 _fill_polygon(c, [(mx, y+bh), (x+bw, my), (mx, y), (x, my)], bg) elif node["type"] == "start": c.setFillColor(bg); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.roundRect(x, y, bw, bh, bh / 2, fill=1, stroke=1) elif node["type"] == "note": c.setFillColor(C["light_grey"]); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.setDash(3, 2); c.rect(x, y, bw, bh, fill=1, stroke=1); c.setDash() else: c.setFillColor(bg); c.setStrokeColor(HexColor("#aaaaaa")); c.setLineWidth(0.5) c.rect(x, y, bw, bh, fill=1, stroke=1) label = node["label"] fs = 7.2 if len(label) > 50 else (7.8 if len(label) > 35 else 8.5) bold = node["type"] in ("start", "decision") c.setFont("Helvetica-Bold" if bold else "Helvetica", fs) c.setFillColor(fg) lines = label.split("\n") lh = fs * 1.35 sy = y + bh / 2 + len(lines) * lh / 2 - lh * 0.75 for j, ln in enumerate(lines): c.drawCentredString(cx, sy - j * lh, ln) if i < len(nodes) - 1: _draw_arrow(c, cx, y, y - self.GAP) y -= self.GAP def draw(self): c = self.canv half = self.width / 2 c.setStrokeColor(HexColor("#cccccc")); c.setLineWidth(0.5) c.line(half, 0, half, self.height) self._col(c, self._ln, half / 2, self.height, self._lt) self._col(c, self._rn, half + half/2, self.height, self._rt) # ── Table helper ─────────────────────────────────────────────────────────── def tbl(data, widths, hbg=None, abg=None, fs=8.5): hbg = hbg or C["med_blue"] abg = abg or C["light_blue"] t = Table(data, colWidths=widths) t.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,0), hbg), ("TEXTCOLOR", (0,0), (-1,0), C["white"]), ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"), ("FONTSIZE", (0,0), (-1,-1), fs), ("FONTNAME", (0,1), (-1,-1), "Helvetica"), ("ROWBACKGROUNDS", (0,1), (-1,-1), [C["white"], abg]), ("GRID", (0,0), (-1,-1), 0.35, HexColor("#aaaaaa")), ("VALIGN", (0,0), (-1,-1), "MIDDLE"), ("LEFTPADDING", (0,0), (-1,-1), 5), ("RIGHTPADDING", (0,0), (-1,-1), 5), ("TOPPADDING", (0,0), (-1,-1), 4), ("BOTTOMPADDING", (0,0), (-1,-1), 4), ])) return t # ── Styles ───────────────────────────────────────────────────────────────── def styles(): ss = getSampleStyleSheet() h2 = ParagraphStyle("h2", parent=ss["Normal"], fontSize=10, fontName="Helvetica-Bold", textColor=C["dark_blue"], spaceBefore=6, spaceAfter=3) note = ParagraphStyle("note", parent=ss["Normal"], fontSize=8, fontName="Helvetica-Oblique", textColor=C["grey"], spaceAfter=3, leftIndent=6) warn = ParagraphStyle("warn", parent=ss["Normal"], fontSize=8.5, fontName="Helvetica", textColor=C["dark_blue"], spaceAfter=4, leftIndent=8, rightIndent=8, leading=13, borderPad=6, backColor=C["light_amber"]) return h2, note, warn # ══════════════════════════════════════════════════════════════════════════════ # BUILD # ══════════════════════════════════════════════════════════════════════════════ def build(path): doc = SimpleDocTemplate( path, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=18*mm, bottomMargin=18*mm, title="Blood Test Interpretation Quick Reference", ) PW = W - 40*mm H2, NOTE, WARN = styles() s = [] def sec(title, sub="", bg=None): s.append(Banner(title, sub, bg or C["dark_blue"], PW)) s.append(Spacer(1, 5)) def leg(): s.append(Legend(PW)); s.append(Spacer(1, 5)) def sp(n=6): s.append(Spacer(1, n)) # ── Cover ─────────────────────────────────────────────────────────────── s.append(Cover(PW, H - 40*mm - 8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 1. FBC # ════════════════════════════════════════════════ sec("1. Full Blood Count (FBC / CBC)", "Haemoglobin • MCV • White Cell Differential • Platelets • Pancytopenia", C["med_blue"]) s.append(Paragraph("1a. Anaemia Classification by MCV", H2)); leg() s.append(Flow([ {"type":"start", "label":"Haemoglobin LOW (Male <130 g/L | Female <120 g/L)"}, {"type":"decision", "label":"What is the MCV?"}, {"type":"note", "label":"MCV <80 fL → MICROCYTIC\nMCV 80–100 fL → NORMOCYTIC\nMCV >100 fL → MACROCYTIC"}, ], PW)); sp() s.append(Dual( ln=[ {"type":"start", "label":"Microcytic (MCV <80 fL)"}, {"type":"decision", "label":"Serum ferritin?"}, {"type":"result", "label":"LOW ferritin\n→ Iron Deficiency Anaemia\n(investigate bleeding source)", "color":C["red"]}, {"type":"result", "label":"Normal / High ferritin\n→ Thalassaemia\n→ Anaemia of chronic disease\n→ Sideroblastic anaemia", "color":C["teal"]}, ], rn=[ {"type":"start", "label":"Macrocytic (MCV >100 fL)"}, {"type":"decision", "label":"Serum B12 and Folate?"}, {"type":"result", "label":"LOW B12 or Folate\n→ Megaloblastic anaemia\n(pernicious, malabsorption)", "color":C["red"]}, {"type":"result", "label":"Normal B12 & Folate\n→ Alcohol / Liver disease\n→ Hypothyroidism\n→ Drugs (methotrexate, hydroxyurea)", "color":C["teal"]}, ], lt="MICROCYTIC", rt="MACROCYTIC", width=PW, )); sp() s.append(Flow([ {"type":"start", "label":"Normocytic Anaemia (MCV 80–100 fL)"}, {"type":"decision", "label":"Reticulocyte count?"}, {"type":"note", "label":"HIGH reticulocytes → Haemolysis or acute blood loss (marrow responding)\n Confirm haemolysis: ↑LDH, ↑indirect bilirubin, ↓haptoglobin, positive direct Coombs test\nLOW reticulocytes → Marrow failure, chronic kidney disease, anaemia of chronic disease"}, ], PW)); sp() s.append(Paragraph("1b. White Cell 5-Part Differential", H2)) s.append(tbl([ ["Cell", "Normal", "Elevated — causes", "Decreased — causes"], ["Neutrophils", "2.0–7.0 ×10⁹/L", "Bacterial infection, steroids,\ntrauma, CML", "Viral infection, drug toxicity,\napplastic anaemia, B12/folate deficiency"], ["Lymphocytes", "1.5–4.0 ×10⁹/L", "Viral (EBV, CMV, HIV), CLL, TB", "HIV/AIDS, steroids, SLE,\npost-chemotherapy"], ["Eosinophils", "0.04–0.4 ×10⁹/L","Parasites, allergy, asthma,\nHodgkin's, Addison's","Corticosteroids, acute infection"], ["Monocytes", "0.2–1.0 ×10⁹/L", "TB, malaria, monocytic leukaemia", "Hairy cell leukaemia"], ["Basophils", "0–0.1 ×10⁹/L", "CML, myeloproliferative, allergy", "Steroids, hyperthyroidism"], ], [68, 72, 195, 180], fs=8)); sp() s.append(Paragraph("1c. Platelet Count & Pancytopenia", H2)) s.append(tbl([ ["Finding", "Threshold", "Common Causes", "Action"], ["Thrombocytopenia", "<150 ×10⁹/L", "ITP, HIT (heparin), DIC, hypersplenism,\nmarrow failure, drug-induced","If <50: bleeding risk; if HIT suspected: stop heparin immediately"], ["Thrombocytosis", ">400 ×10⁹/L", "Reactive (infection, iron def., post-surgery)\nOR Essential Thrombocythaemia (ET)","Exclude reactive cause; persistent → haematology referral"], ["Pancytopenia\n(all 3)", "All low", "Aplastic anaemia, acute leukaemia, MDS, myeloma,\nBM infiltration (TB, lymphoma), megaloblastic","Urgent bone marrow biopsy required"], ], [80, 72, 215, 148], hbg=C["red"], abg=C["light_red"], fs=8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 2. LFTs # ════════════════════════════════════════════════ sec("2. Liver Function Tests (LFTs)", "ALT • AST • ALP • GGT • Bilirubin • Albumin • PT/INR", C["teal"]) s.append(Paragraph("2a. LFT Pattern Interpretation", H2)); leg() s.append(Flow([ {"type":"start", "label":"Abnormal LFTs"}, {"type":"decision", "label":"Which markers are predominantly elevated?"}, {"type":"note", "label":"↑↑↑ ALT / AST (>5× ULN) → Hepatocellular damage (hepatitis, ischaemia, paracetamol OD)\n↑↑ ALP + GGT → Cholestatic / Obstructive pattern\n↑ Bilirubin (unconjugated) → Haemolysis or Gilbert's syndrome\nLow Albumin + ↑ PT/INR → Liver synthetic failure (cirrhosis, fulminant hepatic failure)"}, ], PW)); sp() s.append(Dual( ln=[ {"type":"start", "label":"Hepatocellular Pattern\n(↑↑ ALT, AST)"}, {"type":"decision", "label":"AST : ALT ratio?"}, {"type":"result", "label":">2:1 (AST dominant)\n→ Alcoholic hepatitis\n(GGT also ↑↑↑)", "color":C["red"]}, {"type":"result", "label":"<1:1 (ALT dominant)\n→ Viral hepatitis\n→ NAFLD / NASH\n→ Drug-induced / paracetamol", "color":C["teal"]}, {"type":"note", "label":"ALT >1000 U/L:\nIschaemic hepatitis,\nparacetamol OD, acute viral"}, ], rn=[ {"type":"start", "label":"Cholestatic Pattern\n(↑↑ ALP, GGT)"}, {"type":"decision", "label":"GGT elevated?"}, {"type":"result", "label":"ALP ↑, GGT normal\n→ Bone source:\nPaget's, bone mets,\npregnancy", "color":C["amber"]}, {"type":"result", "label":"ALP ↑ + GGT ↑\n→ Biliary obstruction\n→ PBC / PSC / Drugs\n→ Intrahepatic cholestasis", "color":C["teal"]}, {"type":"note", "label":"First-line imaging:\nUltrasound abdomen\n(check bile ducts)"}, ], lt="HEPATOCELLULAR", rt="CHOLESTATIC", width=PW, )); sp() s.append(Paragraph("2b. Bilirubin — Conjugated vs Unconjugated", H2)) s.append(Flow([ {"type":"start", "label":"Raised Total Bilirubin (Jaundice visible >35 µmol/L)"}, {"type":"decision", "label":"Mainly unconjugated (indirect) OR conjugated (direct)?"}, {"type":"note", "label":"Mainly UNCONJUGATED → Pre-hepatic:\n Haemolysis (+ ↑LDH, ↑reticulocytes, ↓haptoglobin) | Gilbert's syndrome | Neonatal jaundice\n\nMainly CONJUGATED → Hepatic or Post-hepatic:\n Hepatitis, cirrhosis, biliary obstruction, cholangitis, drugs, sepsis"}, {"type":"result", "label":"Gilbert's syndrome: benign, isolated mild unconjugated bilirubinaemia triggered by fasting or illness. No treatment needed."}, ], PW)); sp() s.append(Paragraph("2c. LFT Pattern Summary Table", H2)) s.append(tbl([ ["Condition", "ALT/AST", "ALP", "GGT", "Bilirubin", "Albumin", "PT/INR"], ["Acute viral hepatitis","↑↑↑", "↑", "↑", "↑ direct", "Normal", "↑ (severe)"], ["Alcoholic hepatitis", "↑↑ AST>ALT", "↑", "↑↑↑", "↑", "↓", "↑"], ["Biliary obstruction", "↑", "↑↑↑", "↑↑", "↑↑ direct", "Normal", "Normal"], ["Cirrhosis", "↑/Normal", "↑", "↑", "↑ (late)", "↓↓", "↑↑"], ["NAFLD/NASH", "↑ ALT>AST", "↑", "↑", "Normal", "Normal", "Normal"], ["Haemolysis", "Normal", "Normal", "Normal","↑ indirect", "Normal", "Normal"], ["Gilbert's syndrome", "Normal", "Normal", "Normal","↑ indirect", "Normal", "Normal"], ["Liver metastases", "Normal/↑", "↑↑", "↑↑", "↑ (late)", "↓", "Normal/↑"], ], [105, 55, 42, 42, 72, 58, 60], hbg=C["teal"], abg=C["light_teal"], fs=8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 3. RFTs # ════════════════════════════════════════════════ sec("3. Renal Function Tests (RFTs)", "Urea • Creatinine • eGFR • AKI Classification • CKD Staging", C["med_blue"]) s.append(Paragraph("3a. Raised Creatinine / AKI Flowchart", H2)); leg() s.append(Flow([ {"type":"start", "label":"Raised Creatinine or Urea"}, {"type":"decision", "label":"Urea : Creatinine ratio = Urea (mmol/L) ÷ Creatinine (mmol/L)"}, {"type":"note", "label":"> 100 → PRE-RENAL: Dehydration, GI haemorrhage, heart failure, sepsis, NSAIDs\n40–100 → Intrinsic renal disease or post-renal obstruction\n< 40 → Low urea production (liver failure, low-protein diet)"}, {"type":"decision", "label":"Is the creatinine rise ACUTE or CHRONIC?"}, {"type":"result", "label":"AKI: Creatinine rise ≥26 µmol/L within 48h OR ≥1.5× baseline within 7 days\nCKD: eGFR <60 mL/min/1.73m² for more than 3 months"}, ], PW)); sp() s.append(Paragraph("3b. AKI Classification", H2)) s.append(tbl([ ["AKI Type", "Mechanism", "Common Causes", "Urine Na", "FENa"], ["Pre-renal", "↓ Renal perfusion", "Dehydration, haemorrhage,\nHF, sepsis, hepatorenal","<20 mmol/L","<1%"], ["Intrinsic", "Direct renal damage","ATN (ischaemia / nephrotoxins)\nGlomerulonephritis, vasculitis, myoglobinuria",">40 mmol/L",">2%"], ["Post-renal", "Obstruction", "BPH, renal calculi, pelvic malignancy,\nretroperitoneal fibrosis",">40 mmol/L",">2%"], ], [62, 80, 195, 80, 45], hbg=C["red"], abg=C["light_red"], fs=8)); sp() s.append(Paragraph("3c. CKD Staging by eGFR", H2)) s.append(tbl([ ["Stage","eGFR (mL/min/1.73m²)","Description", "Management"], ["G1", "≥90 (+ markers)", "Normal or high", "Treat risk factors (HTN, DM); annual review"], ["G2", "60–89", "Mildly reduced", "Annual monitoring; lifestyle modification"], ["G3a", "45–59", "Mildly–moderately ↓", "6-monthly; nephrology if rapid progression"], ["G3b", "30–44", "Moderately reduced", "Nephrology referral; manage CKD complications"], ["G4", "15–29", "Severely reduced", "Prepare for RRT; dietitian; anaemia management"], ["G5", "<15 or dialysis", "Kidney failure", "Renal replacement therapy (dialysis / transplant)"], ], [36, 90, 120, 269], fs=8.5)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 4. ELECTROLYTES # ════════════════════════════════════════════════ sec("4. Electrolytes & Anion Gap", "Na • K • Cl • HCO₃ • Ca • Mg • Phosphate • Anion Gap", C["purple"]) s.append(Paragraph("4a. Sodium Interpretation", H2)); leg() s.append(Dual( ln=[ {"type":"start", "label":"Hyponatraemia\nNa⁺ <135 mmol/L"}, {"type":"decision", "label":"Volume status?"}, {"type":"result", "label":"Hypovolaemic:\nVomiting, diarrhoea,\ndiuretics, Addison's", "color":C["red"]}, {"type":"result", "label":"Euvolaemic:\nSIADH, hypothyroidism,\npsychogenic polydipsia", "color":C["teal"]}, {"type":"result", "label":"Hypervolaemic:\nHeart failure, cirrhosis,\nnephrotic syndrome", "color":C["amber"]}, ], rn=[ {"type":"start", "label":"Hypernatraemia\nNa⁺ >145 mmol/L"}, {"type":"decision", "label":"Water loss or Na gain?"}, {"type":"result", "label":"Water loss:\nDehydration,\nDiabetes insipidus,\nexcessive sweating", "color":C["red"]}, {"type":"result", "label":"Sodium gain:\nExcess saline infusion,\nCushing's, Conn's", "color":C["teal"]}, {"type":"note", "label":"Correct slowly!\nMax 10–12 mmol/L / 24h\nto prevent osmotic\ndemyelination syndrome"}, ], lt="HYPONATRAEMIA", rt="HYPERNATRAEMIA", width=PW, )); sp() s.append(Paragraph("4b. Potassium Interpretation", H2)) s.append(Dual( ln=[ {"type":"start", "label":"Hypokalaemia K⁺ <3.5 mmol/L"}, {"type":"result", "label":"GI loss: vomiting, diarrhoea\nRenal loss: thiazide / loop diuretics\nCushing's / Conn's syndrome\nAlkalosis, insulin excess, low Mg", "color":C["red"]}, {"type":"note", "label":"ECG: U waves, flat T waves,\nlong QT → arrhythmia risk"}, {"type":"result", "label":"Replace K⁺ (oral or IV)\nAlways correct Mg concurrently", "color":C["green"]}, ], rn=[ {"type":"start", "label":"Hyperkalaemia K⁺ >5.0 mmol/L"}, {"type":"result", "label":"Renal failure (AKI or CKD)\nACEi / ARBs / Spironolactone\nAddison's disease, acidosis\nRhabdomyolysis, haemolysis", "color":C["red"]}, {"type":"note", "label":"ECG: Tall T waves, wide QRS\nsine wave pattern\nEMERGENCY if K⁺ >6.5"}, {"type":"result", "label":"URGENT: IV Calcium gluconate\n(stabilise myocardium)\nThen insulin/dextrose, salbutamol", "color":C["green"]}, ], lt="HYPOKALAEMIA", rt="HYPERKALAEMIA", width=PW, )); sp() s.append(Paragraph("4c. Anion Gap & Metabolic Acidosis", H2)) s.append(Flow([ {"type":"start", "label":"Metabolic Acidosis (↓ HCO₃⁻, ↓ pH on ABG/VBG)"}, {"type":"decision", "label":"Calculate Anion Gap = Na − (Cl + HCO₃) Normal: 8–12 mmol/L"}, {"type":"note", "label":"HIGH ANION GAP (>12) → MUDPILES mnemonic:\n Methanol | Uraemia | DKA | Propylene glycol | Isoniazid / Iron overdose\n Lactic acidosis | Ethylene glycol | Salicylates\n\nNORMAL ANION GAP (hyperchloraemic acidosis):\n Diarrhoea | Renal tubular acidosis (RTA) | Acetazolamide | Addison's disease"}, {"type":"result", "label":"Always confirm with ABG / VBG: pH, pCO₂, HCO₃⁻, base excess, lactate"}, ], PW)); sp() s.append(Paragraph("4d. Calcium (always correct for albumin: Ca + 0.02 × [40 − Albumin g/L])", H2)) s.append(tbl([ ["Condition", "Ca²⁺","PTH","PO₄","ALP","Common Causes"], ["Primary hyperPTH", "↑", "↑↑", "↓", "↑", "Parathyroid adenoma (80%), hyperplasia"], ["Malignancy", "↑", "↓", "↑/N","↑", "PTHrP (solid tumours), bone mets, myeloma"], ["Sarcoidosis / Vit D OD","↑", "↓", "↑", "N", "Granulomatous disease, Vit D toxicity"], ["Hypoparathyroidism", "↓", "↓", "↑", "N", "Post-thyroid surgery, autoimmune"], ["Vitamin D deficiency", "↓/N", "↑↑", "↓", "↑", "Malabsorption, lack of sunlight, CKD"], ["CKD-MBD", "↓", "↑↑", "↑", "↑", "Secondary hyperPTH, renal osteodystrophy"], ], [115, 35, 35, 35, 38, 257], hbg=C["purple"], abg=C["light_purple"], fs=8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 5. GLUCOSE # ════════════════════════════════════════════════ sec("5. Blood Glucose & HbA1c", "Fasting glucose • Random glucose • OGTT • HbA1c • Hypoglycaemia", C["green"]) s.append(Paragraph("5a. Diabetes Diagnosis Flowchart", H2)); leg() s.append(Flow([ {"type":"start", "label":"Assess Blood Glucose"}, {"type":"decision", "label":"Is the patient symptomatic?\n(polyuria, polydipsia, unexplained weight loss, recurrent infections)"}, {"type":"note", "label":"SYMPTOMATIC → ONE abnormal result is sufficient to diagnose diabetes\nASYMPTOMATIC → TWO abnormal results on separate occasions are required"}, {"type":"decision", "label":"Diagnostic thresholds"}, {"type":"note", "label":"Fasting glucose ≥7.0 mmol/L → Diabetes mellitus\nRandom glucose ≥11.1 mmol/L → Diabetes mellitus\nHbA1c ≥48 mmol/mol (6.5%) → Diabetes mellitus\n2h post-OGTT (75g) ≥11.1 mmol/L → Diabetes mellitus\n\nFasting glucose 6.1–6.9 mmol/L → Impaired Fasting Glucose (IFG)\n2h post-OGTT 7.8–11.0 mmol/L → Impaired Glucose Tolerance (IGT)\nHbA1c 42–47 mmol/mol → Pre-diabetes"}, {"type":"result", "label":"Both IFG and IGT = highest risk for T2DM and cardiovascular disease\nIntervene: lifestyle modification; consider metformin"}, ], PW)); sp() s.append(Paragraph("5b. HbA1c Monitoring (diagnosed diabetic)", H2)) s.append(tbl([ ["HbA1c (%)", "HbA1c (mmol/mol)", "Interpretation", "Action"], ["<6.5%", "<48", "Good glycaemic control", "Continue current regimen; annual review"], ["6.5–7.5%", "48–58", "Acceptable target (most T2DM patients)", "Reinforce lifestyle; review medications"], ["7.5–9.0%", "58–75", "Sub-optimal control", "Intensify therapy (add agent or titrate dose)"], [">9.0%", ">75", "Poor control — high complication risk", "Urgent review; insulin likely required"], ], [65, 78, 200, 172], hbg=C["green"], abg=C["light_green"], fs=8.5)); sp() s.append(Paragraph("5c. Hypoglycaemia (<3.9 mmol/L) — Causes", H2)) s.append(tbl([ ["Category", "Causes"], ["Drugs", "Insulin overdose, sulphonylureas (glibenclamide, gliclazide), alcohol, quinine, pentamidine"], ["Endocrine", "Insulinoma, Addison's disease, hypopituitarism, non-islet cell tumour hypoglycaemia"], ["Hepatic", "Acute liver failure, alcohol toxicity, Reye's syndrome, glycogen storage diseases"], ["Other", "Starvation, critical illness (sepsis, post-cardiac surgery), post-gastric bypass dumping syndrome"], ], [80, 435], hbg=C["green"], abg=C["light_green"], fs=8.5)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 6. TFTs # ════════════════════════════════════════════════ sec("6. Thyroid Function Tests (TFTs)", "TSH • Free T4 • Free T3 • TPO Antibodies • TSH Receptor Antibodies", C["med_blue"]) s.append(Paragraph("6a. TFT Interpretation Flowchart", H2)); leg() s.append(Flow([ {"type":"start", "label":"Check TSH (best initial screening test)"}, {"type":"decision", "label":"TSH level?"}, {"type":"note", "label":"TSH NORMAL (0.3–4.5 mU/L) → Likely euthyroid\nTSH HIGH (>4.5 mU/L) → Check Free T4 (hypothyroid or subclinical)\nTSH LOW (<0.3 mU/L) → Check Free T4 and Free T3 (hyperthyroid or subclinical)"}, ], PW)); sp() s.append(Dual( ln=[ {"type":"start", "label":"TSH HIGH (>4.5 mU/L)"}, {"type":"decision", "label":"Free T4?"}, {"type":"result", "label":"fT4 LOW\n→ Primary Hypothyroidism\n(Hashimoto's, iodine def.,\npost-thyroidectomy / RAI)", "color":C["red"]}, {"type":"result", "label":"fT4 NORMAL\n→ Subclinical Hypothyroidism\n(early / borderline)", "color":C["amber"]}, {"type":"note", "label":"Check TPO antibodies\nTreat if TSH >10 mU/L\nor if symptomatic"}, ], rn=[ {"type":"start", "label":"TSH LOW (<0.3 mU/L)"}, {"type":"decision", "label":"Free T4 / Free T3?"}, {"type":"result", "label":"fT4 / fT3 HIGH\n→ Primary Hyperthyroidism\n(Graves', toxic MNG,\ntoxic adenoma)", "color":C["red"]}, {"type":"result", "label":"fT4 / fT3 NORMAL\n→ Subclinical Hyperthyroidism\n(early Graves' or\nexcess levothyroxine)", "color":C["amber"]}, {"type":"note", "label":"TSH receptor Ab (TRAb)\nconfirms Graves' disease"}, ], lt="HIGH TSH", rt="LOW TSH", width=PW, )); sp() s.append(tbl([ ["Pattern", "TSH", "fT4", "fT3", "Diagnosis / Notes"], ["Primary hypothyroid", "↑↑", "↓", "↓", "Hashimoto's, iodine deficiency, post-RAI/surgery"], ["Subclinical hypothyroid","↑", "Normal", "Normal", "Early Hashimoto's; lithium, amiodarone"], ["Primary hyperthyroid", "↓↓", "↑", "↑", "Graves' disease, toxic nodule, toxic MNG"], ["Subclinical hyperthyroid","↓", "Normal", "Normal", "Early Graves'; excess levothyroxine dose"], ["Secondary hypothyroid", "↓/Normal","↓", "↓", "Pituitary failure (Sheehan's, craniopharyngioma)"], ["Sick euthyroid (NTI)", "Low/N", "↓", "↓↓", "Critical illness — low T3 syndrome. Do NOT treat."], ["Thyroid storm", "↓↓", "↑↑", "↑↑", "Emergency: fever, tachycardia, altered consciousness"], ], [120, 42, 42, 42, 269], fs=8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 7. INFLAMMATORY MARKERS # ════════════════════════════════════════════════ sec("7. Inflammatory Markers", "CRP • ESR • WBC • Procalcitonin • Ferritin • Fibrinogen", C["red"]) s.append(Paragraph("7a. CRP and ESR Interpretation", H2)); leg() s.append(Flow([ {"type":"start", "label":"Raised Inflammatory Markers (CRP / ESR / WBC)"}, {"type":"decision", "label":"CRP level?"}, {"type":"note", "label":"CRP 10–100 mg/L → Mild infection, autoimmune flare, post-surgery\nCRP 100–500 mg/L → Significant bacterial infection, major trauma\nCRP >500 mg/L → Severe sepsis, major burns, necrotising infection"}, {"type":"decision", "label":"CRP raised — is ESR also raised?"}, {"type":"note", "label":"High ESR + High CRP → Bacterial infection, vasculitis, PMR, RA, malignancy\nHigh ESR + Normal CRP → Myeloma / paraproteinaemia, SLE, anaemia, chronic disease\nHigh CRP + Normal/Low ESR → Early viral infection, mild / early inflammation"}, {"type":"result", "label":"CRP >100 not responding to antibiotics → Consider: resistant organism, undrained collection,\nfungal infection, non-infective cause (PE, MI, Still's disease, malignancy)"}, ], PW)); sp() s.append(tbl([ ["Marker", "Normal", "Kinetics", "Key Uses", "Limitations"], ["CRP", "<5 mg/L", "Rises 6–12h;\nfalls quickly","Acute infection; monitors treatment response","Non-specific; any inflammation"], ["ESR", "M: <15 mm/h\nF: <20 mm/h","Rises over days;\nslower to fall","PMR, myeloma, TB,\nchronic inflammatory disease","Affected by anaemia, pregnancy,\nhypergammaglobulinaemia, age"], ["Procalcitonin","<0.1 µg/L", "Rises 3–4h", ">0.5 = possible bacterial infection\n>2.0 = probable sepsis","More specific than CRP;\nguides antibiotic de-escalation"], ["Ferritin", "M: 20–300 µg/L\nF: 10–200 µg/L","Acute phase\nreactant","Iron stores; very high (>5000) → HLH,\nMacrophage Activation Syndrome","Unreliable for iron deficiency\nduring active inflammation"], ["Fibrinogen", "2.0–4.0 g/L", "Acute phase;\n↑ in inflammation","Complement to CRP/ESR;\nalso part of DIC workup","Falls in DIC and liver disease"], ], [75, 72, 65, 175, 128], hbg=C["red"], abg=C["light_red"], fs=8)) s.append(PageBreak()) # ════════════════════════════════════════════════ # 8. COAGULATION # ════════════════════════════════════════════════ sec("8. Coagulation Screen", "PT/INR • APTT • Fibrinogen • D-Dimer • Thrombin Time • DIC Score", C["dark_blue"]) s.append(Paragraph("8a. Coagulation Pathway Interpretation", H2)); leg() s.append(Flow([ {"type":"start", "label":"Abnormal Coagulation Screening"}, {"type":"decision", "label":"Which tests are prolonged?"}, {"type":"note", "label":"PT (INR) ↑ ONLY → Extrinsic pathway:\n Warfarin, Vitamin K deficiency, liver disease (early), Factor VII deficiency\n\nAPTT ↑ ONLY → Intrinsic pathway:\n Unfractionated heparin (UFH), Haemophilia A (↓FVIII), Haemophilia B (↓FIX)\n Lupus anticoagulant, von Willebrand disease, Factor XI / XII deficiency\n\nBOTH PT + APTT ↑ → Common pathway / multiple defects:\n DIC, Severe liver disease, Massive transfusion\n Direct thrombin inhibitors (dabigatran), Factor X / V / II deficiency"}, {"type":"decision", "label":"Both prolonged → check Fibrinogen + D-Dimer + Platelet count"}, {"type":"result", "label":"Fibrinogen ↓ + D-Dimer ↑ + Platelets ↓ → DIC (treat the underlying cause urgently)\nFibrinogen Normal + PT/APTT ↑ → Liver disease or anticoagulant effect"}, ], PW)); sp() s.append(tbl([ ["Test", "Normal Range", "Prolonged / Raised in", "Notes"], ["PT / INR", "INR 0.9–1.2\n(11–14 sec)","Warfarin, Vit K def., liver disease,\nDIC, Factor II/V/VII/X deficiency","INR >1.5 = significant; >3 = high bleeding risk"], ["APTT", "26–37 sec", "UFH, Haemophilia A/B, lupus anticoagulant,\nvWD, DIC","Mixing study: corrects → factor def.; doesn't correct → inhibitor"], ["Fibrinogen", "2.0–4.0 g/L", "Low in DIC, liver failure, massive transfusion,\nfibrinolysis","Rises as acute phase reactant (can mask depletion in DIC)"], ["D-Dimer", "<0.5 mg/L FEU", "VTE (DVT/PE), DIC, malignancy,\npregnancy, post-surgery, sepsis","Negative D-dimer effectively excludes PE/DVT\n(when pre-test probability is low)"], ["Thrombin Time","14–16 sec", "UFH, dabigatran, hypofibrinogenaemia,\nparaproteinaemia","Prolonged by even small amounts of heparin"], ], [68, 82, 205, 160], hbg=C["dark_blue"], abg=C["light_blue"], fs=8)); sp() s.append(Paragraph("8b. ISTH Overt DIC Score (score ≥5 = Overt DIC)", H2)) s.append(tbl([ ["Parameter", "Score 2", "Score 1", "Score 0"], ["Platelets", "<50 ×10⁹/L", "50–100 ×10⁹/L", ">100 ×10⁹/L"], ["D-Dimer / FDP", "Strong increase (>5 mg/L)", "Moderate increase", "No increase"], ["Prolonged PT", ">6 seconds", "3–6 seconds", "<3 seconds"], ["Fibrinogen", "—", "<1.0 g/L", "≥1.0 g/L"], ], [110, 145, 130, 130], hbg=C["red"], abg=C["light_red"], fs=8.5)) s.append(Paragraph("Score <5 = Non-overt DIC — repeat scoring in 24 hours if clinical suspicion remains.", NOTE)) s.append(PageBreak()) # ════════════════════════════════════════════════ # Summary page # ════════════════════════════════════════════════ sec("Quick Reference: Adult Normal Ranges", "", C["dark_blue"]) s.append(tbl([ ["Test", "Normal", "Test", "Normal"], ["Hb (male)", "130–175 g/L", "Sodium (Na⁺)", "135–145 mmol/L"], ["Hb (female)", "115–160 g/L", "Potassium (K⁺)", "3.5–5.0 mmol/L"], ["MCV", "80–100 fL", "Chloride (Cl⁻)", "95–107 mmol/L"], ["MCH", "27–33 pg", "Bicarbonate (HCO₃⁻)", "22–29 mmol/L"], ["WBC", "4.0–11.0 ×10⁹/L", "Calcium (corrected)", "2.2–2.6 mmol/L"], ["Neutrophils", "2.0–7.0 ×10⁹/L", "Phosphate", "0.8–1.5 mmol/L"], ["Lymphocytes", "1.5–4.0 ×10⁹/L", "Magnesium", "0.7–1.0 mmol/L"], ["Platelets", "150–400 ×10⁹/L", "Urea", "2.5–7.1 mmol/L"], ["Reticulocytes", "0.5–2.5%", "Creatinine (male)", "60–110 µmol/L"], ["ALT", "5–40 U/L", "Creatinine (female)", "45–90 µmol/L"], ["AST", "5–40 U/L", "eGFR", ">60 mL/min/1.73m²"], ["ALP", "30–130 U/L", "Fasting glucose", "3.9–6.0 mmol/L"], ["GGT", "5–55 U/L", "HbA1c (normal)", "<42 mmol/mol (<6%)"], ["Bilirubin (total)", "<21 µmol/L", "TSH", "0.3–4.5 mU/L"], ["Albumin", "35–50 g/L", "Free T4", "9–21 pmol/L"], ["Total Protein", "60–80 g/L", "Free T3", "3.5–7.8 pmol/L"], ["CRP", "<5 mg/L", "INR", "0.9–1.2"], ["ESR (male)", "<15 mm/h", "APTT", "26–37 sec"], ["ESR (female)", "<20 mm/h", "Fibrinogen", "2.0–4.0 g/L"], ["Procalcitonin", "<0.1 µg/L", "D-Dimer", "<0.5 mg/L FEU"], ["Ferritin (male)", "20–300 µg/L", "Anion Gap", "8–12 mmol/L"], ["Ferritin (female)", "10–200 µg/L", "Uric acid", "200–430 µmol/L"], ], [120, 100, 120, 100], fs=8.5)); sp(8) s.append(Paragraph( "IMPORTANT: Reference ranges vary between laboratories. Always use your local laboratory ranges. " "Interpret every result in the full clinical context — a 'normal' value may still be clinically significant. " "Trends over time are more informative than single values. " "Haemolysed, clotted, or delayed samples produce spurious results — repeat if in doubt.", WARN)) doc.build(s) print("Done:", path) build("/home/daytona/workspace/blood-tests-pdf/Blood_Test_Interpretation_QuickRef.pdf") PYEOF python /home/daytona/workspace/blood-tests-pdf/generate_pdf.py 2>&1
ls -lh /home/daytona/workspace/blood-tests-pdf/Blood_Test_Interpretation_QuickRef.pdf
Blood Test Interpretation QuickRef
PDF Document · PDF
| Page | Content |
|---|---|
| 1 | Cover page with colour branding and test index |
| 2 | FBC - Anaemia by MCV flowchart, dual micro/macrocytic branches, WBC differential table, platelet & pancytopenia tables |
| 3 | LFTs - Hepatocellular vs cholestatic dual flowchart, bilirubin conjugation flowchart, LFT pattern summary table |
| 4 | RFTs - Urea:Cr ratio flowchart, AKI classification table, CKD G1-G5 staging table |
| 5 | Electrolytes - Na hypo/hypernatraemia dual flowchart, K hypo/hyperkalaemia dual flowchart with ECG clues, anion gap MUDPILES flowchart, calcium causes table |
| 6 | Glucose & HbA1c - Diabetes diagnosis flowchart with OGTT thresholds, HbA1c monitoring table, hypoglycaemia causes table |
| 7 | TFTs - TSH-first algorithm, high/low TSH dual flowchart, full pattern table including sick euthyroid |
| 8 | Inflammatory Markers - CRP/ESR interpretation flowchart, marker comparison table (kinetics, uses, limitations) |
| 9 | Coagulation - PT/APTT pathway flowchart, coag test table, ISTH DIC scoring table |
| 10 | Normal Ranges Summary - All 44 tests in two-column quick-lookup format |