Causes of 5 yr chronic dyspepsia with normal lft

Reading File
Finding Sources
Finding Sources
Searching PubMed

chronic dyspepsia etiology functional organic

Reading File
Reading File
Reading File
I now have a very thorough set of information from Harrison's 22nd edition. Let me compile a comprehensive, well-organized answer.

Causes of 5-Year Chronic Dyspepsia with Normal LFTs

Normal LFTs effectively rule out hepatocellular disease and significant biliary obstruction, but they do NOT exclude gallstones (cholecystolithiasis), biliary dyskinesia, or early/intermittent biliary pathology. The causes below are organized by likelihood.

1. Functional Dyspepsia (Most Common - 70-80% of cases)

The single most likely diagnosis in any chronic dyspepsia with no organic finding on workup.
  • Definition (Rome IV): Bothersome postprandial fullness, early satiety, or epigastric pain/burning for ≥6 months with no identifiable organic cause
  • Two subtypes:
    • Postprandial Distress Syndrome (PDS) - meal-induced fullness, early satiety (prevalence 6.1%)
    • Epigastric Pain Syndrome (EPS) - epigastric pain or burning, may be meal-unrelated (prevalence 2.4%)
  • Mechanisms: Impaired gastric accommodation, visceral hypersensitivity, delayed gastric emptying, CNS-gut axis dysregulation, hypothalamic-pituitary-adrenal axis dysfunction, altered bile salt composition
  • Associated conditions: IBS, fibromyalgia, chronic fatigue, anxiety, depression

2. Gastroesophageal Reflux Disease (GERD)

  • Heartburn/regurgitation affects 18-28% of the population
  • 5+ years of GERD symptoms warrant endoscopy to screen for Barrett's esophagus (6x increased risk vs <1 year of symptoms)
  • Can present as pure dyspepsia (epigastric burning/fullness) without classic heartburn
  • Normal LFTs expected

3. Peptic Ulcer Disease (PUD)

  • Gastric or duodenal ulcers cause a minority of dyspepsia cases
  • H. pylori infection - the most common cause of peptic ulcers; also plays a minor role in functional dyspepsia directly
  • NSAID use - second most common cause; consider if patient uses analgesics regularly
  • Rare causes: Crohn's disease, Zollinger-Ellison syndrome (gastrin-secreting tumor)

4. Gastroparesis

  • Delayed gastric emptying causes postprandial fullness, nausea, bloating, early satiety
  • 20% of gastroparesis patients report pain as the predominant symptom (mimicking dyspepsia without obvious nausea/vomiting)
  • Causes: diabetic autonomic neuropathy, post-viral, post-surgical, idiopathic
  • Normal LFTs expected

5. Biliary Causes (Normal LFTs possible)

  • Gallstone disease / biliary colic - LFTs can be entirely normal between episodes; usually presents as discrete RUQ/epigastric episodes rather than constant burning, but can mimic dyspepsia
  • Biliary dyskinesia (gallbladder dysmotility) - HIDA scan with CCK shows low ejection fraction; LFTs normal
  • Note: chronic, dull epigastric discomfort is less typical of biliary colic

6. Dietary Intolerances

  • Lactose intolerance - gas, bloating, discomfort (more common in Black and Asian populations)
  • Fructose/sorbitol intolerance - similar symptoms
  • Non-celiac gluten sensitivity - bloating, abdominal discomfort, dyspeptic symptoms without small bowel damage; LFTs normal

7. Celiac Disease

  • Can present with bloating, epigastric discomfort, and dyspeptic symptoms
  • LFTs can be mildly elevated (transaminases) or normal
  • Diagnosed by anti-tTG IgA + duodenal biopsy; check serology if not already done

8. Small Intestinal Bacterial Overgrowth (SIBO)

  • Causes dyspepsia, distention, bowel dysfunction, malabsorption
  • Diagnosed by glucose/lactulose hydrogen breath test
  • Normal LFTs expected

9. Pancreatic Disease

  • Chronic pancreatitis - episodic to constant epigastric pain, often post-prandial; LFTs usually normal unless biliary compression
  • Pancreatic malignancy - in a 5-year history this is less likely to be the full duration, but consider in older patients; early disease can have normal LFTs
  • Check serum amylase/lipase, CT abdomen if suspected

10. Drug/Substance-Induced

  • NSAIDs/analgesics - direct gastric mucosal irritation
  • Theophylline, nitrates, calcium channel blockers, progesterone - promote gastroesophageal reflux
  • Alcohol, tobacco, caffeine - lower LES tone, promote reflux
  • Review medication list carefully

11. Metabolic / Endocrine

  • Thyroid disease (hypo- or hyperthyroidism) - can cause dyspeptic symptoms; check TSH
  • Hyperparathyroidism - hypercalcemia causes nausea, abdominal pain, dyspepsia; check serum calcium
  • Normal LFTs expected

12. Less Common Causes to Consider

CauseKey Feature
Eosinophilic gastroenteritisEosinophilia, may need biopsy
Menétrier's diseaseProtein-losing gastropathy, giant rugae
Mesenteric ischemiaPostprandial pain, "food fear," weight loss
Sarcoidosis / mastocytosisSystemic features
Congestive heart failureHepatic congestion, may mimic dyspepsia
Abdominal wall painCarnett's sign positive

Practical Approach to Workup

Given 5 years of symptoms, the following are warranted if not yet done:
  1. H. pylori testing (urea breath test or stool antigen) - first step if not tested
  2. Upper endoscopy (EGD) - indicated given >5 years of dyspepsia, especially if age >50 or any alarm features (weight loss, dysphagia, bleeding, anemia)
  3. Abdominal ultrasound - rule out gallstones, biliary pathology
  4. Serology: TSH, serum calcium, anti-tTG IgA (celiac), fasting glucose
  5. Gastric emptying scan if gastroparesis suspected
  6. HIDA scan with CCK if biliary dyskinesia suspected
Alarm features that should prompt urgent EGD:
  • Unexplained weight loss
  • Recurrent vomiting
  • Dysphagia / odynophagia
  • Occult/overt GI bleeding
  • Iron deficiency anaemia
  • Jaundice (absent here)
  • Palpable mass / lymphadenopathy
  • Family history of GI malignancy

Sources: Harrison's Principles of Internal Medicine 22E (2025), Ch. 48 - Indigestion / Dyspepsia; Ch. 333 - GI Endoscopy, McGraw Hill Medical

Search for recent clinical guidelines on functional dyspepsia diagnosis and management (2024-2025)

Reading File
Searching PubMed

"functional dyspepsia"[MeSH Terms] AND "guideline"[pt]

Searching PubMed

"functional dyspepsia" AND management AND diagnosis

Searching the Web

functional dyspepsia clinical guidelines 2024 2025 ACG AGA Rome IV

Reading Article

PMID: 40628573

Reading Article

PMID: 41656163

Reading Article

PMID: 40504583

Reading Article

PMID: 39360697

Comprehensive data gathered. Here is a fully synthesized summary of the most current evidence.

Recent Clinical Guidelines on Functional Dyspepsia (2024-2026)

Several new consensus documents and guidelines have been published or updated in 2024-2025. Here is a synthesis of key recommendations across them.

Key Documents Identified

DocumentYearPMID
Italian Guidelines (SIGE/SINGEM/AIGO/SIED/SIMG) - Practice Guideline (Delphi, 24 statements, GRADE)202540628573
Belgian Consensus on FD Management - Delphi, 119 statements, GRADE202540504583
Second Asian Consensus on FD (ANMA/APAGE) - 32 statements, Delphi2025/202641656163
Current Opinion: FD - Olson, Lacy et al. - Review202439360697
BSG Guidelines on FD (Black et al., Gut)2022Still current reference standard
ACG/CAG Joint Guideline (Moayyedi et al.)2017Still widely cited

Definition & Diagnosis (Rome IV - Current Standard)

All 2024-2025 guidelines continue to use Rome IV criteria. FD is defined as at least one of the following, present for ≥6 months:
  • Postprandial fullness (bothersome, at least 3 days/week)
  • Early satiation (at least 3 days/week)
  • Epigastric pain (at least 1 day/week)
  • Epigastric burning (at least 1 day/week)
...in the absence of organic cause on workup.
Two subtypes:
  • Postprandial Distress Syndrome (PDS) - meal-triggered fullness/early satiety
  • Epigastric Pain Syndrome (EPS) - epigastric pain/burning, meal-independent or related
Key diagnostic points agreed across 2025 guidelines:
  • FD is a diagnosis of exclusion (Italian, Belgian consensus)
  • Patients should receive a positive diagnosis with explanation and reassurance, not just "exclusion of disease" (Belgian consensus)
  • Co-existence with IBS and other disorders of gut-brain interaction (DGBI) is common and expected
  • Bloating and weight loss can also occur in FD (Belgian consensus, 2025)

When to Perform EGD (Upper Endoscopy)

The 2025 Italian guidelines reached consensus on:
  • EGD with biopsies is required in patients aged ≥45 years, OR with alarm symptoms, OR who are refractory to treatment
  • EGD is NOT routine in young, low-risk patients without alarm features
Alarm features warranting prompt EGD:
  • Unintended weight loss
  • Dysphagia / odynophagia
  • Recurrent vomiting
  • GI bleeding / iron-deficiency anemia
  • Family history of upper GI malignancy
  • Palpable mass or lymphadenopathy
Both Italian and Belgian guidelines recommend against routine abdominal ultrasound, celiac screening, and motility studies in straightforward FD without alarm features.

H. pylori Testing

Strong consensus across all guidelines:
  • All FD patients should be tested for H. pylori (invasive or non-invasive - urea breath test or stool antigen preferred)
  • All H. pylori-positive patients should receive eradication therapy - "test and treat" remains first-line strategy
  • H. pylori eradication provides modest but real symptom benefit in a subset of FD patients (NNT ~14)

Pharmacological Treatment (2025 Updates)

