Causes of 5 yr chronic dyspepsia with normal lft
chronic dyspepsia etiology functional organic
| Cause | Key Feature |
|---|---|
| Eosinophilic gastroenteritis | Eosinophilia, may need biopsy |
| Menétrier's disease | Protein-losing gastropathy, giant rugae |
| Mesenteric ischemia | Postprandial pain, "food fear," weight loss |
| Sarcoidosis / mastocytosis | Systemic features |
| Congestive heart failure | Hepatic congestion, may mimic dyspepsia |
| Abdominal wall pain | Carnett's sign positive |
Search for recent clinical guidelines on functional dyspepsia diagnosis and management (2024-2025)
"functional dyspepsia"[MeSH Terms] AND "guideline"[pt]
"functional dyspepsia" AND management AND diagnosis
functional dyspepsia clinical guidelines 2024 2025 ACG AGA Rome IV
PMID: 40628573
PMID: 41656163
PMID: 40504583
PMID: 39360697
| Document | Year | PMID |
|---|---|---|
| Italian Guidelines (SIGE/SINGEM/AIGO/SIED/SIMG) - Practice Guideline (Delphi, 24 statements, GRADE) | 2025 | 40628573 |
| Belgian Consensus on FD Management - Delphi, 119 statements, GRADE | 2025 | 40504583 |
| Second Asian Consensus on FD (ANMA/APAGE) - 32 statements, Delphi | 2025/2026 | 41656163 |
| Current Opinion: FD - Olson, Lacy et al. - Review | 2024 | 39360697 |
| BSG Guidelines on FD (Black et al., Gut) | 2022 | Still current reference standard |
| ACG/CAG Joint Guideline (Moayyedi et al.) | 2017 | Still widely cited |
| Drug Class | Recommendation | Notes |
|---|---|---|
| PPI (standard dose, short course) | First-line for ALL FD subtypes (Italian, Belgian) | Higher doses confer no added benefit |
| H. pylori eradication | All HP-positive patients | All guidelines agree |
| Drug Class | Subtype | Recommendation |
|---|---|---|
| Prokinetics (domperidone, metoclopramide, itopride) | PDS | Short course; NOT first-line; use preferentially in PDS subtype (Italian guideline) |
| Tricyclic antidepressants (amitriptyline, nortriptyline) | EPS | Recommended for EPS (Italian guideline) - gut neuromodulator effect |
| Mirtazapine | FD with weight loss | Recommended, particularly when weight loss is a feature (Italian guideline) |
| SSRIs | General FD | Insufficient evidence; not recommended routinely |
| H2 receptor antagonists / antacids | General | No consensus on routine use (Italian); not first-line |
| Domain | Recommendation |
|---|---|
| Diagnosis | Rome IV criteria; FD = diagnosis of exclusion |
| H. pylori | Test all; treat if positive |
| EGD | Age ≥45, alarm features, or treatment refractory |
| Routine imaging/motility | NOT recommended routinely |
| First-line drug | Short-course standard-dose PPI |
| PDS subtype | Prokinetics (short course, not first-line) |
| EPS subtype | Tricyclic antidepressants |
| Weight loss + FD | Mirtazapine |
| Refractory FD | CBT; reassess diagnosis |
| Diet | General healthy advice; exclusion diets discouraged |
| Invasive/surgical treatment | Recommended against |
Create a patient management flowchart for functional dyspepsia based on 2025 guidelines
ls -la /home/daytona/workspace/ 2>/dev/null && python3 -c "import matplotlib; print(matplotlib.__version__)"
mkdir -p /home/daytona/workspace/fd-flowchart && python3 -c "import matplotlib.patches as mpatches; import matplotlib; print('OK')"
~/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.")
cd /home/daytona/workspace/fd-flowchart && python3 build_flowchart.py 2>&1
ls -lh /home/daytona/workspace/fd-flowchart/

FD Management Flowchart 2025
Image · PNG
Low fodmap diet
"low FODMAP" AND diet AND "irritable bowel"
low FODMAP diet foods list mechanism evidence 2024 2025
PMID: 40258374
PMID: 38643782
| Category | What it includes | Key high-FODMAP sources |
|---|---|---|
| Oligosaccharides - Fructans | Fructose chains | Wheat, rye, barley, onion, garlic, leek, shallots |
| Oligosaccharides - GOS | Galacto-oligosaccharides | Legumes, lentils, chickpeas, kidney beans |
| Disaccharides - Lactose | Milk sugar | Cow's milk, soft cheeses, yoghurt, ice cream |
| Monosaccharides - Fructose | Excess fructose (> glucose) | Apples, pears, mango, honey, high-fructose corn syrup |
| Polyols | Sugar alcohols | Stone fruits (cherry, peach, plum, apricot), mushrooms, cauliflower, sorbitol/mannitol/xylitol sweeteners |
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
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.
| Category | Examples |
|---|---|
| Grains | Rice, oats (rolled), quinoa, polenta, gluten-free pasta/bread, sourdough (long fermented), corn tortillas/chips, potato |
| Vegetables | Carrot, 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) |
| Fruits | Banana (firm/unripe), strawberry, blueberry, raspberry, kiwi, pineapple, orange, mandarin, lemon, lime, grapes, papaya, passionfruit |
| Dairy | Hard cheeses (cheddar, parmesan, brie), lactose-free milk/yoghurt, butter |
| Proteins | All plain meat, poultry, fish, seafood, eggs, firm tofu, tempeh |
| Nuts/seeds | Macadamia, peanut, pecan, walnut, Brazil nut, chia seeds, pumpkin seeds |
| Beverages | Water, most teas, coffee (small amounts), most wine/beer/spirits |
| Sweeteners | Sucrose (table sugar), glucose, maple syrup (small amounts), stevia |
| Category | Examples |
|---|---|
| Grains | Wheat bread/pasta, rye, barley |
| Vegetables | Onion, garlic, leek, shallot, asparagus, artichoke, mushrooms (button), cauliflower, beetroot (large portions) |
| Fruits | Apple, pear, mango, watermelon, peach, apricot, cherry, plum, prune, blackberry, dates |
| Dairy | Cow's milk, soft cheese, ice cream, yoghurt (regular), soy milk |
| Legumes | Most beans, lentils, chickpeas, baked beans, falafel |
| Sweeteners | Honey, high-fructose corn syrup, sorbitol, mannitol, xylitol, isomalt (sugar-free products) |
| Point | Detail |
|---|---|
| Serving sizes matter | Many foods are low-FODMAP at small portions but high-FODMAP at larger ones (e.g., sweet potato, broccoli) |
| Garlic/onion substitutes | Garlic-infused oil (FODMAPs are not oil-soluble), chives, spring onion green tops |
| Eating out | Difficult - the biggest adherence challenge |
| Apps | Monash University FODMAP app - the gold standard reference for serve sizes |
| Duration of Phase 1 | 2 weeks minimum, 6 weeks maximum before reintroduction |
| Who should supervise | Registered dietitian with FODMAP training |
| Who should NOT do it | Patients with active eating disorders, undernutrition, or those who cannot access dietitian support |
Create a printable low-FODMAP foods guide (PDF) for patient counselling
python3 -c "from reportlab.platypus import SimpleDocTemplate; from reportlab.lib.pagesizes import A4; print('ReportLab OK')"
~/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}")
cd /home/daytona/workspace/fd-flowchart && python3 build_fodmap_pdf.py 2>&1
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') "
Low FODMAP Patient Guide
PDF Document · PDF