investigations done in general medicine

Reading File
Asking for Preferences
Asking for Preferences
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Reading File
Finding Sources
Reading File
Reading File
Now I have enough material from the textbooks to give a thorough, well-structured answer. Let me compile this.

Interpretation of Blood Tests in General Medicine

Here is a systematic guide to interpreting the main blood investigations used in clinical practice.

1. Full Blood Count (FBC / CBC)

The FBC is fully automated and provides three main groups of results.

Red Cell Parameters

ParameterWhat it MeasuresClinical Implication
Hb (Haemoglobin)Total Hb in bloodLow = anaemia
RCC (Red Cell Count)Number of RBCsCan be high even with low Hb (e.g. thalassaemia)
MCV (Mean Corpuscular Volume)Size of RBCLow = microcytic; High = macrocytic
MCH (Mean Corpuscular Haemoglobin)Amount of Hb per cellLow in iron deficiency
MCHCHb concentration inside cellsLow in iron deficiency, high in hereditary spherocytosis
Reticulocyte countYoung RBCs from bone marrowHigh = marrow actively producing RBCs (haemolysis or blood loss recovery)
Interpreting Anaemia by MCV:
  • Microcytic anaemia (low MCV): Iron deficiency, thalassaemia, anaemia of chronic disease, sideroblastic anaemia
  • Normocytic anaemia (normal MCV): Acute blood loss, anaemia of chronic disease, renal failure, haemolysis
  • Macrocytic anaemia (high MCV): B12/folate deficiency (megaloblastic), alcohol, hypothyroidism, liver disease, drugs (methotrexate, hydroxyurea)

White Cell Parameters (5-Part Differential)

CellNormal RangeHigh (causes)Low (causes)
Neutrophils2-7 x10⁹/LBacterial infection, steroids, stress, malignancyViral infection, drug toxicity, marrow failure
Lymphocytes1.5-4 x10⁹/LViral infection (EBV, CMV), CLL, TBHIV, steroids, SLE
Eosinophils0.04-0.4 x10⁹/LParasites, allergy, asthma, Addison'sSteroids
Monocytes0.2-1.0 x10⁹/LTB, malaria, inflammation-
Basophils0-0.1 x10⁹/LCML, allergic reactions-

Platelets

  • Thrombocytopenia (<150 x10⁹/L): ITP, heparin-induced, DIC, hypersplenism, marrow failure
  • Thrombocytosis (>400 x10⁹/L): Reactive (infection, iron deficiency, post-surgery), essential thrombocythaemia
Pancytopenia (low RBCs + WBCs + platelets) occurs from:
  • Bone marrow failure: aplastic anaemia, acute leukaemia, myelodysplasia, myeloma
  • Infiltration: lymphoma, TB, solid tumours
  • Megaloblastic anaemia
  • Increased peripheral destruction: hypersplenism, haemophagocytic syndrome