First-Line:

Drug ClassRecommendationNotes
PPI (standard dose, short course)First-line for ALL FD subtypes (Italian, Belgian)Higher doses confer no added benefit
H. pylori eradicationAll HP-positive patientsAll guidelines agree

Second-Line / Subtype-Specific:

Drug ClassSubtypeRecommendation
Prokinetics (domperidone, metoclopramide, itopride)PDSShort course; NOT first-line; use preferentially in PDS subtype (Italian guideline)
Tricyclic antidepressants (amitriptyline, nortriptyline)EPSRecommended for EPS (Italian guideline) - gut neuromodulator effect
MirtazapineFD with weight lossRecommended, particularly when weight loss is a feature (Italian guideline)
SSRIsGeneral FDInsufficient evidence; not recommended routinely
H2 receptor antagonists / antacidsGeneralNo consensus on routine use (Italian); not first-line

Newer / Emerging Therapies (2024-2025 updates):

  • Potassium-competitive acid blockers (P-CABs): Emerging data, especially in Asia; Asian consensus acknowledges limited scope of current studies
  • Probiotics: Microbiome-modulation is a research target; promising but not yet guideline-standard (Olson et al., 2024)
  • Low-FODMAP diet: Some benefit; being studied in large trials (Olson et al., 2024)
  • Gut-directed hypnotherapy / virtual reality gut-brain therapy: Novel delivery methods showing promising results (Olson et al., 2024)
  • Antibiotics (rifaximin): Exploratory target given SIBO-FD overlap; not currently recommended routinely (Belgian consensus recommends against antibiotics)

Non-Pharmacological Management

All 2025 guidelines emphasize:
  • Explanation and reassurance are the cornerstone of management (Belgian consensus)
  • Healthy lifestyle advice is appropriate and recommended
  • Exclusion / restrictive diets are discouraged (Italian and Belgian both advise against)
  • Cognitive behavioural therapy (CBT) is recommended for patients who fail medical therapy (Italian guideline)
  • Osteopathy and most complementary/alternative medicine: insufficient evidence to recommend (Italian, Belgian)
The gut-brain interaction model is the dominant conceptual framework in all 2025 documents - FD is explicitly categorized as a DGBI, not a purely organic or purely psychiatric disorder.

Pathophysiology Updates (2024-2025)

Recent research has expanded understanding of FD mechanisms:
  • Intestinal microbiome alterations and impaired intestinal membrane integrity (leaky gut) are now recognized as potentially important (Olson et al., 2024)
  • CNS-gut axis dysregulation (functional MRI evidence of altered brain activation)
  • Altered bile salt composition
  • Duodenal low-grade inflammation and eosinophilia
  • Post-infectious FD (post-COVID-19 gut dysfunction is an emerging area)

Summary Table: 2025 Guideline Recommendations at a Glance

DomainRecommendation
DiagnosisRome IV criteria; FD = diagnosis of exclusion
H. pyloriTest all; treat if positive
EGDAge ≥45, alarm features, or treatment refractory
Routine imaging/motilityNOT recommended routinely
First-line drugShort-course standard-dose PPI
PDS subtypeProkinetics (short course, not first-line)
EPS subtypeTricyclic antidepressants
Weight loss + FDMirtazapine
Refractory FDCBT; reassess diagnosis
DietGeneral healthy advice; exclusion diets discouraged
Invasive/surgical treatmentRecommended against

Create a patient management flowchart for functional dyspepsia based on 2025 guidelines

Running Command

ls -la /home/daytona/workspace/ 2>/dev/null && python3 -c "import matplotlib; print(matplotlib.__version__)"

Running Command

mkdir -p /home/daytona/workspace/fd-flowchart && python3 -c "import matplotlib.patches as mpatches; import matplotlib; print('OK')"

Writing File

~/fd-flowchart/build_flowchart.py

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch

fig, ax = plt.subplots(figsize=(20, 34))
ax.set_xlim(0, 20)
ax.set_ylim(0, 34)
ax.axis('off')
fig.patch.set_facecolor('#F8F9FA')

# ── colour palette ──────────────────────────────────────────────────────────
C_TITLE   = '#1A237E'   # dark navy
C_START   = '#1565C0'   # blue  – start/end ovals
C_ALARM   = '#B71C1C'   # red   – alarm box
C_DECISION= '#E65100'   # orange – diamond decisions
C_ACTION  = '#1B5E20'   # green – treatment boxes
C_SUBTYPE = '#4A148C'   # purple – subtype boxes
C_REFRACTORY = '#880E4F' # dark pink – refractory path
C_BOX     = '#E3F2FD'   # light blue – general boxes
C_ALARM_BG= '#FFEBEE'
C_SUBTYPE_BG = '#F3E5F5'
C_ACTION_BG = '#E8F5E9'
C_REFRAC_BG = '#FCE4EC'
C_DECISION_BG = '#FFF3E0'
C_TEXT    = '#212121'

# ── helper functions ─────────────────────────────────────────────────────────

def rounded_box(ax, x, y, w, h, text, fc='#E3F2FD', ec='#1565C0', tc='#212121',
                fontsize=11, bold=False, radius=0.4, lw=1.8, valign='center'):
    box = FancyBboxPatch((x - w/2, y - h/2), w, h,
                         boxstyle=f"round,pad=0.05,rounding_size={radius}",
                         facecolor=fc, edgecolor=ec, linewidth=lw, zorder=3)
    ax.add_patch(box)
    weight = 'bold' if bold else 'normal'
    ax.text(x, y, text, ha='center', va=valign, fontsize=fontsize,
            color=tc, weight=weight, zorder=4,
            wrap=True, multialignment='center',
            bbox=dict(boxstyle='square,pad=0', facecolor='none', edgecolor='none'))

def diamond(ax, x, y, w, h, text, fc=C_DECISION_BG, ec=C_DECISION, tc=C_TEXT, fontsize=10):
    dx, dy = w/2, h/2
    pts = [(x, y+dy), (x+dx, y), (x, y-dy), (x-dx, y)]
    poly = plt.Polygon(pts, closed=True, facecolor=fc, edgecolor=ec, linewidth=1.8, zorder=3)
    ax.add_patch(poly)
    ax.text(x, y, text, ha='center', va='center', fontsize=fontsize,
            color=tc, weight='bold', zorder=4, multialignment='center')

def arrow(ax, x1, y1, x2, y2, color='#424242', label='', lw=1.8):
    ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle='->', color=color, lw=lw,
                                connectionstyle='arc3,rad=0.0'),
                zorder=2)
    if label:
        mx, my = (x1+x2)/2, (y1+y2)/2
        ax.text(mx+0.15, my, label, fontsize=9, color=color, weight='bold', zorder=5)

def line(ax, x1, y1, x2, y2, color='#424242', lw=1.5, style='-'):
    ax.plot([x1, x2], [y1, y2], color=color, lw=lw, linestyle=style, zorder=2)

def label_at(ax, x, y, text, fontsize=9, color='#424242', bold=False):
    w = 'bold' if bold else 'normal'
    ax.text(x, y, text, fontsize=fontsize, color=color, weight=w,
            ha='center', va='center', zorder=5)

# ════════════════════════════════════════════════════════════════════════════
# LAYOUT  (y increases upward in matplotlib – we work top-to-bottom by
#          using high y values at top and decreasing downward)
# ════════════════════════════════════════════════════════════════════════════

# Title banner
title_box = FancyBboxPatch((0.3, 32.4), 19.4, 1.3,
                            boxstyle="round,pad=0.1,rounding_size=0.3",
                            facecolor=C_TITLE, edgecolor=C_TITLE, linewidth=0, zorder=3)
ax.add_patch(title_box)
ax.text(10, 33.15, 'Functional Dyspepsia – Patient Management Flowchart',
        ha='center', va='center', fontsize=17, color='white', weight='bold', zorder=4)
ax.text(10, 32.65, 'Based on Italian (SIGE/SINGEM, 2025), Belgian (2025) & Asian Consensus (2025) Guidelines  |  Rome IV Criteria',
        ha='center', va='center', fontsize=9, color='#B0BEC5', zorder=4)

# ── STEP 1: Patient presentation ────────────────────────────────────────────
Y = 31.5
rounded_box(ax, 10, Y, 11, 0.75,
            'Patient presents with UPPER ABDOMINAL SYMPTOMS\n'
            '(epigastric pain/burning, postprandial fullness, early satiety, nausea)',
            fc='#BBDEFB', ec=C_START, tc=C_TITLE, fontsize=11, bold=True)

arrow(ax, 10, Y-0.38, 10, Y-0.38-0.52)

# ── STEP 2: Duration check ───────────────────────────────────────────────────
Y = 30.4
diamond(ax, 10, Y, 7, 0.85,
        'Symptoms ≥ 6 months?\n(Rome IV threshold)',
        fontsize=10)

# No branch – refer/reassess
arrow(ax, 13.5, Y, 17.5, Y, label='NO')
rounded_box(ax, 18.5, Y, 2.2, 0.7,
            'Reassess / follow up\nshort-term (<6 months)',
            fc='#ECEFF1', ec='#607D8B', tc='#37474F', fontsize=9)

# Yes branch down
arrow(ax, 10, Y-0.43, 10, Y-0.43-0.52, label='')
label_at(ax, 10.3, Y-0.6, 'YES', bold=True, color=C_START)

# ── STEP 3: Alarm features? ──────────────────────────────────────────────────
Y = 29.2
diamond(ax, 10, Y, 7.5, 0.9,
        'ALARM FEATURES present?\n(see box →)',
        ec=C_ALARM, fc=C_ALARM_BG, fontsize=10)

# Alarm box on the right
rounded_box(ax, 17.2, Y, 4.8, 1.8,
            'Alarm Features:\n• Unintended weight loss\n• Dysphagia / odynophagia\n'
            '• GI bleeding / iron-deficiency anaemia\n• Recurrent vomiting\n'
            '• Age ≥ 45 at first presentation\n• Palpable mass / lymphadenopathy\n'
            '• Family Hx upper GI malignancy',
            fc=C_ALARM_BG, ec=C_ALARM, tc='#B71C1C', fontsize=8.5, bold=False)
arrow(ax, 13.75, Y, 14.8, Y, color=C_ALARM, label='YES')

# No → continue
arrow(ax, 10, Y-0.45, 10, Y-0.45-0.5)
label_at(ax, 10.3, Y-0.65, 'NO', bold=True, color=C_START)

# ── STEP 4: H. pylori test ───────────────────────────────────────────────────
Y = 28.0
rounded_box(ax, 10, Y, 10, 0.8,
            'Routine bloods (FBC, TFTs, Ca²⁺, coeliac serology if indicated)\n'
            '+ TEST FOR H. pylori  (UBT or stool antigen)  — ALL patients',
            fc=C_BOX, ec=C_START, tc=C_TEXT, fontsize=10, bold=False)

arrow(ax, 10, Y-0.4, 10, Y-0.4-0.5)

# ── STEP 5: H. pylori positive? ─────────────────────────────────────────────
Y = 26.9
diamond(ax, 10, Y, 6, 0.85, 'H. pylori\npositive?', fontsize=10)

# Positive branch
arrow(ax, 7, Y, 4.5, Y, color=C_ALARM, label='')
label_at(ax, 5.7, Y+0.2, 'YES', bold=True, color=C_ALARM)
rounded_box(ax, 3.2, Y, 3.8, 0.85,
            'ERADICATE H. pylori\n(e.g. triple/quadruple therapy)\nReassess at 4-6 weeks',
            fc='#FFF9C4', ec='#F57F17', tc='#E65100', fontsize=9, bold=True)

# Negative / post-eradication → both lead to same next box
arrow(ax, 10, Y-0.43, 10, Y-0.43-0.52)
label_at(ax, 10.35, Y-0.62, 'NO / post-eradication', bold=False, color=C_START, fontsize=8)

# Connect H pylori treatment box down to same point
line(ax, 3.2, Y-0.43, 3.2, 26.2, color='#F57F17', lw=1.5)
line(ax, 3.2, 26.2, 10, 26.2, color='#F57F17', lw=1.5)

# ── STEP 6: Diagnose FD + Subtype ────────────────────────────────────────────
Y = 25.9
rounded_box(ax, 10, Y, 12, 0.75,
            'Confirm FUNCTIONAL DYSPEPSIA diagnosis  (Rome IV, diagnosis of exclusion)\n'
            'Provide positive explanation & reassurance  |  Healthy lifestyle advice',
            fc='#E8EAF6', ec=C_TITLE, tc=C_TITLE, fontsize=10, bold=True)

arrow(ax, 10, Y-0.38, 10, Y-0.38-0.45)

# ── STEP 7: Subtype ──────────────────────────────────────────────────────────
Y = 24.8
diamond(ax, 10, Y, 7, 0.85,
        'Identify SUBTYPE\n(predominant symptom pattern)',
        fc=C_SUBTYPE_BG, ec=C_SUBTYPE, fontsize=10)

# PDS left branch
arrow(ax, 6.5, Y, 3.8, Y, color=C_SUBTYPE)
label_at(ax, 5.1, Y+0.25, 'PDS', bold=True, color=C_SUBTYPE, fontsize=9)
label_at(ax, 5.0, Y-0.22, '(meal-related\nfullness/satiety)', bold=False, color=C_SUBTYPE, fontsize=7.5)

rounded_box(ax, 2.5, Y, 3.8, 1.0,
            'POSTPRANDIAL DISTRESS\nSYNDROME (PDS)\nPostprandial fullness\nEarly satiation',
            fc=C_SUBTYPE_BG, ec=C_SUBTYPE, tc=C_SUBTYPE, fontsize=9, bold=True)

# EPS right branch
arrow(ax, 13.5, Y, 16.2, Y, color=C_SUBTYPE)
label_at(ax, 14.85, Y+0.25, 'EPS', bold=True, color=C_SUBTYPE, fontsize=9)
label_at(ax, 14.85, Y-0.22, '(pain/burning\nnot meal-related)', bold=False, color=C_SUBTYPE, fontsize=7.5)

rounded_box(ax, 17.5, Y, 3.8, 1.0,
            'EPIGASTRIC PAIN\nSYNDROME (EPS)\nEpigastric pain\nEpigastric burning',
            fc=C_SUBTYPE_BG, ec=C_SUBTYPE, tc=C_SUBTYPE, fontsize=9, bold=True)

# Both merge down
arrow(ax, 2.5, Y-0.5, 2.5, 23.35)
line(ax, 2.5, 23.35, 10, 23.35, color='#424242', lw=1.5)
arrow(ax, 17.5, Y-0.5, 17.5, 23.35)
line(ax, 17.5, 23.35, 10, 23.35, color='#424242', lw=1.5)
arrow(ax, 10, 23.35, 10, 23.0)

# ── STEP 8: First-line treatment ─────────────────────────────────────────────
Y = 22.55
rounded_box(ax, 10, Y, 14, 0.85,
            'FIRST-LINE TREATMENT  (4-8 weeks)\n'
            'Standard-dose PPI  (omeprazole 20 mg / pantoprazole 40 mg od)\n'
            'Note: Higher PPI doses confer NO additional benefit',
            fc=C_ACTION_BG, ec=C_ACTION, tc='#1B5E20', fontsize=10, bold=True)

arrow(ax, 10, Y-0.43, 10, Y-0.43-0.5)

# ── STEP 9: Response? ────────────────────────────────────────────────────────
Y = 21.4
diamond(ax, 10, Y, 6.5, 0.85,
        'Adequate symptom\nresponse?', fontsize=10)

# Yes → maintenance
arrow(ax, 13.25, Y, 17, Y, color=C_ACTION, label='')
label_at(ax, 14.7, Y+0.22, 'YES', bold=True, color=C_ACTION, fontsize=9)
rounded_box(ax, 18.5, Y, 2.5, 1.1,
            'Continue PRN or\nstep-down therapy\nRegular review\n& reassurance',
            fc=C_ACTION_BG, ec=C_ACTION, tc='#1B5E20', fontsize=9)

# No → subtype-specific 2nd line
arrow(ax, 10, Y-0.43, 10, Y-0.43-0.5)
label_at(ax, 10.3, Y-0.62, 'NO', bold=True, color=C_REFRACTORY, fontsize=9)

# ── STEP 10: Second-line by subtype ──────────────────────────────────────────
Y = 20.2
rounded_box(ax, 10, Y, 16, 0.7,
            'SECOND-LINE TREATMENT  —  guided by predominant subtype',
            fc='#E8EAF6', ec=C_TITLE, tc=C_TITLE, fontsize=10, bold=True)

arrow(ax, 10, Y-0.35, 10, Y-0.35-0.3)

# Two columns
Y = 19.3
# PDS column (left)
rounded_box(ax, 4.8, Y, 8.0, 1.4,
            'PDS  —  Meal-related symptoms\n'
            '• Short-course PROKINETIC\n  (domperidone / itopride / mosapride)\n'
            '• Dietary review (small frequent meals)\n'
            '• Acotiamide (where available)',
            fc='#E1F5FE', ec='#0277BD', tc='#01579B', fontsize=9.5, bold=False)

# EPS column (right)
rounded_box(ax, 15.2, Y, 8.0, 1.4,
            'EPS  —  Pain-predominant symptoms\n'
            '• TRICYCLIC ANTIDEPRESSANT\n  (amitriptyline 10–25 mg nocte)\n'
            '• MIRTAZAPINE  (if weight loss present)\n'
            '• SSRIs: insufficient evidence',
            fc='#F3E5F5', ec='#6A1B9A', tc='#4A148C', fontsize=9.5, bold=False)

# Both merge
arrow(ax, 4.8, Y-0.7, 4.8, 17.7)
line(ax, 4.8, 17.7, 10, 17.7, color='#424242', lw=1.5)
arrow(ax, 15.2, Y-0.7, 15.2, 17.7)
line(ax, 15.2, 17.7, 10, 17.7, color='#424242', lw=1.5)
arrow(ax, 10, 17.7, 10, 17.4)

# ── STEP 11: Re-assess at 6-8 weeks ─────────────────────────────────────────
Y = 17.0
diamond(ax, 10, Y, 7, 0.85,
        'Re-assess at 6-8 weeks\nSymptomatic response?', fontsize=10)

# Yes → same maintenance
arrow(ax, 13.5, Y, 17.2, Y, color=C_ACTION)
label_at(ax, 15.1, Y+0.22, 'YES', bold=True, color=C_ACTION, fontsize=9)
# Connect to same maintenance box (arrow up)
ax.annotate('', xy=(17.6, 21.4-0.55), xytext=(17.6, Y),
            arrowprops=dict(arrowstyle='->', color=C_ACTION, lw=1.5,
                            connectionstyle='arc3,rad=0.0'), zorder=2)
label_at(ax, 17.9, 19.2, '→ Continue', fontsize=8.5, color=C_ACTION)

# No → refractory
arrow(ax, 10, Y-0.43, 10, Y-0.43-0.5)
label_at(ax, 10.35, Y-0.62, 'NO\n(Refractory FD)', bold=True, color=C_REFRACTORY, fontsize=9)

# ── STEP 12: Refractory FD pathway ───────────────────────────────────────────
Y = 15.7
rounded_box(ax, 10, Y, 14, 0.65,
            'REFRACTORY FUNCTIONAL DYSPEPSIA',
            fc=C_REFRAC_BG, ec=C_REFRACTORY, tc=C_REFRACTORY, fontsize=11, bold=True)

arrow(ax, 10, Y-0.33, 10, Y-0.33-0.42)