(Scott-Brown's Otorhinolaryngology Head & Neck Surgery, Vol 1)

2. Liver Function Tests (LFTs)

The liver has three functional systems that are tested by different markers.

Hepatocellular Damage Markers

TestSignificance
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
LDHRises in hepatocyte injury and necrosis
ALT:AST ratio >2 suggests alcoholic liver disease; if AST dominates, think MI or muscle disease.

Cholestatic / Obstructive Markers

TestSignificance
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

Synthetic Function Markers

TestSignificance
AlbuminReflects chronic liver synthetic function; falls only when >80% liver is destroyed
Total ProteinFollows albumin trends
PT / INRClotting factors made by liver; prolonged in hepatic failure
AmmoniaRises when >80% liver is destroyed; causes hepatic encephalopathy

Six Patterns of LFT Interpretation

  1. Acute hepatitis / necrosis - Markedly elevated ALT + AST, elevated direct bilirubin, normal ALP
  2. Cholestasis / biliary obstruction - Elevated ALP, GGT, bilirubin (mainly direct), mildly elevated transaminases
  3. Alcoholic liver disease - GGT disproportionately high, AST:ALT >2
  4. Cirrhosis - Low albumin, prolonged PT, variable bilirubin
  5. Hepatic infiltration/metastases - Elevated ALP, mildly raised transaminases
  6. Haemolysis - Elevated indirect (unconjugated) bilirubin, normal ALP, elevated LDH
(Henry's Clinical Diagnosis and Management by Laboratory Methods)

3. Renal Function Tests (RFTs)

TestNormal RangeInterpretation
Urea (BUN)2.5-7.1 mmol/LRaised in prerenal (dehydration, GI bleed), renal failure; low in liver disease
Creatinine60-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 acid200-430 µmol/LRaised in gout, myeloproliferative disease, renal failure
Urea:Creatinine Ratio:
  • 100 (or >20 in US units) = Prerenal cause (dehydration, GI bleed, heart failure)
  • Normal ratio = Intrinsic renal disease or postrenal obstruction

4. Electrolytes

ElectrolyteNormalHigh (causes)Low (causes)
Sodium (Na⁺)135-145 mmol/LDehydration, DI, excess NaClSIADH, hypothyroidism, heart failure, diarrhoea
Potassium (K⁺)3.5-5.0 mmol/LRenal failure, ACEi/ARBs, Addison's, acidosisDiarrhoea, diuretics, Cushing's, alkalosis
Chloride (Cl⁻)95-107 mmol/LDehydration, renal failureVomiting, metabolic alkalosis
Bicarbonate (HCO₃⁻)22-29 mmol/LMetabolic alkalosisMetabolic acidosis, diarrhoea
Calcium (Ca²⁺)2.2-2.6 mmol/LHyperparathyroidism, malignancy, sarcoid, Vit D toxicityHypoparathyroidism, Vit D deficiency, malabsorption
Phosphate0.8-1.5 mmol/LRenal failure, hypoparathyroidismMalnutrition, hyperparathyroidism, refeeding syndrome
Magnesium0.7-1.0 mmol/LRenal failureDiarrhoea, alcohol, diuretics, PPIs
Anion Gap = Na - (Cl + HCO₃), normal 8-12 mmol/L
  • Raised AG acidosis (MUDPILES): Methanol, Uraemia, DKA, Propylene glycol, Isoniazid/Iron, Lactic acidosis, Ethylene glycol, Salicylates
  • Normal AG (hyperchloraemic) acidosis: Diarrhoea, renal tubular acidosis, acetazolamide

5. Blood Glucose

TestInterpretation
Fasting glucose <6.1 mmol/LNormal
6.1-6.9 mmol/LImpaired fasting glucose (pre-diabetes)
≥7.0 mmol/L (on 2 occasions)Diabetes mellitus
Random glucose ≥11.1 mmol/LDiabetes mellitus (with symptoms)
HbA1c ≥48 mmol/mol (6.5%)Diabetes mellitus
HbA1c 42-47 mmol/molPre-diabetes

6. Thyroid Function Tests (TFTs)

PatternTSHFree T4Free T3Diagnosis
Primary hypothyroidismHighLowLowHashimoto's, iodine deficiency
Subclinical hypothyroidismHighNormalNormalEarly Hashimoto's
Primary hyperthyroidismLow/undetectableHighHighGraves', toxic nodule
Subclinical hyperthyroidismLowNormalNormalEarly Graves', excess thyroxine
Secondary hypothyroidismLow/normalLowLowPituitary failure

7. Inflammatory Markers

TestUsesLimitations
CRP (C-Reactive Protein)Rises within 6-12 hours of inflammation/infection; falls quickly on recoveryNon-specific
ESR (Erythrocyte Sedimentation Rate)Rises more slowly; useful in PMR, myeloma, TBAffected by age, sex, anaemia, pregnancy
WBCNeutrophilia = bacterial; Lymphocytosis = viral-
ProcalcitoninMore specific for bacterial sepsisExpensive, not universally available
FerritinAcute phase reactant; also measures iron storesVery high in haemophagocytic syndrome, Still's disease

8. Coagulation Screen

TestNormalProlonged in
PT / INRINR 0.9-1.2Warfarin, liver disease, Vit K deficiency, DIC
APTT26-37 secondsHeparin, haemophilia A/B, lupus anticoagulant, DIC
Fibrinogen2-4 g/LDecreased in DIC, liver failure (consumed)
D-dimer<0.5 mg/LRaised in VTE, DIC, malignancy, post-surgery (sensitive, not specific)

Key Clinical Principles

  1. Always interpret results in clinical context - a "normal" result may still be abnormal for a particular patient (e.g. a creatinine of 90 in a young muscular male vs. a frail elderly woman).
  2. Trends matter more than single values - a creatinine rising from 80 to 120 is more significant than a stable creatinine of 130.
  3. Check specimen quality - clotted, haemolysed, or delayed samples give spurious results, especially for potassium and LDH.
  4. Correct for confounders - albumin-corrected calcium, MCHC-adjusted MCV, and medication effects on results.

Create a quick reference PDF with interpretation flowcharts for each test

Reading File
Running Command

ls /home/daytona/workspace/ 2>/dev/null && mkdir -p /home/daytona/workspace/blood-tests-pdf

Writing File

~/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")
Running Command

cd /home/daytona/workspace/blood-tests-pdf && python generate_pdf.py 2>&1

Editing File

~/blood-tests-pdf/generate_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
Editing File

~/blood-tests-pdf/generate_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
Editing File

~/blood-tests-pdf/generate_pdf.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/blood-tests-pdf/generate_pdf.py'
Running Command

cd /home/daytona/workspace/blood-tests-pdf && python generate_pdf.py 2>&1

Reading File
Reading File
Writing File

~/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

Running Command

ls /home/daytona/workspace/blood-tests-pdf/

Running Command

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

Running Command

ls -lh /home/daytona/workspace/blood-tests-pdf/Blood_Test_Interpretation_QuickRef.pdf

PDF generated successfully (42 KB). Here it is:

Blood Test Interpretation QuickRef

PDF Document · PDF


What's inside (10 pages):
PageContent
1Cover page with colour branding and test index
2FBC - Anaemia by MCV flowchart, dual micro/macrocytic branches, WBC differential table, platelet & pancytopenia tables
3LFTs - Hepatocellular vs cholestatic dual flowchart, bilirubin conjugation flowchart, LFT pattern summary table
4RFTs - Urea:Cr ratio flowchart, AKI classification table, CKD G1-G5 staging table
5Electrolytes - Na hypo/hypernatraemia dual flowchart, K hypo/hyperkalaemia dual flowchart with ECG clues, anion gap MUDPILES flowchart, calcium causes table
6Glucose & HbA1c - Diabetes diagnosis flowchart with OGTT thresholds, HbA1c monitoring table, hypoglycaemia causes table
7TFTs - TSH-first algorithm, high/low TSH dual flowchart, full pattern table including sick euthyroid
8Inflammatory Markers - CRP/ESR interpretation flowchart, marker comparison table (kinetics, uses, limitations)
9Coagulation - PT/APTT pathway flowchart, coag test table, ISTH DIC scoring table
10Normal Ranges Summary - All 44 tests in two-column quick-lookup format
This is a shared conversation. Sign in to Orris to start your own chat.