# Three refractory options
Y = 14.85
rounded_box(ax, 3.3, Y, 5.5, 1.35,
            'RE-EVALUATE DIAGNOSIS\n'
            '• EGD if age ≥45 or not done\n'
            '• Gastric emptying study\n  (rule out gastroparesis)\n'
            '• Abdominal ultrasound\n• Coeliac / SIBO screen',
            fc='#FFF8E1', ec='#FF8F00', tc='#E65100', fontsize=9)

rounded_box(ax, 10, Y, 5.5, 1.35,
            'PSYCHOLOGICAL THERAPY\n'
            '• Cognitive behavioural therapy\n  (CBT) — recommended\n'
            '• Gut-directed hypnotherapy\n'
            '• Relaxation techniques',
            fc=C_REFRAC_BG, ec=C_REFRACTORY, tc=C_REFRACTORY, fontsize=9)

rounded_box(ax, 16.7, Y, 5.5, 1.35,
            'PHARMACOLOGICAL REVIEW\n'
            '• Review & optimise neuromodulator\n  (switch TCA ↔ mirtazapine)\n'
            '• Consider low-FODMAP diet trial\n'
            '• Probiotics (emerging evidence)\n'
            '• Specialist GI referral',
            fc='#E8F5E9', ec=C_ACTION, tc='#1B5E20', fontsize=9)

# All three merge downward
for xc in [3.3, 10, 16.7]:
    arrow(ax, xc, Y-0.68, xc, 13.55)
    line(ax, xc, 13.55, 10, 13.55, color='#424242', lw=1.5)
arrow(ax, 10, 13.55, 10, 13.3)

# ── STEP 13: Multidisciplinary / ongoing ─────────────────────────────────────
Y = 12.95
rounded_box(ax, 10, Y, 14, 0.75,
            'ONGOING MANAGEMENT  —  Shared care model\n'
            'Regular follow-up  |  Reassurance & education  |  Address psychosocial factors  |  Avoid restrictive diets & opioids',
            fc='#E8EAF6', ec=C_TITLE, tc=C_TITLE, fontsize=10, bold=False)

# ── KEY NOTES BOX ─────────────────────────────────────────────────────────────
notes_box = FancyBboxPatch((0.3, 10.2), 8.5, 2.45,
                            boxstyle="round,pad=0.1,rounding_size=0.3",
                            facecolor='#FFF9C4', edgecolor='#F9A825', linewidth=1.5, zorder=3)
ax.add_patch(notes_box)
ax.text(4.55, 12.45, 'KEY PRINCIPLES (2025 Guidelines)', ha='center', va='center',
        fontsize=10, color='#E65100', weight='bold', zorder=4)
ax.text(0.65, 12.1,
        '• FD = diagnosis of exclusion (Rome IV)\n'
        '• Test & treat H. pylori in ALL patients\n'
        '• EGD mandatory: age ≥45, alarm features, or treatment-refractory\n'
        '• Higher-dose PPI: NO additional benefit\n'
        '• Exclusion/restrictive diets: discouraged\n'
        '• Opioids, antibiotics, invasive procedures: NOT recommended\n'
        '• Gut-brain interaction model: cornerstone of patient explanation\n'
        '• CBT: recommended for refractory cases',
        ha='left', va='top', fontsize=8.8, color=C_TEXT, zorder=4)

# ── ABBREVIATIONS BOX ────────────────────────────────────────────────────────
abbrev_box = FancyBboxPatch((9.2, 10.2), 10.5, 2.45,
                             boxstyle="round,pad=0.1,rounding_size=0.3",
                             facecolor='#E3F2FD', edgecolor='#1565C0', linewidth=1.5, zorder=3)
ax.add_patch(abbrev_box)
ax.text(14.45, 12.45, 'ABBREVIATIONS', ha='center', va='center',
        fontsize=10, color=C_TITLE, weight='bold', zorder=4)
ax.text(9.5, 12.1,
        'FD = Functional Dyspepsia        PDS = Postprandial Distress Syndrome\n'
        'EPS = Epigastric Pain Syndrome   PPI = Proton Pump Inhibitor\n'
        'TCA = Tricyclic Antidepressant    CBT = Cognitive Behavioural Therapy\n'
        'EGD = Esophagogastroduodenoscopy  SIBO = Small Intestinal Bacterial Overgrowth\n'
        'UBT = Urea Breath Test            FODMAP = Fermentable Oligosaccharides,\n'
        '                                           Disaccharides, Monosaccharides & Polyols',
        ha='left', va='top', fontsize=8.8, color=C_TEXT, zorder=4)

# ── SOURCES FOOTER ────────────────────────────────────────────────────────────
ax.text(10, 9.85,
        'Sources: Italian Guidelines SIGE/SINGEM/AIGO/SIED/SIMG (Sarnelli et al., Dig Liver Dis 2025, PMID 40628573)  |  '
        'Belgian Consensus (Kindt et al., Acta Gastroenterol Belg 2025, PMID 40504583)\n'
        'Second Asian Consensus (Mahadeva et al., J Gastroenterol Hepatol 2025, PMID 41656163)  |  '
        'BSG Guidelines (Black et al., Gut 2022)',
        ha='center', va='center', fontsize=8, color='#757575', style='italic', zorder=4)

# ── Save ──────────────────────────────────────────────────────────────────────
plt.tight_layout(pad=0)
plt.savefig('/home/daytona/workspace/fd-flowchart/FD_Management_Flowchart_2025.png',
            dpi=180, bbox_inches='tight', facecolor=fig.get_facecolor())
print("Saved successfully.")
Running Command

cd /home/daytona/workspace/fd-flowchart && python3 build_flowchart.py 2>&1

Running Command

ls -lh /home/daytona/workspace/fd-flowchart/

The flowchart has been generated successfully (1.2 MB PNG at 180 dpi). Here it is:
FD Management Flowchart 2025
Download:

FD Management Flowchart 2025

Image · PNG


What the flowchart covers (top to bottom):

  1. Entry - Patient with upper abdominal symptoms
  2. Duration gate - Symptoms ≥6 months? (Rome IV threshold)
  3. Alarm features screen - With a full list of red-flag features in a side box; if present → urgent EGD pathway
  4. Routine bloods + H. pylori testing - Mandatory in ALL patients
  5. H. pylori eradication - Test-and-treat branch before FD confirmation
  6. FD diagnosis + subtype identification - PDS vs EPS
  7. First-line treatment - Standard-dose PPI 4-8 weeks (higher dose not beneficial)
  8. Response assessment - Adequate response → PRN/step-down; no response → second-line
  9. Second-line by subtype - PDS: prokinetics | EPS: TCAs / mirtazapine
  10. Re-assessment at 6-8 weeks - Response vs refractory branch
  11. Refractory FD pathway - Three parallel tracks: re-evaluate diagnosis, CBT/psychological therapy, pharmacological review + specialist referral
  12. Ongoing management - Shared-care, reassurance, avoid restrictive diets and opioids
  13. Key principles, abbreviations, and full source citations at the bottom

Low fodmap diet

Finding Sources
Searching PubMed

"low FODMAP" AND diet AND "irritable bowel"

Searching the Web

low FODMAP diet foods list mechanism evidence 2024 2025

Reading File
Reading Article

PMID: 40258374

Reading Article

PMID: 38643782

Excellent data. Comprehensive answer below.

Low-FODMAP Diet

What are FODMAPs?

FODMAP is an acronym for Fermentable Oligosaccharides, Disaccharides, Monosaccharides And Poly­ols - a group of short-chain carbohydrates that share two properties:
  1. Poor absorption in the small intestine (small bowel permeability is variable and often incomplete)
  2. Rapid fermentation by colonic bacteria, producing gas (H₂, CO₂, CH₄)
The resulting osmotic water draw + gas distension triggers bloating, cramping, urgency, and altered bowel habit in susceptible individuals - particularly those with visceral hypersensitivity (IBS, functional dyspepsia).

The Five FODMAP Categories

CategoryWhat it includesKey high-FODMAP sources
Oligosaccharides - FructansFructose chainsWheat, rye, barley, onion, garlic, leek, shallots
Oligosaccharides - GOSGalacto-oligosaccharidesLegumes, lentils, chickpeas, kidney beans
Disaccharides - LactoseMilk sugarCow's milk, soft cheeses, yoghurt, ice cream
Monosaccharides - FructoseExcess fructose (> glucose)Apples, pears, mango, honey, high-fructose corn syrup
PolyolsSugar alcoholsStone fruits (cherry, peach, plum, apricot), mushrooms, cauliflower, sorbitol/mannitol/xylitol sweeteners

Mechanism of Action

High-FODMAP food ingested
        ↓
Poor absorption in small intestine (osmotic load)
        ↓
Water drawn into gut lumen → loose stool / urgency
        ↓
FODMAPs reach colon → rapid bacterial fermentation
        ↓
Gas production (H₂, CO₂, CH₄)
        ↓
Luminal distension → activates mechanoreceptors
        ↓
In visceral hypersensitivity (IBS/FD): exaggerated pain, bloating, urgency
The low-FODMAP diet reduces the osmotic load and fermentable substrate, decreasing luminal distension and gas - thereby reducing symptoms.

The Three-Phase Protocol (Monash University)

The diet is not meant to be permanent. It follows three structured phases:

Phase 1 - Strict Elimination (2-6 weeks)

  • Eliminate ALL high-FODMAP foods
  • Aim: establish a symptom-free or near-symptom-free baseline
  • Must be supervised by a dietitian - nutritional adequacy is a concern
  • ~75% of IBS patients achieve meaningful symptom reduction

Phase 2 - Systematic Reintroduction (6-8 weeks)

  • Reintroduce one FODMAP subgroup at a time, every 3 days
  • Identify personal triggers - most patients tolerate some FODMAPs (fewer than half of food challenges provoke symptoms)
  • The gut is highly individual: triggers vary person to person

Phase 3 - Personalised Long-Term Diet

  • Only the personally identified triggers remain restricted
  • Most foods tolerated during Phase 2 are reintroduced
  • Avoids unnecessary long-term nutritional restriction
Important: The diet should never remain in full elimination phase long-term. FODMAPs (prebiotics) feed beneficial gut bacteria and a permanently restricted diet reduces microbiome diversity.

Food Lists

LOW-FODMAP (Generally Safe) Foods

CategoryExamples
GrainsRice, oats (rolled), quinoa, polenta, gluten-free pasta/bread, sourdough (long fermented), corn tortillas/chips, potato
VegetablesCarrot, spinach, kale, lettuce, bok choy, cucumber, eggplant, green capsicum/bell pepper, potato, sweet potato (small portion), broccoli heads, snow peas, green beans, tomato (small portions), spring onion (green tops only)
FruitsBanana (firm/unripe), strawberry, blueberry, raspberry, kiwi, pineapple, orange, mandarin, lemon, lime, grapes, papaya, passionfruit
DairyHard cheeses (cheddar, parmesan, brie), lactose-free milk/yoghurt, butter
ProteinsAll plain meat, poultry, fish, seafood, eggs, firm tofu, tempeh
Nuts/seedsMacadamia, peanut, pecan, walnut, Brazil nut, chia seeds, pumpkin seeds
BeveragesWater, most teas, coffee (small amounts), most wine/beer/spirits
SweetenersSucrose (table sugar), glucose, maple syrup (small amounts), stevia

HIGH-FODMAP (Avoid in Phase 1) Foods

CategoryExamples
GrainsWheat bread/pasta, rye, barley
VegetablesOnion, garlic, leek, shallot, asparagus, artichoke, mushrooms (button), cauliflower, beetroot (large portions)
FruitsApple, pear, mango, watermelon, peach, apricot, cherry, plum, prune, blackberry, dates
DairyCow's milk, soft cheese, ice cream, yoghurt (regular), soy milk
LegumesMost beans, lentils, chickpeas, baked beans, falafel
SweetenersHoney, high-fructose corn syrup, sorbitol, mannitol, xylitol, isomalt (sugar-free products)

Clinical Evidence (2024-2025 Updates)

Best evidence: IBS

[Systematic Review + Network Meta-Analysis . 2025] Cuffe et al., Lancet Gastroenterol Hepatol [PMID: 40258374]
  • 28 RCTs, 2,338 patients, 11 dietary interventions compared
  • Low-FODMAP diet ranked 4th for global IBS symptoms (RR of non-improvement 0.51, 95% CI 0.37-0.70) vs habitual diet
  • Best for abdominal bloating among interventions studied in multiple trials (RR 0.55)
  • Notably, a starch/sucrose-reduced diet ranked above low-FODMAP for overall symptoms - suggesting low-FODMAP is effective but not uniquely superior
  • Low-FODMAP was superior to standard BDA/NICE dietary advice for bowel habit
[RCT . 2024] CARIBS trial - Nybacka et al., Lancet Gastroenterol Hepatol [PMID: 38643782]
  • Low-FODMAP + traditional dietary advice vs low-carbohydrate diet vs optimised pharmacological treatment (n=294)
  • 76% response rate in low-FODMAP group vs 71% low-carb vs 58% pharmacotherapy at 4 weeks
  • Dietary interventions were significantly superior to drug treatment (p=0.023)
  • At 6-month follow-up, symptom benefits were maintained in both dietary groups
[RCT . 2024] Van den Houte et al., Gastroenterology [PMID: 38401741]
  • Blinded reintroduction phase: systematic FODMAP reintroduction is safe and allows most foods to be returned to diet without symptom relapse

Evidence in Functional Dyspepsia

The 2024 current opinion (Olson & Lacy, [PMID: 39360697]) identifies the low-FODMAP diet as a promising but not yet standard therapy for FD, with large trials ongoing. The 2025 Italian and Belgian FD guidelines recommend against restrictive diets in general, while acknowledging that a supervised low-FODMAP trial may be considered in refractory FD, particularly when bloating/gas are prominent features.

Nutritional Concerns

The elimination phase restricts many nutritious foods:
  • Fibre intake drops (prebiotic loss - reduced Bifidobacterium and Lactobacillus)
  • Calcium risk if dairy eliminated without lactose-free alternatives
  • Microbiome diversity decreases with prolonged restriction
  • Complex to implement - dietitian supervision is strongly recommended
  • Not recommended without dietitian oversight, particularly in patients with nutritional deficiencies, eating disorder history, or pregnancy

Practical Considerations

PointDetail
Serving sizes matterMany foods are low-FODMAP at small portions but high-FODMAP at larger ones (e.g., sweet potato, broccoli)
Garlic/onion substitutesGarlic-infused oil (FODMAPs are not oil-soluble), chives, spring onion green tops
Eating outDifficult - the biggest adherence challenge
AppsMonash University FODMAP app - the gold standard reference for serve sizes
Duration of Phase 12 weeks minimum, 6 weeks maximum before reintroduction
Who should superviseRegistered dietitian with FODMAP training
Who should NOT do itPatients with active eating disorders, undernutrition, or those who cannot access dietitian support

Summary

The low-FODMAP diet is the most evidence-based dietary intervention for IBS (response rate ~75%), outperforming standard dietary advice and even pharmacotherapy in recent head-to-head trials. Its role in functional dyspepsia is emerging, particularly for patients with prominent bloating. The three-phase approach is key - the goal is a personalised long-term diet, not permanent blanket restriction. Dietitian supervision is essential.
Key sources:

Create a printable low-FODMAP foods guide (PDF) for patient counselling

Reading File
Running Command

python3 -c "from reportlab.platypus import SimpleDocTemplate; from reportlab.lib.pagesizes import A4; print('ReportLab OK')"

Writing File

~/fd-flowchart/build_fodmap_pdf.py

"""
Low-FODMAP Patient Counselling Guide - PDF Generator
Uses ReportLab platypus for a clean, printable A4 document.
"""

from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, cm
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import Frame, PageTemplate
from reportlab.platypus.flowables import Flowable
import os

# ── Output path ──────────────────────────────────────────────────────────────
OUT = '/home/daytona/workspace/fd-flowchart/Low_FODMAP_Patient_Guide.pdf'

# ── Colour palette ───────────────────────────────────────────────────────────
DARK_BLUE   = colors.HexColor('#1A237E')
MID_BLUE    = colors.HexColor('#1565C0')
LIGHT_BLUE  = colors.HexColor('#E3F2FD')
GREEN_DARK  = colors.HexColor('#1B5E20')
GREEN_MID   = colors.HexColor('#388E3C')
GREEN_LIGHT = colors.HexColor('#E8F5E9')
RED_DARK    = colors.HexColor('#B71C1C')
RED_LIGHT   = colors.HexColor('#FFEBEE')
ORANGE_DARK = colors.HexColor('#E65100')
ORANGE_LIGHT= colors.HexColor('#FFF3E0')
AMBER_DARK  = colors.HexColor('#F57F17')
AMBER_LIGHT = colors.HexColor('#FFF9C4')
GREY_BG     = colors.HexColor('#F5F5F5')
GREY_DARK   = colors.HexColor('#424242')
WHITE       = colors.white
BLACK       = colors.black

# ── Page dimensions ───────────────────────────────────────────────────────────
W, H = A4  # 595.27 x 841.89 pts
MARGIN = 18*mm

# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()

def S(name, **kw):
    """Quick ParagraphStyle factory."""
    return ParagraphStyle(name, **kw)

title_style = S('Title2', fontSize=22, leading=28, textColor=WHITE,
                fontName='Helvetica-Bold', alignment=TA_CENTER)
subtitle_style = S('Subtitle2', fontSize=10, leading=13, textColor=colors.HexColor('#B0BEC5'),
                   fontName='Helvetica', alignment=TA_CENTER)

h1 = S('H1', fontSize=13, leading=17, textColor=DARK_BLUE,
        fontName='Helvetica-Bold', spaceBefore=8, spaceAfter=4)
h2 = S('H2', fontSize=11, leading=14, textColor=MID_BLUE,
        fontName='Helvetica-Bold', spaceBefore=6, spaceAfter=3)
h3 = S('H3', fontSize=10, leading=13, textColor=GREY_DARK,
        fontName='Helvetica-Bold', spaceBefore=4, spaceAfter=2)

body = S('Body2', fontSize=9, leading=13, textColor=GREY_DARK,
          fontName='Helvetica', spaceAfter=2)
body_sm = S('BodySm', fontSize=8.5, leading=12, textColor=GREY_DARK,
             fontName='Helvetica')
body_bold = S('BodyBold', fontSize=9, leading=13, textColor=GREY_DARK,
               fontName='Helvetica-Bold')
bullet = S('Bullet2', fontSize=9, leading=13, textColor=GREY_DARK,
            fontName='Helvetica', leftIndent=12, bulletIndent=2,
            spaceAfter=1)
note_style = S('Note', fontSize=8, leading=11, textColor=ORANGE_DARK,
                fontName='Helvetica-Oblique')
footer_style = S('Footer', fontSize=7.5, leading=10, textColor=colors.HexColor('#9E9E9E'),
                  fontName='Helvetica', alignment=TA_CENTER)
white_bold = S('WBold', fontSize=10, leading=13, textColor=WHITE,
                fontName='Helvetica-Bold', alignment=TA_CENTER)
white_body = S('WBody', fontSize=8.5, leading=12, textColor=WHITE,
                fontName='Helvetica', alignment=TA_CENTER)
green_bold = S('GBold', fontSize=9, leading=12, textColor=GREEN_DARK,
                fontName='Helvetica-Bold')
red_bold = S('RBold', fontSize=9, leading=12, textColor=RED_DARK,
              fontName='Helvetica-Bold')
cell_sm = S('CellSm', fontSize=8.5, leading=11, textColor=GREY_DARK,
             fontName='Helvetica')
cell_hdr = S('CellHdr', fontSize=9, leading=12, textColor=WHITE,
              fontName='Helvetica-Bold', alignment=TA_CENTER)
cell_hdr_dk = S('CellHdrDk', fontSize=9, leading=12, textColor=WHITE,
                 fontName='Helvetica-Bold', alignment=TA_CENTER)

# ── Helper: coloured banner paragraph ────────────────────────────────────────
class ColorBanner(Flowable):
    """A full-width coloured rectangle with centred text."""
    def __init__(self, text, sub='', bg=DARK_BLUE, h=26*mm):
        super().__init__()
        self.text = text
        self.sub  = sub
        self.bg   = bg
        self.bh   = h
        self.width = W - 2*MARGIN

    def wrap(self, aw, ah):
        self.width = aw
        return aw, self.bh

    def draw(self):
        c = self.canv
        c.setFillColor(self.bg)
        c.roundRect(0, 0, self.width, self.bh, 6, fill=1, stroke=0)
        c.setFillColor(WHITE)
        c.setFont('Helvetica-Bold', 18)
        y = self.bh - 16 if self.sub else self.bh/2 + 4
        c.drawCentredString(self.width/2, y, self.text)
        if self.sub:
            c.setFont('Helvetica', 8.5)
            c.setFillColor(colors.HexColor('#B0BEC5'))
            c.drawCentredString(self.width/2, self.bh - 28, self.sub)

class SectionBanner(Flowable):
    """Narrower coloured section header."""
    def __init__(self, text, bg=MID_BLUE, h=8*mm):
        super().__init__()
        self.text = text
        self.bg = bg
        self.bh = h
        self.width = 0

    def wrap(self, aw, ah):
        self.width = aw
        return aw, self.bh

    def draw(self):
        c = self.canv
        c.setFillColor(self.bg)
        c.roundRect(0, 0, self.width, self.bh, 4, fill=1, stroke=0)
        c.setFillColor(WHITE)
        c.setFont('Helvetica-Bold', 10)
        c.drawString(8, self.bh/2 - 4, self.text)

class PhaseBox(Flowable):
    """Numbered phase box."""
    def __init__(self, num, title, body_text, color):
        super().__init__()
        self.num = num
        self.title = title
        self.body_text = body_text
        self.color = color
        self.width = 0

    def wrap(self, aw, ah):
        self.width = aw
        return aw, 28*mm

    def draw(self):
        c = self.canv
        bw = self.width
        bh = 28*mm
        # background
        c.setFillColor(colors.HexColor('#F5F5F5'))
        c.roundRect(0, 0, bw, bh, 5, fill=1, stroke=0)
        # left colour strip
        c.setFillColor(self.color)
        c.roundRect(0, 0, 14*mm, bh, 5, fill=1, stroke=0)
        c.rect(7*mm, 0, 7*mm, bh, fill=1, stroke=0)
        # number
        c.setFillColor(WHITE)
        c.setFont('Helvetica-Bold', 20)
        c.drawCentredString(7*mm, bh/2 - 7, self.num)
        # title
        c.setFillColor(self.color)
        c.setFont('Helvetica-Bold', 11)
        c.drawString(16*mm, bh - 12, self.title)
        # body
        c.setFillColor(GREY_DARK)
        c.setFont('Helvetica', 8.5)
        lines = self.body_text.split('\n')
        y = bh - 22
        for line in lines:
            c.drawString(16*mm, y, line)
            y -= 11

# ── Document setup ────────────────────────────────────────────────────────────
os.makedirs(os.path.dirname(OUT), exist_ok=True)

def add_footer(canvas, doc):
    canvas.saveState()
    canvas.setFont('Helvetica', 7)
    canvas.setFillColor(colors.HexColor('#9E9E9E'))
    canvas.drawCentredString(W/2, 10*mm,
        'Low-FODMAP Patient Counselling Guide  |  Based on Monash University FODMAP Programme & '
        'Cuffe et al., Lancet Gastroenterol Hepatol 2025  |  For patient education only - not a substitute for dietitian advice')
    canvas.drawString(MARGIN, 10*mm, f'Page {doc.page}')
    canvas.restoreState()

doc = SimpleDocTemplate(
    OUT,
    pagesize=A4,
    leftMargin=MARGIN, rightMargin=MARGIN,
    topMargin=MARGIN, bottomMargin=20*mm,
    title='Low-FODMAP Diet Patient Guide',
    author='Clinical Nutrition - GI Outpatient',
)

story = []
TW = W - 2*MARGIN  # text width

# ═══════════════════════════════════════════════════════════════════════
# PAGE 1
# ═══════════════════════════════════════════════════════════════════════

# Header banner
story.append(ColorBanner(
    'Low-FODMAP Diet Guide',
    sub='Patient Counselling Resource  |  Prepared by GI / Nutrition Team  |  Based on Monash University FODMAP Programme',
    bg=DARK_BLUE, h=30*mm))
story.append(Spacer(1, 5*mm))

# What is FODMAP box
what_data = [[
    Paragraph('What are FODMAPs?', h2),
    Paragraph(
        '<b>FODMAP</b> stands for <b>F</b>ermentable <b>O</b>ligosaccharides, <b>D</b>isaccharides, '
        '<b>M</b>onosaccharides <b>A</b>nd <b>P</b>olyols - a group of short-chain carbohydrates that are '
        'poorly absorbed in the small intestine and rapidly fermented by gut bacteria, causing gas, bloating, '
        'pain and altered bowel habit in sensitive individuals.',
        body)
]]
wt = Table(what_data, colWidths=[TW])
wt.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,-1), LIGHT_BLUE),
    ('BOX', (0,0), (-1,-1), 0.5, MID_BLUE),
    ('ROUNDEDCORNERS', [5]),
    ('TOPPADDING', (0,0), (-1,-1), 6),
    ('BOTTOMPADDING', (0,0), (-1,-1), 6),
    ('LEFTPADDING', (0,0), (-1,-1), 8),
    ('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(wt)
story.append(Spacer(1, 4*mm))

# FODMAP categories table
story.append(SectionBanner('The 5 FODMAP Categories', bg=MID_BLUE))
story.append(Spacer(1, 2*mm))

cat_headers = [
    Paragraph('Category', cell_hdr),
    Paragraph('Full Name', cell_hdr),
    Paragraph('Key High-FODMAP Sources', cell_hdr),
]
cat_rows = [
    ['Fructans\n(Oligosaccharides)',
     'Fructose polymer chains',
     'Wheat, rye, barley, onion, garlic, leek, shallots, asparagus'],
    ['GOS\n(Oligosaccharides)',
     'Galacto-oligosaccharides',
     'Lentils, chickpeas, kidney beans, most legumes'],
    ['Lactose\n(Disaccharide)',
     'Milk sugar',
     "Cow's milk, soft cheeses, yoghurt, ice cream, cream"],
    ['Fructose\n(Monosaccharide)',
     'Excess fructose > glucose',
     'Apples, pears, mango, honey, high-fructose corn syrup, watermelon'],
    ['Polyols',
     'Sugar alcohols',
     'Stone fruits (peach, plum, cherry, apricot), mushrooms,\ncauliflower, sorbitol, mannitol, xylitol (sugar-free products)'],
]
cat_table_data = [cat_headers] + [
    [Paragraph(r[0], cell_sm), Paragraph(r[1], cell_sm), Paragraph(r[2], cell_sm)]
    for r in cat_rows
]
ct = Table(cat_table_data, colWidths=[38*mm, 52*mm, TW - 90*mm])
ct.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,0), MID_BLUE),
    ('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, GREY_BG]),
    ('BOX', (0,0), (-1,-1), 0.5, MID_BLUE),
    ('INNERGRID', (0,0), (-1,-1), 0.3, colors.HexColor('#BDBDBD')),
    ('TOPPADDING', (0,0), (-1,-1), 4),
    ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ('LEFTPADDING', (0,0), (-1,-1), 5),
    ('RIGHTPADDING', (0,0), (-1,-1), 5),
    ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
]))
story.append(ct)
story.append(Spacer(1, 4*mm))

# Three phase protocol
story.append(SectionBanner('The 3-Phase Protocol  (Monash University)', bg=GREEN_DARK))
story.append(Spacer(1, 2*mm))

phases = [
    ('1', 'PHASE 1 - Strict Elimination  (2-6 weeks)',
     'Avoid ALL high-FODMAP foods listed in this guide.\nEstablish a symptom-free baseline.\nMust be supervised by a trained dietitian.',
     GREEN_DARK),
    ('2', 'PHASE 2 - Systematic Reintroduction  (6-8 weeks)',
     'Reintroduce ONE FODMAP group at a time, every 3 days.\nIdentify your personal triggers - most challenges do not cause symptoms.\nKeep a food-symptom diary throughout.',
     MID_BLUE),
    ('3', 'PHASE 3 - Personalised Long-Term Diet',
     'Restrict ONLY foods confirmed as personal triggers.\nReintroduce tolerated foods - avoid permanent blanket restriction.\nFODMAPs are prebiotic: long-term full restriction harms gut microbiome.',
     ORANGE_DARK),
]
for num, title, body_txt, col in phases:
    story.append(PhaseBox(num, title, body_txt, col))
    story.append(Spacer(1, 2*mm))

story.append(Spacer(1, 1*mm))

# Important note
note_data = [[
    Paragraph(
        '<b>Important:</b> Serve sizes matter - many foods are low-FODMAP in small portions but '
        'high-FODMAP in larger amounts. Use the <b>Monash University FODMAP App</b> for accurate '
        'portion guidance. This guide lists typical standard serve examples.',
        note_style)
]]
nt = Table(note_data, colWidths=[TW])
nt.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,-1), AMBER_LIGHT),
    ('BOX', (0,0), (-1,-1), 0.8, AMBER_DARK),
    ('TOPPADDING', (0,0), (-1,-1), 5),
    ('BOTTOMPADDING', (0,0), (-1,-1), 5),
    ('LEFTPADDING', (0,0), (-1,-1), 8),
    ('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(nt)

# ═══════════════════════════════════════════════════════════════════════
# PAGE 2 - Food Lists
# ═══════════════════════════════════════════════════════════════════════
story.append(PageBreak())

story.append(ColorBanner('Food Lists at a Glance', bg=MID_BLUE, h=16*mm))
story.append(Spacer(1, 4*mm))

# ── SIDE-BY-SIDE tables: LOW FODMAP (green) | HIGH FODMAP (red) ──────────────
col_w = (TW - 4*mm) / 2

def food_section(title, items, hdr_color, light_color, hdr_style):
    """Build a single-column mini table with header + items."""
    rows = [[Paragraph(title, hdr_style)]]
    for cat, foods in items:
        rows.append([Paragraph(f'<b>{cat}</b>', cell_sm)])
        rows.append([Paragraph(foods, cell_sm)])
    t = Table(rows, colWidths=[col_w])
    style = [
        ('BACKGROUND', (0,0), (-1,0), hdr_color),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, light_color]),
        ('BOX', (0,0), (-1,-1), 0.6, hdr_color),
        ('INNERGRID', (0,0), (-1,-1), 0.2, colors.HexColor('#E0E0E0')),
        ('TOPPADDING', (0,0), (-1,-1), 3),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('RIGHTPADDING', (0,0), (-1,-1), 5),
        ('VALIGN', (0,0), (-1,-1), 'TOP'),
    ]
    t.setStyle(TableStyle(style))
    return t

low_items = [
    ('Grains & Bread',
     'Rice, oats (rolled), quinoa, polenta, gluten-free pasta/bread, '
     'sourdough (long-fermented), corn tortillas/chips, potato, popcorn'),
    ('Vegetables',
     'Carrot, spinach, kale, lettuce, bok choy, cucumber, eggplant, '
     'green capsicum, potato, broccoli heads, snow peas, green beans, '
     'tomato (small amounts), spring onion (green tops only), radish, '
     'parsnip, sweet potato (1/3 cup), zucchini'),
    ('Fruits',
     'Banana (firm/unripe), strawberry, blueberry, raspberry, kiwi, '
     'pineapple, orange, mandarin, lemon, lime, grapes, papaya, '
     'passionfruit, star fruit, cantaloupe (3/4 cup)'),
    ('Dairy & Alternatives',
     'Hard cheeses (cheddar, parmesan, brie, camembert), lactose-free '
     'milk, lactose-free yoghurt, butter, brie, almond milk, '
     'soy milk (protein-based), coconut yoghurt'),
    ('Protein',
     'All plain meat, poultry, fish, seafood, eggs, firm tofu, tempeh'),
    ('Nuts & Seeds',
     'Macadamia, peanut, pecan, walnut, Brazil nut, chia seeds, '
     'pumpkin seeds, sesame seeds (small amounts)'),
    ('Beverages',
     'Water, coffee (small), most teas (not chamomile/fennel/oolong), '
     'most wine, beer, spirits'),
    ('Sweeteners',
     'Sucrose (table sugar), glucose, maple syrup (1 tbsp), stevia'),
    ('Condiments / Flavourings',
     'Garlic-infused oil, chives, fresh herbs, soy sauce (small amounts), '
     'most vinegars, mustard, mayonnaise, olive oil'),
]

high_items = [
    ('Grains & Bread',
     'Wheat bread, pasta & cereals, rye, barley, wheat-based crackers, '
     'couscous, gnocchi (wheat)'),
    ('Vegetables',
     'Onion, garlic, leek, shallot, spring onion (white part), '
     'asparagus, artichoke, button/portobello mushrooms, cauliflower, '
     'celery, beetroot (large), sugar snap peas (large amounts)'),
    ('Fruits',
     'Apple, pear, mango, watermelon, peach, apricot, cherry, plum, '
     'prune, blackberry, dates, dried fruit generally'),
    ('Dairy & Alternatives',
     "Cow's milk, goat's milk, soy milk (whole bean), soft cheeses, "
     'ice cream, regular yoghurt, custard, cream cheese'),
    ('Legumes / Pulses',
     'Most beans, lentils (large portions), chickpeas (canned ok '
     'in small portions, rinsed), baked beans, falafel'),
    ('Nuts',
     'Cashews, pistachios (high fructan content)'),
    ('Sweeteners',
     'Honey, high-fructose corn syrup, agave syrup, sorbitol, '
     'mannitol, xylitol, isomalt (most "sugar-free" products)'),
    ('Beverages',
     'Apple/pear juice, chamomile tea, fennel tea, oolong tea, '
     'chai tea, rum, high-fructose sodas'),
    ('Condiments',
     'Garlic/onion powder, garlic salt, most gravies, relishes, '
     'chutneys (check labels for onion/honey/HFCS)'),
]

low_hdr_style = S('LowHdr', fontSize=11, leading=14, textColor=WHITE,
                   fontName='Helvetica-Bold', alignment=TA_CENTER)
high_hdr_style = S('HighHdr', fontSize=11, leading=14, textColor=WHITE,
                    fontName='Helvetica-Bold', alignment=TA_CENTER)

# We build two parallel single-col tables and merge into one 2-col table
low_rows = [
    [Paragraph('✔  LOW-FODMAP  (Phase 1 Safe)', S('LH', fontSize=11, leading=14,
                textColor=WHITE, fontName='Helvetica-Bold', alignment=TA_CENTER))]
]
for cat, foods in low_items:
    low_rows.append([Paragraph(f'<b>{cat}</b>', cell_sm)])
    low_rows.append([Paragraph(foods, cell_sm)])

high_rows = [
    [Paragraph('✘  HIGH-FODMAP  (Avoid in Phase 1)', S('HH', fontSize=11, leading=14,
                textColor=WHITE, fontName='Helvetica-Bold', alignment=TA_CENTER))]
]
for cat, foods in high_items:
    high_rows.append([Paragraph(f'<b>{cat}</b>', cell_sm)])
    high_rows.append([Paragraph(foods, cell_sm)])

# Equalise row count
while len(low_rows) < len(high_rows):
    low_rows.append([Paragraph('', cell_sm)])
while len(high_rows) < len(low_rows):
    high_rows.append([Paragraph('', cell_sm)])

combined = [[lr[0], hr[0]] for lr, hr in zip(low_rows, high_rows)]
ct2 = Table(combined, colWidths=[col_w, col_w], spaceBefore=0, spaceAfter=0)
ts2 = TableStyle([
    # Left (green) header
    ('BACKGROUND', (0,0), (0,0), GREEN_DARK),
    # Right (red) header
    ('BACKGROUND', (1,0), (1,0), RED_DARK),
    # Alternating rows per column
    ('ROWBACKGROUNDS', (0,1), (0,-1), [WHITE, GREEN_LIGHT]),
    ('ROWBACKGROUNDS', (1,1), (1,-1), [WHITE, RED_LIGHT]),
    ('BOX', (0,0), (0,-1), 0.6, GREEN_DARK),
    ('BOX', (1,0), (1,-1), 0.6, RED_DARK),
    ('INNERGRID', (0,0), (-1,-1), 0.2, colors.HexColor('#E0E0E0')),
    ('TOPPADDING', (0,0), (-1,-1), 3),
    ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ('LEFTPADDING', (0,0), (-1,-1), 5),
    ('RIGHTPADDING', (0,0), (-1,-1), 5),
    ('VALIGN', (0,0), (-1,-1), 'TOP'),
    ('LINEAFTER', (0,0), (0,-1), 2, colors.HexColor('#E0E0E0')),
])
ct2.setStyle(ts2)
story.append(ct2)

# ═══════════════════════════════════════════════════════════════════════
# PAGE 3 - Tips, Swaps, Evidence, Contacts
# ═══════════════════════════════════════════════════════════════════════
story.append(PageBreak())

story.append(ColorBanner('Practical Tips & Clever Swaps', bg=GREEN_DARK, h=16*mm))
story.append(Spacer(1, 4*mm))

# Tips in 2-column layout
tips = [
    ('Garlic & Onion Flavour',
     'Use <b>garlic-infused oil</b> (FODMAPs are not fat-soluble, so the oil is safe). '
     'Replace onion with the <b>green tops of spring onions</b> or chives.'),
    ('Wheat Bread',
     'Choose <b>long-fermented sourdough</b> (most FODMAPs are broken down during fermentation) '
     'or certified gluten-free bread.'),
    ('Milk & Dairy',
     'Swap to <b>lactose-free milk/yoghurt</b> - same nutrition, no lactose. '
     'Most <b>hard cheeses</b> (cheddar, parmesan) are naturally very low in lactose.'),
    ('Legumes',
     '<b>Canned chickpeas and lentils</b>, well rinsed, are lower in FODMAPs than dried/cooked. '
     'Limit to ~1/4 cup per meal.'),
    ('Sweeteners',
     'Avoid honey, agave and "sugar-free" products (contain polyols). '
     '<b>Maple syrup</b> (1 tbsp) and <b>table sugar</b> are low-FODMAP.'),
    ('Eating Out',
     'Ask for dressings/sauces on the side. Request dishes without onion/garlic. '
     'Plain grilled proteins with rice or potato are usually safe.'),
    ('Meal Prep',
     'Batch-cook safe grains (rice, quinoa, oats). Keep safe snacks ready: '
     'rice cakes + peanut butter, hard cheese + grapes, firm banana.'),
    ('Label Reading',
     'Look for: onion powder, garlic powder, high-fructose corn syrup, '
     'chicory root/inulin, apple juice - all high-FODMAP additives.'),
]
tip_rows = []
for i in range(0, len(tips), 2):
    left_title, left_body = tips[i]
    if i+1 < len(tips):
        right_title, right_body = tips[i+1]
        right_cell = [Paragraph(f'<b>{right_title}</b>', body_bold),
                      Paragraph(right_body, body_sm)]
    else:
        right_cell = [Paragraph('', body_sm)]
    tip_rows.append([
        [Paragraph(f'<b>{left_title}</b>', body_bold), Paragraph(left_body, body_sm)],
        right_cell if isinstance(right_cell, list) else [right_cell]
    ])

tip_table_data = []
for row in tip_rows:
    tip_table_data.append([row[0], row[1]])

def nested_cell(items):
    return items  # list of Paragraphs becomes a nested cell

tip_table = Table(
    [[nested_cell(row[0]), nested_cell(row[1])] for row in tip_rows],
    colWidths=[(TW-4*mm)/2, (TW-4*mm)/2]
)
tip_table.setStyle(TableStyle([
    ('ROWBACKGROUNDS', (0,0), (-1,-1), [WHITE, GREY_BG]),
    ('BOX', (0,0), (-1,-1), 0.4, colors.HexColor('#BDBDBD')),
    ('INNERGRID', (0,0), (-1,-1), 0.3, colors.HexColor('#E0E0E0')),
    ('TOPPADDING', (0,0), (-1,-1), 5),
    ('BOTTOMPADDING', (0,0), (-1,-1), 5),
    ('LEFTPADDING', (0,0), (-1,-1), 6),
    ('RIGHTPADDING', (0,0), (-1,-1), 6),
    ('VALIGN', (0,0), (-1,-1), 'TOP'),
]))
story.append(tip_table)
story.append(Spacer(1, 4*mm))

# Sample Day meal plan
story.append(SectionBanner('Sample Low-FODMAP Day  (Illustrative Only)', bg=DARK_BLUE))
story.append(Spacer(1, 2*mm))

meal_data = [
    [Paragraph('Meal', cell_hdr), Paragraph('Example', cell_hdr), Paragraph('Notes', cell_hdr)],
    [Paragraph('Breakfast', cell_sm),
     Paragraph('Rolled oats (1/2 cup) + lactose-free milk + firm banana + blueberries', cell_sm),
     Paragraph('No honey - use maple syrup (1 tsp)', cell_sm)],
    [Paragraph('Mid-morning snack', cell_sm),
     Paragraph('Rice cakes (2) + peanut butter (2 tbsp) + grapes', cell_sm),
     Paragraph('Check PB for no added HFCS', cell_sm)],
    [Paragraph('Lunch', cell_sm),
     Paragraph('Grilled chicken + rice + spinach + carrot + cucumber salad + olive oil/lemon dressing', cell_sm),
     Paragraph('No onion/garlic in marinade - use garlic-infused oil', cell_sm)],
    [Paragraph('Afternoon snack', cell_sm),
     Paragraph('Hard cheddar (30g) + rice crackers + orange', cell_sm),
     Paragraph('Hard cheeses are naturally low-lactose', cell_sm)],
    [Paragraph('Dinner', cell_sm),
     Paragraph('Salmon fillet + quinoa + green beans + tomato + bok choy stir-fry', cell_sm),
     Paragraph('Season with soy sauce (1 tbsp), ginger, spring onion tops', cell_sm)],
    [Paragraph('Dessert (optional)', cell_sm),
     Paragraph('Lactose-free yoghurt + strawberries + kiwi', cell_sm),
     Paragraph('Avoid flavoured yoghurts (often contain HFCS/honey)', cell_sm)],
]
mt = Table(meal_data, colWidths=[36*mm, TW - 36*mm - 55*mm, 55*mm])
mt.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,0), DARK_BLUE),
    ('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, LIGHT_BLUE]),
    ('BOX', (0,0), (-1,-1), 0.5, DARK_BLUE),
    ('INNERGRID', (0,0), (-1,-1), 0.3, colors.HexColor('#BDBDBD')),
    ('TOPPADDING', (0,0), (-1,-1), 4),
    ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ('LEFTPADDING', (0,0), (-1,-1), 5),
    ('RIGHTPADDING', (0,0), (-1,-1), 5),
    ('VALIGN', (0,0), (-1,-1), 'TOP'),
]))
story.append(mt)
story.append(Spacer(1, 4*mm))

# Evidence box
story.append(SectionBanner('Evidence Summary  (2024-2025)', bg=MID_BLUE))
story.append(Spacer(1, 2*mm))

ev_data = [[
    Paragraph(
        '<b>CARIBS Trial (Lancet Gastroenterol Hepatol, 2024):</b> Low-FODMAP diet achieved <b>76% response rate</b> '
        'in moderate-severe IBS at 4 weeks, significantly outperforming optimised drug treatment (58%, p=0.023).<br/><br/>'
        '<b>Network Meta-Analysis (Lancet Gastroenterol Hepatol, 2025):</b> Among 11 dietary interventions in 28 RCTs (2,338 patients), '
        'low-FODMAP ranked <b>4th for overall IBS symptoms</b> and <b>best for abdominal bloating</b> (RR 0.55, 95% CI 0.37-0.80).<br/><br/>'
        '<b>In Functional Dyspepsia:</b> Emerging evidence supports a supervised low-FODMAP trial in patients with prominent '
        'bloating/gas. Italian (2025) and Belgian (2025) consensus guidelines acknowledge it as an option for refractory cases.',
        body_sm)
]]
et = Table(ev_data, colWidths=[TW])
et.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,-1), LIGHT_BLUE),
    ('BOX', (0,0), (-1,-1), 0.5, MID_BLUE),
    ('TOPPADDING', (0,0), (-1,-1), 6),
    ('BOTTOMPADDING', (0,0), (-1,-1), 6),
    ('LEFTPADDING', (0,0), (-1,-1), 8),
    ('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(et)
story.append(Spacer(1, 4*mm))

# Cautions
story.append(SectionBanner('When to Use with Caution / Contraindications', bg=RED_DARK))
story.append(Spacer(1, 2*mm))
cautions = [
    'History of eating disorder or disordered eating behaviour',
    'Undernutrition / significant unintentional weight loss (investigate cause first)',
    'Children and adolescents - specialist dietitian involvement mandatory',
    'Pregnancy or breastfeeding - ensure adequate nutritional intake',
    'Inflammatory Bowel Disease (Crohn\'s / UC) in active flare - different priorities',
    'Patients without access to a trained FODMAP dietitian - unsupervised restriction risks nutritional deficiency',
]
caut_data = [[Paragraph('  \u2022  ' + c, body_sm)] for c in cautions]
cautT = Table(caut_data, colWidths=[TW])
cautT.setStyle(TableStyle([
    ('ROWBACKGROUNDS', (0,0), (-1,-1), [WHITE, RED_LIGHT]),
    ('BOX', (0,0), (-1,-1), 0.5, RED_DARK),
    ('TOPPADDING', (0,0), (-1,-1), 3),
    ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ('LEFTPADDING', (0,0), (-1,-1), 8),
    ('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(cautT)
story.append(Spacer(1, 4*mm))

# Resources footer box
res_data = [[
    Paragraph(
        '<b>Key Resources:</b>  '
        'Monash University FODMAP App (monashfodmap.com)  |  '
        'ACG Low-FODMAP guide: gi.org/topics/low-fodmap-diet  |  '
        'Cuffe et al. (2025) PMID: 40258374  |  CARIBS trial (2024) PMID: 38643782',
        S('Res', fontSize=8, leading=11, textColor=DARK_BLUE,
          fontName='Helvetica', alignment=TA_CENTER))
]]
resT = Table(res_data, colWidths=[TW])
resT.setStyle(TableStyle([
    ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#E8EAF6')),
    ('BOX', (0,0), (-1,-1), 0.8, DARK_BLUE),
    ('TOPPADDING', (0,0), (-1,-1), 6),
    ('BOTTOMPADDING', (0,0), (-1,-1), 6),
    ('LEFTPADDING', (0,0), (-1,-1), 8),
    ('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(resT)

# ── Build PDF ─────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=add_footer, onLaterPages=add_footer)
print(f"PDF saved: {OUT}")
Running Command

cd /home/daytona/workspace/fd-flowchart && python3 build_fodmap_pdf.py 2>&1

Running Command

ls -lh /home/daytona/workspace/fd-flowchart/Low_FODMAP_Patient_Guide.pdf && python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/fd-flowchart/Low_FODMAP_Patient_Guide.pdf') print(f'Pages: {len(r.pages)}') for i, p in enumerate(r.pages): print(f' Page {i+1}: {len(p.extract_text())} chars') "

Your printable patient guide is ready:

Low FODMAP Patient Guide

PDF Document · PDF


What's inside (4 pages, A4, print-ready):

Page 1 - Introduction & Protocol
  • Dark navy header banner with document title
  • "What are FODMAPs?" explanation box (light blue)
  • Full 5-category FODMAP table (Fructans, GOS, Lactose, Fructose, Polyols) with key food sources
  • Three-phase protocol in colour-coded numbered boxes (green = Phase 1, blue = Phase 2, orange = Phase 3)
  • Amber caution box on serving sizes + Monash App reference
Page 2 - Food Lists at a Glance
  • Side-by-side two-column layout:
    • Left (green): LOW-FODMAP safe foods across 9 categories
    • Right (red): HIGH-FODMAP foods to avoid in Phase 1
  • Covers grains, vegetables, fruits, dairy, proteins, nuts/seeds, beverages, sweeteners, condiments
Page 3 - Tips, Meal Plan & Evidence
  • 8 practical swap tips in a 2-column grid (garlic-infused oil, sourdough, lactose-free swaps, label reading, eating out etc.)
  • Sample low-FODMAP day - 6 meals with practical notes
  • Evidence summary box (CARIBS trial 2024 + Lancet network meta-analysis 2025)
  • Red cautions/contraindications section
Page 4 - Overflow content + footer on every page with source citations (Cuffe et al. 2025 PMID 40258374, CARIBS 2024 PMID 38643782) and the Monash FODMAP App reference
This is a shared conversation. Sign in to Orris to start your own chat.