Prepare a flow chart of diagnostic algorithm of women with abdominal mass
python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/5bc63290-afb4-4076-a271-855cf69448af/Fibromyoma.pdf') as pdf: print(f'Pages: {len(pdf.pages)}') for i, page in enumerate(pdf.pages[:5]): print(f'--- Page {i+1} ---') print(page.extract_text()[:2000]) "
python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/5bc63290-afb4-4076-a271-855cf69448af/Fibromyoma.pdf') as pdf: for i, page in enumerate(pdf.pages[5:20]): print(f'--- Page {i+6} ---') t = page.extract_text() if t: print(t[:1500]) print() "
python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/5bc63290-afb4-4076-a271-855cf69448af/Fibromyoma.pdf') as pdf: for i, page in enumerate(pdf.pages[20:47]): print(f'--- Page {i+21} ---') t = page.extract_text() if t: print(t[:2000]) print() "
mkdir -p /tmp/workspace/abdominal-mass-flowchart && ls /tmp/workspace/abdominal-mass-flowchart/
~/abdominal-mass-flowchart/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(1, 1, figsize=(28, 42))
ax.set_xlim(0, 28)
ax.set_ylim(0, 42)
ax.axis('off')
fig.patch.set_facecolor('#F0F4F8')
# ── colour palette ──────────────────────────────────────────────
C = {
'title': '#1A237E',
'start': '#283593',
'history': '#1565C0',
'exam': '#0277BD',
'bedside': '#00695C',
'invest': '#558B2F',
'origin': '#E65100',
'gynae': '#AD1457',
'uterine': '#C62828',
'adnexal': '#6A1B9A',
'ovarian': '#4527A0',
'nongynae': '#37474F',
'uro': '#2E7D32',
'gi': '#F57F17',
'retro': '#4E342E',
'arrow': '#455A64',
'text_dark': '#212121',
'text_light':'#FFFFFF',
'leaf': '#E8F5E9',
'leaf2': '#FFF3E0',
}
def box(ax, x, y, w, h, text, fc='#E3F2FD', ec='#1565C0', tc='#212121',
fs=8.5, bold=False, radius=0.3, ha='center', va='center', lw=1.4):
rect = FancyBboxPatch((x - w/2, y - h/2), w, h,
boxstyle=f"round,pad={radius}",
facecolor=fc, edgecolor=ec, linewidth=lw, zorder=3)
ax.add_patch(rect)
weight = 'bold' if bold else 'normal'
ax.text(x, y, text, ha=ha, va=va, fontsize=fs, color=tc,
fontweight=weight, wrap=True, zorder=4,
multialignment='center')
def diamond(ax, x, y, w, h, text, fc='#FFF9C4', ec='#F57F17', tc='#212121', fs=8):
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.6, zorder=3)
ax.add_patch(poly)
ax.text(x, y, text, ha='center', va='center', fontsize=fs,
color=tc, fontweight='bold', zorder=4, multialignment='center')
def arr(ax, x1, y1, x2, y2, label='', color='#455A64', lw=1.5):
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.1, my, label, fontsize=7.5, color=color, fontstyle='italic', zorder=5)
# ════════════════════════════════════════════════════════════════
# TITLE
# ════════════════════════════════════════════════════════════════
ax.text(14, 41.2, 'DIAGNOSTIC ALGORITHM', ha='center', va='center',
fontsize=22, fontweight='bold', color=C['title'], zorder=5)
ax.text(14, 40.6, 'Abdominal / Pelvic Mass in Women', ha='center', va='center',
fontsize=14, color='#37474F', zorder=5)
# ─── TOP: Presentation ──────────────────────────────────────────
box(ax, 14, 39.7, 9, 0.8,
'WOMAN PRESENTS WITH ABDOMINAL / PELVIC MASS',
fc=C['start'], ec=C['start'], tc='white', fs=11, bold=True)
arr(ax, 14, 39.3, 14, 38.6)
# ─── History ────────────────────────────────────────────────────
box(ax, 14, 38.1, 11, 0.9,
'HISTORY ▸ Age · Parity · Menstrual Hx · Pain · AUB · Urinary/Bowel Sx\n'
'Weight loss · Fever · Family Hx (Ca) · OCP/HRT use · Subfertility',
fc='#E3F2FD', ec=C['history'], fs=8.5, bold=False)
arr(ax, 14, 37.65, 14, 36.9)
# ─── Physical Examination ───────────────────────────────────────
box(ax, 14, 36.4, 12, 0.9,
'PHYSICAL EXAMINATION ▸ Site · Size · Surface · Consistency · Mobility\n'
'P/V & P/R Bimanual · Fothergill\'s Sign · Cervical Motion Tenderness · Ascites',
fc='#E3F2FD', ec=C['exam'], fs=8.5)
arr(ax, 14, 35.95, 14, 35.3)
# ─── Bedside diagnostics ────────────────────────────────────────
box(ax, 14, 34.85, 13, 0.8,
'BEDSIDE DIAGNOSTICS ▸ Mass below umbilicus vs above · Uterus moves with cervix?\n'
'Resonant vs dull percussion · Fothergill\'s sign · Bladder catheterisation if midline',
fc='#E0F2F1', ec=C['bedside'], fs=8.5)
arr(ax, 14, 34.45, 14, 33.75)
# ─── First-line investigations ──────────────────────────────────
box(ax, 14, 33.25, 12, 0.9,
'1st-LINE INVESTIGATIONS ▸ USG Abdomen & Pelvis (TVS preferred)\n'
'CBC · CRP/ESR · Urine hCG (exclude pregnancy) · Serum CA-125',
fc='#F1F8E9', ec=C['invest'], fs=8.5)
arr(ax, 14, 32.8, 14, 32.1)
# ─── DIAMOND: Origin? ───────────────────────────────────────────
diamond(ax, 14, 31.5, 5.5, 1.2,
'Determine\nOrgan of Origin?',
fc='#FFFDE7', ec=C['origin'])
# ── Branch arrows ────────────────────────────────────────────────
# Left (Gynaecological)
ax.annotate('', xy=(5.5, 31.5), xytext=(11.25, 31.5),
arrowprops=dict(arrowstyle='->', color=C['gynae'], lw=1.8), zorder=2)
ax.text(8.0, 31.7, 'GYNAECOLOGICAL', ha='center', fontsize=8.5, color=C['gynae'], fontweight='bold')
# Right (Non-gynaecological)
ax.annotate('', xy=(22.5, 31.5), xytext=(16.75, 31.5),
arrowprops=dict(arrowstyle='->', color=C['nongynae'], lw=1.8), zorder=2)
ax.text(19.8, 31.7, 'NON-GYNAECOLOGICAL', ha='center', fontsize=8.5, color=C['nongynae'], fontweight='bold')
# ════════════════════════════════════════════════════════════════
# LEFT COLUMN – GYNAECOLOGICAL
# ════════════════════════════════════════════════════════════════
box(ax, 5.5, 30.9, 5, 0.7, 'GYNAECOLOGICAL\nORIGIN',
fc=C['gynae'], ec=C['gynae'], tc='white', fs=9, bold=True)
arr(ax, 5.5, 30.55, 5.5, 29.85)
# Uterine vs Adnexal diamond
diamond(ax, 5.5, 29.2, 5, 1.1, 'Uterine\nor Adnexal?',
fc='#FCE4EC', ec=C['gynae'])
# ── UTERINE (left of diamond) ────────────────────────────────────
ax.annotate('', xy=(1.8, 29.2), xytext=(3.0, 29.2),
arrowprops=dict(arrowstyle='->', color=C['uterine'], lw=1.6), zorder=2)
ax.text(2.4, 29.45, 'UTERINE', ha='center', fontsize=7.5, color=C['uterine'], fontweight='bold')
box(ax, 1.8, 28.6, 3.0, 0.9,
'UTERINE CAUSES\n(moves with cervix;\nno groove btw uterus & mass)',
fc='#FFEBEE', ec=C['uterine'], fs=7.5)
arr(ax, 1.8, 28.15, 1.8, 27.55)
# Uterine sub-boxes
for i, (txt, clr) in enumerate([
('FIBROMYOMA\n▸ Firm-hard, irregular\n▸ Nodular surface\n▸ Restricted mobility\n▸ AUB / Menorrhagia\n▸ TVS: hypo/hyperechoic\n▸ MRI: exact mapping', '#FFCDD2'),
('ADENOMYOSIS\n▸ Symmetrical, tender\n▸ ≤12wk size\n▸ Menorrhagia +\n dysmenorrhoea\n▸ MRI (choice)\n▸ TVS: ill-defined hypoechoic', '#FFCDD2'),
('PYOMETRA\n▸ Uniform suprapubic\n▸ Purulent discharge\n▸ USG: fluid-filled cavity\n▸ R/O endometrial/\n cervical Ca', '#FFCDD2'),
]):
ypos = 26.8 - i*1.7
box(ax, 1.8, ypos, 3.2, 1.35, txt,
fc=clr, ec=C['uterine'], fs=6.8, bold=False)
if i < 2:
arr(ax, 1.8, ypos - 0.68, 1.8, ypos - 1.02)
# ── ADNEXAL (right of diamond) ───────────────────────────────────
ax.annotate('', xy=(9.0, 29.2), xytext=(8.0, 29.2),
arrowprops=dict(arrowstyle='->', color=C['adnexal'], lw=1.6), zorder=2)
ax.text(8.5, 29.45, 'ADNEXAL', ha='center', fontsize=7.5, color=C['adnexal'], fontweight='bold')
# Adnexal box
box(ax, 9.5, 28.6, 3.5, 0.9,
'ADNEXAL / TUBAL-OVARIAN\n(separate from uterus;\ncervix does NOT move with mass)',
fc='#EDE7F6', ec=C['adnexal'], fs=7.5)
arr(ax, 9.5, 28.15, 9.5, 27.5)
# Adnexal causes
adnexal = [
('HYDROSALPINX\n▸ Thin-walled tubular\n cystic mass\n▸ TVS: beads-on-a-string\n▸ HSG: distal obstruction\n▸ Laparoscopy (gold std)', '#D1C4E9'),
('TUBO-OVARIAN\nABSCESS\n▸ Fever + adnexal mass\n▸ CМT +ve\n▸ WBC↑ / CRP↑\n▸ TVS: complex thick-\n walled cystic mass\n▸ CT if uncertain', '#D1C4E9'),
('OVARIAN MASS\n(see right column\nfor full algorithm)', '#D1C4E9'),
]
for i, (txt, clr) in enumerate(adnexal):
ypos = 26.7 - i * 1.75
box(ax, 9.5, ypos, 3.4, 1.4, txt,
fc=clr, ec=C['adnexal'], fs=6.8)
if i < 2:
arr(ax, 9.5, ypos - 0.70, 9.5, ypos - 1.05)
# ════════════════════════════════════════════════════════════════
# CENTRE-RIGHT COLUMN – OVARIAN ALGORITHM
# ════════════════════════════════════════════════════════════════
box(ax, 14, 30.9, 5, 0.7, 'OVARIAN MASS ALGORITHM',
fc='#4527A0', ec='#4527A0', tc='white', fs=9, bold=True)
arr(ax, 14, 30.55, 14, 29.85)
# Advanced investigations
box(ax, 14, 29.3, 5.8, 0.9,
'ADVANCED INVESTIGATIONS\n'
'TVS + Colour Doppler · MRI pelvis · CT abdomen/pelvis\n'
'Tumour markers: CA-125 · AFP · β-hCG · LDH · Inhibin',
fc='#EDE7F6', ec=C['ovarian'], fs=7.8)
arr(ax, 14, 28.85, 14, 28.15)
diamond(ax, 14, 27.5, 5.5, 1.2,
'Functional\nor Neoplastic?',
fc='#E8EAF6', ec=C['ovarian'])
# Functional left
ax.annotate('', xy=(10.2, 27.5), xytext=(11.25, 27.5),
arrowprops=dict(arrowstyle='->', color=C['ovarian'], lw=1.5), zorder=2)
ax.text(10.7, 27.7, 'FUNCTIONAL', ha='center', fontsize=7.5, color=C['ovarian'], fontweight='bold')
# Neoplastic right
ax.annotate('', xy=(17.8, 27.5), xytext=(16.75, 27.5),
arrowprops=dict(arrowstyle='->', color=C['ovarian'], lw=1.5), zorder=2)
ax.text(17.3, 27.7, 'NEOPLASTIC', ha='center', fontsize=7.5, color=C['ovarian'], fontweight='bold')
# Functional cysts
box(ax, 9.8, 26.2, 4.0, 2.0,
'FUNCTIONAL CYSTS\n\nFollicular Cyst\n▸ <3 cm: no Ix\n▸ <7 cm, simple: repeat USG 3-6/12\n▸ >7 cm or persistent: cystectomy\n\nCorpus Luteum Cyst\n▸ hCG, USG, culdocentesis\n▸ Often spontaneous regression\n▸ Rupture: laparoscopy\n\nTheca Lutein Cyst\n▸ Bilateral; assoc. GTD\n▸ Suppress with COCPs',
fc='#E8EAF6', ec=C['ovarian'], fs=6.8)
# Neoplastic
box(ax, 18.2, 26.2, 5.0, 2.0,
'OVARIAN NEOPLASM\n\nBENIGN\n▸ Serous/Mucinous cystadenoma\n▸ Dermoid cyst (Mature teratoma)\n▸ Fibroma, Thecoma\n▸ Endometrioma (chocolate cyst)\n\nMALIGNANT\n▸ Epithelial Ca (serous/mucinous/\n endometrioid/clear cell)\n▸ Germ cell (Dysgerminoma etc.)\n▸ Sex cord-stromal (GCT, SLCT)\n▸ Metastatic (Krukenberg)',
fc='#EDE7F6', ec='#311B92', fs=6.8)
arr(ax, 18.2, 25.2, 18.2, 24.5)
# RMI assessment box
box(ax, 18.2, 23.9, 5.0, 1.0,
'RISK OF MALIGNANCY ASSESSMENT\n'
'RMI / IOTA criteria / ADNEX model\n'
'CT chest-abdomen-pelvis for staging',
fc='#311B92', ec='#1A0072', tc='white', fs=7.5, bold=False)
arr(ax, 18.2, 23.4, 18.2, 22.7)
box(ax, 18.2, 22.3, 5.0, 0.75,
'DEFINITIVE MANAGEMENT\n'
'Benign: Cystectomy / Oophorectomy\n'
'Malignant: TAH+BSO+Omentectomy ± Chemo',
fc='#E8EAF6', ec='#311B92', fs=7.5)
# ════════════════════════════════════════════════════════════════
# RIGHT COLUMN – NON-GYNAECOLOGICAL
# ════════════════════════════════════════════════════════════════
box(ax, 22.5, 30.9, 5, 0.7, 'NON-GYNAECOLOGICAL\nORIGIN',
fc=C['nongynae'], ec=C['nongynae'], tc='white', fs=9, bold=True)
arr(ax, 22.5, 30.55, 22.5, 29.85)
non_gynae = [
('UROLOGICAL\n▸ Distended bladder (midline, dull,\n disappears on catheterisation)\n▸ Pelvic kidney · Hydronephrosis\n▸ Polycystic kidney · Renal Ca\n→ CT urogram / Urology referral',
'#E8F5E9', C['uro']),
('GASTROINTESTINAL\n▸ Appendicular mass/abscess\n (RIF, tender, fever, WBC↑)\n▸ Cecal / Sigmoid carcinoma\n (irregular, hard, bowel habit ΔΔ)\n▸ GIST\n→ CT / Colonoscopy / FNAC',
'#FFF8E1', C['gi']),
('RETROPERITONEAL /\nABDOMINAL WALL\n▸ Mesenteric cyst (resonant,\n perpendicular mobility)\n▸ Encysted ascites\n▸ Retro sarcoma / Lymphoma\n▸ Desmoid tumor\n→ CT / MRI / Image-guided biopsy',
'#EFEBE9', C['retro']),
]
for i, (txt, fc, ec) in enumerate(non_gynae):
ypos = 29.0 - i * 2.2
box(ax, 22.5, ypos, 5.0, 1.8, txt, fc=fc, ec=ec, fs=7.0)
if i < 2:
arr(ax, 22.5, ypos - 0.90, 22.5, ypos - 1.30)
# ════════════════════════════════════════════════════════════════
# BOTTOM: INVESTIGATION SUMMARY PANEL
# ════════════════════════════════════════════════════════════════
# Horizontal divider
ax.plot([0.5, 27.5], [20.3, 20.3], color='#90A4AE', lw=1.5, linestyle='--')
box(ax, 14, 20.0, 27, 0.45,
'INVESTIGATION SUMMARY TABLE',
fc='#37474F', ec='#37474F', tc='white', fs=10, bold=True)
inv_cols = [
('IMAGING', '#1565C0', '#E3F2FD',
'USG Abdomen/Pelvis (TVS)\n→ 1st-line; cystic vs solid\n\nMRI Pelvis\n→ Tissue characterisation;\n adenomyosis vs fibroid\n\nCT Abdomen/Pelvis\n→ Staging; lymph nodes;\n retro / GIT masses'),
('TUMOUR MARKERS', '#6A1B9A', '#F3E5F5',
'CA-125 → Epithelial ovarian Ca\nAFP → Yolk sac tumor\nβ-hCG → Choriocarcinoma\nLDH → Dysgerminoma\nInhibin → Granulosa cell tumor\nCEA → Mucinous tumors\nCA 19-9 → Mucinous tumors'),
('SPECIAL PROCEDURES', '#00695C', '#E0F2F1',
'Diagnostic Laparoscopy\n→ Gold std for adnexal/tubal\n pathology; pelvic endo\n\nHysteroscopy\n→ Intracavitary fibroids;\n endometrial biopsy\n\nSIS / HSG\n→ Submucous fibroids;\n tubal patency'),
('LABS & OTHERS', '#E65100', '#FFF3E0',
'CBC → Anemia / Infection\nLFT → Liver metastasis\nRFT → Pre-chemo baseline\nUrine hCG → R/O pregnancy\nEndocervical swabs → PID\nAscitic fluid cytology\nHistopathology (gold std)'),
]
for i, (hdr, ec, fc, body) in enumerate(inv_cols):
x = 1.5 + i * 6.5
box(ax, x, 19.5, 6.0, 0.5, hdr, fc=ec, ec=ec, tc='white', fs=8.5, bold=True)
box(ax, x, 17.7, 6.0, 3.2, body, fc=fc, ec=ec, fs=7.5, va='center')
# ── Arrows from bottom of main algo to summary panel
arr(ax, 14, 22.0, 14, 20.25, color='#78909C')
# ════════════════════════════════════════════════════════════════
# FOTHERGILL's & CLINICAL CLUES panel
# ════════════════════════════════════════════════════════════════
ax.plot([0.5, 27.5], [15.8, 15.8], color='#90A4AE', lw=1.5, linestyle='--')
box(ax, 14, 15.55, 27, 0.4,
'KEY CLINICAL CLUES & DIFFERENTIATING SIGNS',
fc='#263238', ec='#263238', tc='white', fs=10, bold=True)
clues = [
('Mass moves with cervix', 'Uterine origin', '#C62828'),
('Mass separate; cervix fixed', 'Ovarian/adnexal origin', '#6A1B9A'),
('Disappears on catheterisation', 'Distended bladder', '#2E7D32'),
('Fothergill\'s sign +ve\n(persists on rectus contraction)', 'Abdominal wall mass', '#1565C0'),
('Resonant percussion over mass', 'Mesenteric/bowel origin', '#4E342E'),
('Bilateral, cystic, rapid onset', 'Consider theca lutein / GTD', '#0277BD'),
('Menorrhagia + worsening dysmenorrhoea', 'Adenomyosis strongly suggested', '#AD1457'),
('Irregular, firm, nodular uterus', 'Fibromyoma', '#C62828'),
]
cols = 4
for i, (sign, dx, clr) in enumerate(clues):
col = i % cols
row = i // cols
x = 2.0 + col * 6.5
y = 14.8 - row * 1.05
box(ax, x, y, 6.1, 0.85, f'Sign: {sign}\n→ Dx: {dx}',
fc='#FAFAFA', ec=clr, fs=7.5, bold=False)
# ════════════════════════════════════════════════════════════════
# MANAGEMENT SUMMARY PANEL
# ════════════════════════════════════════════════════════════════
ax.plot([0.5, 27.5], [12.6, 12.6], color='#90A4AE', lw=1.5, linestyle='--')
box(ax, 14, 12.35, 27, 0.4,
'MANAGEMENT PRINCIPLES BY DIAGNOSIS',
fc='#1B5E20', ec='#1B5E20', tc='white', fs=10, bold=True)
mgmt = [
('FIBROMYOMA', '#C62828', '#FFEBEE',
'Small/Asymptomatic: Observation\nMedical: GnRH agonists, LNG-IUS\nSurgical: Myomectomy (fertility)\nHysterectomy (definitive)'),
('ADENOMYOSIS', '#AD1457', '#FCE4EC',
'NSAIDs; GnRH agonists; Mirena LNG-IUS\nHysterectomy (definitive)\nAdenomyomectomy (fertility-sparing)\nMRgFUS (emerging)'),
('TOA / HYDROSALPINX', '#37474F', '#ECEFF1',
'TOA: IV antibiotics 14 days;\ndrainage if ≥8 cm or no response\nHydrosalpinx: Salpingectomy;\nIVF for severe tubal disease'),
('OVARIAN CYST\n(Benign)', '#4527A0', '#EDE7F6',
'Functional: Expectant / COCPs\n<7 cm simple: USG in 3-6/12\nPersistent/large: Cystectomy\n(lap or open)'),
('OVARIAN Ca\n(Malignant)', '#311B92', '#E8EAF6',
'TAH + BSO + Omentectomy +\nperitoneal washings\nFertility-sparing (Stage Ia, young)\nChemo: Carboplatin + Paclitaxel\nTargeted: Bevacizumab, PARP inh.'),
('NON-GYNAECOLOGICAL', '#263238', '#ECEFF1',
'Bladder: Catheterise + Urology\nAppendix mass: Ochsner-Sherren;\nInterval appendectomy\nBowel Ca: Hemicolectomy\nTB mass: ATT'),
]
for i, (hdr, ec, fc, body) in enumerate(mgmt):
col = i % 3
row = i // 3
x = 2.5 + col * 8.5
y = 11.3 - row * 2.5
box(ax, x, y, 7.8, 0.5, hdr, fc=ec, ec=ec, tc='white', fs=8.5, bold=True)
box(ax, x, y - 0.9, 7.8, 1.6, body, fc=fc, ec=ec, fs=7.5, va='center')
# ── Footer ───────────────────────────────────────────────────────
ax.text(14, 0.35,
'Sources: DC Dutta\'s Textbook of Gynaecology · Shaw\'s Textbook of Gynaecology · Williams Gynaecology',
ha='center', fontsize=7, color='#607D8B', style='italic')
plt.tight_layout(pad=0.2)
plt.savefig('/tmp/workspace/abdominal-mass-flowchart/diagnostic_algorithm.png',
dpi=180, bbox_inches='tight', facecolor=fig.get_facecolor())
print("Saved successfully.")
cd /tmp/workspace/abdominal-mass-flowchart && python3 flowchart.py 2>&1
ls -lh /tmp/workspace/abdominal-mass-flowchart/
diagnostic algorithm
Image · PNG
Generate as powerpoint
~/abdominal-mass-flowchart/build_pptx.py
"""
Diagnostic Algorithm: Abdominal/Pelvic Mass in Women
Multi-slide PPTX with embedded flowchart visuals via matplotlib + python-pptx
"""
import io, textwrap
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.util import Inches, Pt
# ── Colour palette ──────────────────────────────────────────────
PAL = dict(
navy='#1A237E', blue='#1565C0', teal='#00695C', green='#2E7D32',
purple='#6A1B9A', dpurple='#4527A0', xpurple='#311B92',
red='#C62828', pink='#AD1457', orange='#E65100', amber='#F57F17',
grey='#37474F', dgrey='#263238', brown='#4E342E',
ltblue='#E3F2FD', ltgreen='#E8F5E9', ltpurple='#EDE7F6',
ltred='#FFEBEE', ltpink='#FCE4EC', ltorange='#FFF3E0',
ltamber='#FFF8E1', white='#FFFFFF', offwhite='#F0F4F8',
)
def hex2rgb(h):
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def rgb(h):
return RGBColor(*hex2rgb(h))
# ══════════════════════════════════════════════════════════════════
# Helper: render a matplotlib figure and return a BytesIO PNG stream
# ══════════════════════════════════════════════════════════════════
def fig_to_stream(fig):
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=200, bbox_inches='tight',
facecolor=fig.get_facecolor())
buf.seek(0)
return buf
# ══════════════════════════════════════════════════════════════════
# Matplotlib helper functions (shared across figures)
# ══════════════════════════════════════════════════════════════════
def mbox(ax, x, y, w, h, text,
fc='#E3F2FD', ec='#1565C0', tc='#212121',
fs=8.5, bold=False, radius=0.25, va='center', lw=1.4):
rect = FancyBboxPatch((x - w/2, y - h/2), w, h,
boxstyle=f"round,pad={radius}",
facecolor=fc, edgecolor=ec, linewidth=lw, zorder=3)
ax.add_patch(rect)
ax.text(x, y, text, ha='center', va=va, fontsize=fs,
color=tc, fontweight='bold' if bold else 'normal',
zorder=4, multialignment='center')
def diamond(ax, x, y, w, h, text,
fc='#FFFDE7', ec='#F57F17', tc='#212121', fs=8.5):
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=fs,
color=tc, fontweight='bold', zorder=4, multialignment='center')
def arr(ax, x1, y1, x2, y2, label='', color='#455A64', lw=1.6):
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+0.05, (y1+y2)/2
ax.text(mx, my, label, fontsize=7, color=color,
fontstyle='italic', zorder=5)
def harr(ax, x1, y, x2, label='', color='#455A64', lw=1.6):
ax.annotate('', xy=(x2, y), xytext=(x1, y),
arrowprops=dict(arrowstyle='->', color=color, lw=lw), zorder=2)
if label:
ax.text((x1+x2)/2, y+0.08, label, ha='center', fontsize=7,
color=color, fontweight='bold', zorder=5)
# ══════════════════════════════════════════════════════════════════
# SLIDE 1 FIGURE: Title / Overview
# ══════════════════════════════════════════════════════════════════
def make_title_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#0D1B4B')
# gradient-like header band
for i, alpha in enumerate([0.18, 0.14, 0.10]):
ax.add_patch(mpatches.Rectangle((0, 9-(i+1)*1.0), 16, 1.0,
fc='white', alpha=alpha, zorder=1))
ax.text(8, 7.8, 'DIAGNOSTIC ALGORITHM',
ha='center', fontsize=32, fontweight='bold',
color='white', zorder=5)
ax.text(8, 7.0, 'Abdominal / Pelvic Mass in Women',
ha='center', fontsize=20, color='#90CAF9', zorder=5)
ax.plot([2, 14], [6.65, 6.65], color='#42A5F5', lw=2)
# 6 topic boxes
topics = [
('1. History &\nExamination', '#1565C0', '#E3F2FD'),
('2. Bedside\nDiagnostics', '#00695C', '#E0F2F1'),
('3. First-line\nInvestigations', '#558B2F', '#F1F8E9'),
('4. Uterine\nCauses', '#C62828', '#FFEBEE'),
('5. Adnexal /\nOvarian Causes', '#6A1B9A', '#EDE7F6'),
('6. Non-Gynae\nCauses', '#37474F', '#ECEFF1'),
]
cols = 3
for i, (title, ec, fc) in enumerate(topics):
col = i % cols
row = i // cols
x = 1.8 + col * 4.3
y = 5.2 - row * 2.2
mbox(ax, x, y, 3.8, 1.6, title, fc=fc, ec=ec, tc=ec, fs=12, bold=True)
ax.text(8, 0.4,
'Sources: DC Dutta\'s Textbook of Gynaecology · Shaw\'s Textbook · Williams Gynaecology',
ha='center', fontsize=8, color='#90CAF9', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 2 FIGURE: History & Examination + Bedside Diagnostics
# ══════════════════════════════════════════════════════════════════
def make_history_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#F0F4F8')
ax.text(8, 8.6, 'Step 1 & 2: History, Examination & Bedside Diagnostics',
ha='center', fontsize=16, fontweight='bold', color='#1A237E')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#1565C0', lw=2)
# HISTORY box
mbox(ax, 4.0, 7.6, 7.5, 0.9,
'HISTORY', fc='#1565C0', ec='#1565C0', tc='white', fs=12, bold=True)
history_items = [
'▸ Age, parity, menopausal status',
'▸ Menstrual history: AUB, menorrhagia, dysmenorrhoea',
'▸ Pain: onset, character, radiation, associated features',
'▸ Urinary / bowel symptoms (pressure effects)',
'▸ Subfertility / recurrent pregnancy loss',
'▸ Weight loss, fever, anorexia (malignancy screen)',
'▸ Family history of gynaecological / GI malignancy',
'▸ OCP / HRT use; prior pelvic surgery or STI',
]
for i, item in enumerate(history_items):
ax.text(0.6, 6.95 - i * 0.35, item, fontsize=9, color='#212121')
# EXAMINATION box
mbox(ax, 12.0, 7.6, 7.5, 0.9,
'PHYSICAL EXAMINATION', fc='#0277BD', ec='#0277BD', tc='white', fs=12, bold=True)
exam_items = [
'▸ General: pallor, lymphadenopathy, cachexia',
'▸ Abdominal: site, size, shape, surface, margins',
'▸ Consistency: firm-hard (fibroid), cystic (ovarian)',
'▸ Mobility: side-to-side vs restricted',
'▸ Percussion: dull (solid/fluid) vs resonant (bowel)',
'▸ P/V bimanual: uterine vs adnexal; cervical motion',
'▸ P/R: POD nodules, uterosacral involvement',
'▸ Confirm lower pole unreachable → pelvic origin',
]
for i, item in enumerate(exam_items):
ax.text(8.6, 6.95 - i * 0.35, item, fontsize=9, color='#212121')
ax.plot([0.3, 15.7], [4.1, 4.1], color='#00695C', lw=1.5, linestyle='--')
# BEDSIDE diagnostics
mbox(ax, 8.0, 3.7, 15.0, 0.7,
'BEDSIDE DIAGNOSTIC CLUES', fc='#00695C', ec='#00695C', tc='white', fs=12, bold=True)
bedside = [
('Uterus moves with cervix?', 'YES → Uterine origin', '#C62828'),
('Separate from uterus; cervix fixed?', 'YES → Adnexal/ovarian origin', '#6A1B9A'),
('Mass disappears on catheterisation?', 'YES → Distended bladder', '#2E7D32'),
("Fothergill's sign +ve?", 'Mass persists on rectus contraction\n→ Abdominal wall origin', '#1565C0'),
('Resonant note over mass?', 'Mesenteric/bowel mass', '#4E342E'),
('Lower pole cannot be reached?', 'Pelvic (not abdominal) origin', '#AD1457'),
]
cols = 2
for i, (sign, dx, clr) in enumerate(bedside):
col = i % cols
row = i // cols
x = 2.2 + col * 7.5
y = 2.9 - row * 0.85
mbox(ax, x, y, 7.0, 0.7,
f'Sign: {sign}\n→ {dx}',
fc='#FAFAFA', ec=clr, tc='#212121', fs=8.5)
ax.text(8, 0.25,
'KEY: Cervical traction distinguishes uterine from adnexal · Fothergill\'s sign distinguishes abdominal wall from intraperitoneal mass',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 3 FIGURE: Investigations
# ══════════════════════════════════════════════════════════════════
def make_investigations_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#F0F4F8')
ax.text(8, 8.6, 'Step 3: Investigations',
ha='center', fontsize=16, fontweight='bold', color='#1A237E')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#1565C0', lw=2)
panels = [
('IMAGING', '#1565C0', '#E3F2FD',
['USG Abdomen & Pelvis (TVS) — 1st-line',
' ▸ Cystic vs solid; organ of origin',
' ▸ Fibroid: hypo/hyperechoic, peripheral vascularisation',
' ▸ TOA: complex thick-walled cystic mass',
' ▸ Hydrosalpinx: beads-on-a-string sign',
'MRI Pelvis',
' ▸ Best for adenomyosis vs fibroid',
' ▸ Soft tissue characterisation',
'CT Abdomen & Pelvis',
' ▸ Staging malignancy; lymph node assessment',
' ▸ Retroperitoneal / GIT masses',
'3D USG — accurate fibroid localisation',
'SIS — submucous fibroid / endometrial polyp',
]),
('TUMOUR MARKERS', '#6A1B9A', '#F3E5F5',
['CA-125 → Epithelial ovarian carcinoma',
'AFP (Alpha-fetoprotein) → Yolk sac tumour',
'β-hCG → Choriocarcinoma / GTD',
'LDH → Dysgerminoma',
'Inhibin → Granulosa cell tumour',
'CEA / CA 19-9 → Mucinous tumours',
'Note: CA-125 usually NEGATIVE in hydrosalpinx',
' (helps differentiate from ovarian malignancy)',
]),
('SPECIAL PROCEDURES', '#00695C', '#E0F2F1',
['Diagnostic Laparoscopy (Gold Standard)',
' ▸ Pelvic endometriosis / tubal pathology',
' ▸ Differentiates pedunculated fibroid from',
' ovarian tumour',
'Hysteroscopy',
' ▸ Direct cavity visualisation',
' ▸ Submucous fibroid; endometrial biopsy',
'SIS / HSG',
' ▸ Submucous fibroid / tubal patency',
'Uterine Curettage',
' ▸ Coexisting endometrial pathology',
' ▸ Hysteroscopy + biopsy preferred',
]),
('LABS & OTHERS', '#E65100', '#FFF3E0',
['CBC → Anaemia (AUB), infection (TOA)',
'CRP / ESR → Raised in PID, TOA',
'LFT → Liver metastasis screen',
'RFT → Pre-chemo / contrast CT baseline',
'Urine hCG → Exclude pregnancy (all ages)',
'Cervical / vaginal swabs → STI / PID',
'Ascitic fluid cytology → Malignant cells',
'Histopathology → Gold standard for Dx,',
' tumour type & grade',
'Colonoscopy / OGD → Rule out GIT Ca',
'Image-guided FNAC / Biopsy → Lymphoma,',
' TB mass, retroperitoneal sarcoma',
]),
]
for i, (hdr, ec, fc, items) in enumerate(panels):
col = i % 2
row = i // 2
x0 = 0.3 + col * 8.0
y0 = 7.6 - row * 3.8
w, h = 7.6, 3.5
# header
mbox(ax, x0 + w/2, y0, w, 0.55, hdr,
fc=ec, ec=ec, tc='white', fs=11, bold=True)
# body background
rect = FancyBboxPatch((x0, y0 - h), w, h,
boxstyle="round,pad=0.1",
facecolor=fc, edgecolor=ec, linewidth=1.2, zorder=2)
ax.add_patch(rect)
for j, item in enumerate(items):
ax.text(x0 + 0.15, y0 - 0.3 - j * 0.27, item,
fontsize=8, color='#212121', zorder=4)
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 4 FIGURE: Main Decision Flowchart
# ══════════════════════════════════════════════════════════════════
def make_main_flow_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#F0F4F8')
ax.text(8, 8.6, 'Step 4: Determine Origin — Main Decision Flow',
ha='center', fontsize=15, fontweight='bold', color='#1A237E')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#1565C0', lw=2)
# START
mbox(ax, 8, 7.8, 9, 0.65,
'WOMAN WITH ABDOMINAL / PELVIC MASS',
fc='#1A237E', ec='#1A237E', tc='white', fs=12, bold=True)
arr(ax, 8, 7.47, 8, 7.05)
# History + Exam + Investigations
mbox(ax, 8, 6.7, 9, 0.6,
'History ▸ Examination ▸ Bedside Tests ▸ USG + Basic Labs',
fc='#E3F2FD', ec='#1565C0', fs=10)
arr(ax, 8, 6.4, 8, 5.98)
# Diamond: Organ of origin
diamond(ax, 8, 5.55, 4.5, 0.9,
'Determine Organ\nof Origin (USG + clinical)',
fc='#FFFDE7', ec='#E65100')
# LEFT: Gynaecological
harr(ax, 5.75, 5.55, 2.5, 'GYNAECOLOGICAL', '#AD1457')
arr(ax, 2.5, 5.1, 2.5, 4.7)
# RIGHT: Non-gynaecological
harr(ax, 10.25, 5.55, 13.5, 'NON-GYNAECOLOGICAL', '#37474F')
arr(ax, 13.5, 5.1, 13.5, 4.7)
# GYNAE sub-diamond
diamond(ax, 2.5, 4.3, 4.0, 0.75,
'Uterine or\nAdnexal?',
fc='#FCE4EC', ec='#AD1457')
harr(ax, 0.5, 4.3, 0.5, '#C62828') # placeholder
ax.annotate('', xy=(0.6, 4.3), xytext=(0.5, 4.3), # tiny stub
arrowprops=dict(arrowstyle='->', color='#C62828', lw=1.2))
# Uterine (below left of gynae diamond)
ax.annotate('', xy=(1.2, 4.3), xytext=(0.5, 4.3),
arrowprops=dict(arrowstyle='->', color='#C62828', lw=1.5))
# Actually draw arrows properly
# Left arm: UTERINE
ax.annotate('', xy=(1.3, 3.55), xytext=(2.5, 3.95),
arrowprops=dict(arrowstyle='->', color='#C62828', lw=1.5))
ax.text(1.5, 3.85, 'UTERINE', fontsize=7.5, color='#C62828', fontweight='bold')
# Right arm: ADNEXAL
ax.annotate('', xy=(4.6, 3.55), xytext=(3.4, 3.95),
arrowprops=dict(arrowstyle='->', color='#6A1B9A', lw=1.5))
ax.text(3.8, 3.85, 'ADNEXAL', fontsize=7.5, color='#6A1B9A', fontweight='bold')
# Uterine causes box
mbox(ax, 1.3, 3.0, 2.4, 0.8,
'UTERINE\n▸ Fibromyoma\n▸ Adenomyosis\n▸ Pyometra',
fc='#FFEBEE', ec='#C62828', fs=8)
# Adnexal causes box
mbox(ax, 4.6, 3.0, 2.4, 0.8,
'ADNEXAL\n▸ Hydrosalpinx\n▸ TOA\n▸ Ovarian mass',
fc='#EDE7F6', ec='#6A1B9A', fs=8)
arr(ax, 1.3, 2.6, 1.3, 2.15)
arr(ax, 4.6, 2.6, 4.6, 2.15)
# See detailed slides note
mbox(ax, 1.3, 1.8, 2.4, 0.55,
'See Slide 5\n(Uterine detail)',
fc='#FFCDD2', ec='#C62828', fs=7.5)
mbox(ax, 4.6, 1.8, 2.4, 0.55,
'See Slide 6\n(Adnexal detail)',
fc='#D1C4E9', ec='#6A1B9A', fs=7.5)
# NON-GYNAE box
mbox(ax, 13.5, 4.3, 4.5, 0.65,
'NON-GYNAECOLOGICAL',
fc='#37474F', ec='#37474F', tc='white', fs=10, bold=True)
arr(ax, 13.5, 3.97, 13.5, 3.6)
ng_items = [
('UROLOGICAL\n▸ Distended bladder\n▸ Pelvic kidney / Hydronephrosis', '#2E7D32', '#E8F5E9'),
('GASTROINTESTINAL\n▸ Appendicular mass\n▸ Cecal / Sigmoid Ca · GIST', '#F57F17', '#FFF8E1'),
('RETROPERITONEAL\n▸ Mesenteric cyst\n▸ Lymphoma / Sarcoma', '#4E342E', '#EFEBE9'),
]
for j, (txt, ec, fc) in enumerate(ng_items):
y = 3.2 - j * 1.05
mbox(ax, 13.5, y, 4.2, 0.85, txt, fc=fc, ec=ec, fs=8)
if j < 2:
arr(ax, 13.5, y - 0.43, 13.5, y - 0.62)
mbox(ax, 13.5, 0.85, 4.2, 0.55,
'See Slide 7\n(Non-Gynae detail)',
fc='#ECEFF1', ec='#37474F', fs=7.5)
# Ovarian algorithm pointer
mbox(ax, 8, 2.3, 4, 0.65,
'OVARIAN MASS\nSub-Algorithm',
fc='#4527A0', ec='#4527A0', tc='white', fs=9, bold=True)
ax.annotate('', xy=(6.82, 2.55), xytext=(4.6+1.2, 2.55),
arrowprops=dict(arrowstyle='->', color='#4527A0', lw=1.5))
mbox(ax, 8, 1.6, 4, 0.55,
'See Slide 8\n(Ovarian algorithm)',
fc='#EDE7F6', ec='#4527A0', fs=7.5)
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 5 FIGURE: Uterine Causes
# ══════════════════════════════════════════════════════════════════
def make_uterine_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#FFF8F8')
ax.text(8, 8.6, 'Uterine Causes of Pelvic Mass',
ha='center', fontsize=16, fontweight='bold', color='#B71C1C')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#C62828', lw=2)
mbox(ax, 8, 7.8, 9.5, 0.65,
'UTERINE ORIGIN (mass moves with cervix · no groove between uterus and mass)',
fc='#C62828', ec='#C62828', tc='white', fs=11, bold=True)
causes = [
{
'title': 'FIBROMYOMA (Leiomyoma)',
'col': '#C62828', 'fc': '#FFEBEE',
'clinical': [
'Most common benign solid tumour in females',
'Asymptomatic in 75%; AUB/Menorrhagia (30%)',
'Dysmenorrhoea, dyspareunia, subfertility',
'Firm-hard, irregular, nodular uterus',
'Mobility: restricted up-down; free side-side',
'Pressure symptoms on bladder / rectum',
],
'ix': [
'TVS + Colour Doppler (1st-line)',
' ▸ Hypo/hyperechoic lesion; peripheral vascularity',
' ▸ Central vascularity → degeneration',
'SIS — submucous fibroids',
'HSG — filling defect (submucous)',
'MRI — most accurate; maps location/size',
'Hysteroscopy — submucous resection',
],
},
{
'title': 'ADENOMYOSIS',
'col': '#AD1457', 'fc': '#FCE4EC',
'clinical': [
'Endometrial glands within myometrium',
'Multiparous women ~40 years',
'Menorrhagia + progressive 2° dysmenorrhoea',
'Symmetrical tender uterus (≤12 wk size)',
'Rarely exceeds 12-week size',
'KEY: pain differentiates from fibroids',
],
'ix': [
'TVS: ill-defined hypoechoic areas; heterogeneous echo',
'MRI (investigation of choice)',
' ▸ Hypoechoic/anechoic areas in wall',
' ▸ Best for differentiating from fibroid',
'Diagnostic Hx + Curettage',
' ▸ Exclude intrauterine pathology',
],
},
{
'title': 'PYOMETRA',
'col': '#E65100', 'fc': '#FFF3E0',
'clinical': [
'Pus collection within uterine cavity',
'Prerequisites: cervical obstruction + infection',
'Causes: carcinoma, senile endometritis,',
' post-procedure (conisation, cautery)',
'Purulent blood-stained vaginal discharge',
'Uniform suprapubic swelling',
],
'ix': [
'USG: distended uterine cavity with fluid',
'MUST exclude endometrial / cervical Ca',
'Management steps:',
' 1. Exclude malignancy',
' 2. Cervical dilatation + drain pus',
' 3. Broad-spectrum IV antibiotics',
' 4. Curettage after 7-14 days',
],
},
]
for i, c in enumerate(causes):
x0 = 0.3 + i * 5.3
w = 5.0
# Title header
mbox(ax, x0 + w/2, 6.85, w, 0.6, c['title'],
fc=c['col'], ec=c['col'], tc='white', fs=10, bold=True)
# Clinical features
mbox(ax, x0 + w/2, 6.45, w, 0.45, 'Clinical Features',
fc=c['fc'], ec=c['col'], fs=9, bold=True)
rect = FancyBboxPatch((x0, 3.65), w, 2.7,
boxstyle="round,pad=0.1",
facecolor=c['fc'], edgecolor=c['col'],
linewidth=1.2, zorder=2)
ax.add_patch(rect)
for j, item in enumerate(c['clinical']):
ax.text(x0+0.15, 6.15 - j*0.41, item, fontsize=8.2, color='#212121', zorder=4)
# Investigations
mbox(ax, x0 + w/2, 3.45, w, 0.45, 'Investigations',
fc=c['fc'], ec=c['col'], fs=9, bold=True)
rect2 = FancyBboxPatch((x0, 0.55), w, 2.8,
boxstyle="round,pad=0.1",
facecolor='#FAFAFA', edgecolor=c['col'],
linewidth=1.2, zorder=2)
ax.add_patch(rect2)
for j, item in enumerate(c['ix']):
ax.text(x0+0.15, 3.15 - j*0.37, item, fontsize=8, color='#212121', zorder=4)
ax.text(8, 0.2,
'KEY CLUE: Menorrhagia + worsening dysmenorrhoea → Adenomyosis · Irregular firm nodular uterus → Fibromyoma · Purulent discharge + suprapubic swelling → Pyometra',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 6 FIGURE: Adnexal Causes
# ══════════════════════════════════════════════════════════════════
def make_adnexal_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#F3E5F5')
ax.text(8, 8.6, 'Adnexal / Tubal-Ovarian Causes',
ha='center', fontsize=16, fontweight='bold', color='#4A148C')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#6A1B9A', lw=2)
mbox(ax, 8, 7.8, 9.5, 0.65,
'ADNEXAL ORIGIN (separate from uterus · bimanual: cervix does NOT move with mass)',
fc='#6A1B9A', ec='#6A1B9A', tc='white', fs=11, bold=True)
causes = [
{
'title': 'HYDROSALPINX',
'col': '#5E35B1', 'fc': '#EDE7F6',
'clinical': [
'Fallopian tube distension with serous fluid',
'End-result of PID / acute salpingitis',
'Often asymptomatic; chronic pelvic pain',
'Infertility; pelvic heaviness',
'Adnexal cystic mass; restricted mobility',
'Torsion → acute pain',
],
'ix': [
'TVS (investigation of choice)',
' ▸ Thin-walled elongated cystic mass',
' ▸ Beads-on-a-string sign',
' ▸ Incomplete septae',
'HSG: distal obstruction; no peritoneal spill',
'Diagnostic laparoscopy (gold standard)',
'CA-125 usually NEGATIVE',
],
},
{
'title': 'TUBO-OVARIAN ABSCESS (TOA)',
'col': '#C62828', 'fc': '#FFEBEE',
'clinical': [
'Inflammatory abscess: tube + ovary + pelvis',
'Complication of PID (most common cause)',
'Fever, lower abdominal pain, vaginal discharge',
'Cervical motion tenderness (CMT)',
'Adnexal tenderness / mass',
'Leukocytosis; raised CRP/ESR',
],
'ix': [
'CBC → Leukocytosis',
'CRP/ESR → Raised',
'TVS: complex cystic mass, thick walls,',
' internal echoes, cul-de-sac fluid',
'CT: thick-walled cystic mass + inflamm.',
'Blood cultures (if septic)',
'Swabs: STI / STD screen',
],
},
{
'title': 'OVARIAN MASS',
'col': '#4527A0', 'fc': '#E8EAF6',
'clinical': [
'Bimanual: uterus separated from mass',
'Mass movement ≠ cervical movement',
'Lower pole felt through fornix',
'Benign: mobile, smooth, cystic',
'Malignant: fixed, nodular, irregular',
'POD nodules → advanced malignancy',
],
'ix': [
'TVS + Colour Doppler',
' ▸ Tumour volume, cyst wall, septa,',
' ▸ Vascularity pattern (neovascularisation)',
'CT: adnexal mass; staging',
'MRI: benign vs malignant',
'Tumour markers: CA-125, AFP, β-hCG',
'→ See Ovarian Algorithm (Slide 8)',
],
},
]
for i, c in enumerate(causes):
x0 = 0.3 + i * 5.3
w = 5.0
mbox(ax, x0 + w/2, 6.85, w, 0.6, c['title'],
fc=c['col'], ec=c['col'], tc='white', fs=10, bold=True)
mbox(ax, x0 + w/2, 6.45, w, 0.45, 'Clinical Features',
fc=c['fc'], ec=c['col'], fs=9, bold=True)
rect = FancyBboxPatch((x0, 3.65), w, 2.7,
boxstyle="round,pad=0.1",
facecolor=c['fc'], edgecolor=c['col'],
linewidth=1.2, zorder=2)
ax.add_patch(rect)
for j, item in enumerate(c['clinical']):
ax.text(x0+0.15, 6.15 - j*0.41, item, fontsize=8.2, color='#212121', zorder=4)
mbox(ax, x0 + w/2, 3.45, w, 0.45, 'Investigations',
fc=c['fc'], ec=c['col'], fs=9, bold=True)
rect2 = FancyBboxPatch((x0, 0.55), w, 2.8,
boxstyle="round,pad=0.1",
facecolor='#FAFAFA', edgecolor=c['col'],
linewidth=1.2, zorder=2)
ax.add_patch(rect2)
for j, item in enumerate(c['ix']):
ax.text(x0+0.15, 3.15 - j*0.37, item, fontsize=8, color='#212121', zorder=4)
ax.text(8, 0.2,
'MANAGEMENT: TOA → IV Antibiotics 14 days; drain if ≥8 cm · Hydrosalpinx → Salpingectomy / Neosalpingostomy · Ovarian mass → see Slide 8',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 7 FIGURE: Non-Gynaecological Causes
# ══════════════════════════════════════════════════════════════════
def make_nongynae_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#ECEFF1')
ax.text(8, 8.6, 'Non-Gynaecological Causes of Abdominal Mass',
ha='center', fontsize=16, fontweight='bold', color='#263238')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#37474F', lw=2)
mbox(ax, 8, 7.8, 9.5, 0.65,
'NON-GYNAECOLOGICAL ORIGIN (bimanual: uterus/adnexa independent of mass)',
fc='#37474F', ec='#37474F', tc='white', fs=11, bold=True)
cols_data = [
{
'title': 'UROLOGICAL', 'col': '#2E7D32', 'fc': '#E8F5E9',
'conditions': [
('Distended Bladder',
'▸ Midline suprapubic dull mass\n▸ Disappears on catheterisation\n▸ Acute/chronic urinary retention'),
('Pelvic Kidney',
'▸ Fixed in pelvis; non-mobile\n▸ IV urogram / CT urogram\n▸ Urology referral'),
('Hydronephrosis',
'▸ Flank mass extending to iliac fossa\n▸ USG: dilated pelvicalyceal system\n▸ CT urogram confirms'),
('Renal Cell Ca / Wilms\' Tumour',
'▸ Haematuria, flank mass\n▸ CT: heterogeneous renal mass\n▸ Urology / oncology referral'),
],
},
{
'title': 'GASTROINTESTINAL', 'col': '#E65100', 'fc': '#FFF3E0',
'conditions': [
('Appendicular Mass / Abscess',
'▸ Fixed tender mass RIF\n▸ Preceded by acute RIF pain + fever\n▸ Ochsner-Sherren regimen initially'),
('Cecal Carcinoma',
'▸ Hard irregular mass RIF\n▸ Anaemia, occult blood loss, wt loss\n▸ Colonoscopy + CT staging'),
('Sigmoid Carcinoma',
'▸ Left iliac fossa mass\n▸ Altered bowel habits, rectal bleeding\n▸ Sigmoidoscopy + CT'),
('GIST',
'▸ Smooth, mobile, well-defined\n▸ CT: heterogeneous enhancing mass\n▸ Surgical resection'),
],
},
{
'title': 'RETROPERITONEAL / WALL', 'col': '#4E342E', 'fc': '#EFEBE9',
'conditions': [
('Mesenteric Cyst',
'▸ Mobile perpendicular to mesentery\n▸ Band of resonance over mass\n▸ CT / MRI; surgical excision'),
('Encysted Ascites',
'▸ Dull, shifting; no free fluid\n▸ History: TB, malignancy\n▸ CT + ascitic fluid cytology'),
('Retroperitoneal Sarcoma /\nLymphoma',
'▸ Deep fixed mass; systemic Sx\n▸ CT/MRI; LDH raised in lymphoma\n▸ Image-guided biopsy'),
('Abdominal Wall\n(Desmoid / Haematoma)',
'▸ Fothergill\'s sign POSITIVE\n▸ Persists on rectus contraction\n▸ USG / MRI; Rectus sheath'),
],
},
]
for i, cat in enumerate(cols_data):
x0 = 0.2 + i * 5.25
w = 5.0
mbox(ax, x0 + w/2, 7.0, w, 0.55, cat['title'],
fc=cat['col'], ec=cat['col'], tc='white', fs=11, bold=True)
for j, (name, detail) in enumerate(cat['conditions']):
y = 6.15 - j * 1.5
mbox(ax, x0 + w/2, y, w, 0.45, name,
fc=cat['fc'], ec=cat['col'], fs=9, bold=True)
lines = detail.split('\n')
for k, line in enumerate(lines):
ax.text(x0 + 0.15, y - 0.28 - k * 0.28, line,
fontsize=8, color='#212121', zorder=4)
ax.text(8, 0.25,
'KEY BEDSIDE TEST: Catheterise for midline mass → rules out distended bladder immediately · Fothergill\'s sign: mass visible on tensing rectus → abdominal wall origin',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 8 FIGURE: Ovarian Mass Algorithm
# ══════════════════════════════════════════════════════════════════
def make_ovarian_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#EDE7F6')
ax.text(8, 8.6, 'Ovarian Mass — Diagnostic Sub-Algorithm',
ha='center', fontsize=16, fontweight='bold', color='#311B92')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#4527A0', lw=2)
# Entry
mbox(ax, 8, 7.75, 8, 0.65,
'OVARIAN MASS CONFIRMED ON CLINICAL EXAM + USG',
fc='#4527A0', ec='#4527A0', tc='white', fs=11, bold=True)
arr(ax, 8, 7.42, 8, 6.98)
# Advanced investigations
mbox(ax, 8, 6.65, 10, 0.6,
'Advanced Ix: TVS + Colour Doppler · MRI · CT · Tumour markers (CA-125, AFP, β-hCG, LDH, Inhibin)',
fc='#E8EAF6', ec='#4527A0', fs=9)
arr(ax, 8, 6.35, 8, 5.93)
# Diamond: Functional vs Neoplastic
diamond(ax, 8, 5.52, 5, 0.85,
'Functional or\nNeoplastic?',
fc='#F3E5F5', ec='#4527A0')
# Functional: LEFT
ax.annotate('', xy=(3.0, 5.52), xytext=(5.5, 5.52),
arrowprops=dict(arrowstyle='->', color='#6A1B9A', lw=1.8))
ax.text(4.2, 5.68, 'FUNCTIONAL', ha='center', fontsize=8.5,
color='#6A1B9A', fontweight='bold')
# Neoplastic: RIGHT
ax.annotate('', xy=(13.5, 5.52), xytext=(10.5, 5.52),
arrowprops=dict(arrowstyle='->', color='#311B92', lw=1.8))
ax.text(12.1, 5.68, 'NEOPLASTIC', ha='center', fontsize=8.5,
color='#311B92', fontweight='bold')
# FUNCTIONAL CYSTS
mbox(ax, 3.0, 4.95, 5.5, 0.6, 'FUNCTIONAL CYSTS',
fc='#6A1B9A', ec='#6A1B9A', tc='white', fs=10, bold=True)
func_cysts = [
('Follicular Cyst', '#D1C4E9',
'<3 cm: no Ix needed\n<7 cm simple: USG in 3-6/12\n>7 cm or persistent: cystectomy'),
('Corpus Luteum Cyst', '#D1C4E9',
'hCG + vaginal USG + culdocentesis\nSpontaneous regression usually\nRupture/hematocrit >15% → surgery'),
('Theca Lutein Cyst', '#D1C4E9',
'Bilateral; associated with GTD\nResolves as hCG normalises\nCOCPs suppress ovarian activity'),
]
for j, (name, fc, detail) in enumerate(func_cysts):
y = 4.25 - j * 1.25
mbox(ax, 3.0, y, 5.3, 0.5, name, fc=fc, ec='#6A1B9A', fs=9, bold=True)
lines = detail.split('\n')
for k, line in enumerate(lines):
ax.text(0.35, y - 0.28 - k * 0.27, line, fontsize=8, color='#212121')
# NEOPLASTIC
mbox(ax, 13.0, 4.95, 5.5, 0.6, 'NEOPLASTIC',
fc='#311B92', ec='#311B92', tc='white', fs=10, bold=True)
arr(ax, 13.0, 4.65, 13.0, 4.3)
# Benign vs Malignant diamond
diamond(ax, 13.0, 3.9, 4.2, 0.75,
'Benign or\nMalignant?',
fc='#E8EAF6', ec='#311B92')
# Benign: left
ax.annotate('', xy=(10.3, 3.9), xytext=(10.9, 3.9),
arrowprops=dict(arrowstyle='->', color='#4527A0', lw=1.5))
ax.text(10.6, 4.08, 'BENIGN', ha='center', fontsize=7.5,
color='#4527A0', fontweight='bold')
mbox(ax, 10.3, 3.1, 4.0, 1.3,
'BENIGN NEOPLASMS\n▸ Serous cystadenoma (40% malignant potential)\n▸ Mucinous cystadenoma (5-10% malignant)\n▸ Dermoid cyst (mature teratoma; 1-2% malignant)\n▸ Fibroma / Thecoma\n▸ Endometrioma (chocolate cyst)\nTx: Cystectomy / Oophorectomy',
fc='#E8EAF6', ec='#4527A0', fs=7.5)
# Malignant: right (down from diamond)
ax.annotate('', xy=(13.0, 2.75), xytext=(13.0, 3.52),
arrowprops=dict(arrowstyle='->', color='#B71C1C', lw=1.5))
ax.text(13.2, 3.18, 'MALIG.', fontsize=7.5, color='#B71C1C', fontweight='bold')
mbox(ax, 13.0, 2.1, 5.5, 1.0,
'MALIGNANT OVARIAN TUMOURS\nEpithelial: Serous / Mucinous / Endometrioid / Clear cell Ca\nGerm cell: Dysgerminoma · Immature teratoma · Yolk sac\nSex cord-stromal: Granulosa cell · Sertoli-Leydig\nMetastatic: Krukenberg tumour (from GIT primary)\nRMI / IOTA criteria · CT staging (FIGO)\nTx: TAH + BSO + Omentectomy ± Carboplatin/Paclitaxel',
fc='#FFEBEE', ec='#B71C1C', fs=7.5)
ax.text(8, 0.25,
'FIGO Staging: Stage I – confined to ovaries · Stage II – pelvic extension · Stage III – peritoneal metastasis / retro nodes · Stage IV – distant metastasis',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# SLIDE 9 FIGURE: Management Summary
# ══════════════════════════════════════════════════════════════════
def make_management_fig():
fig, ax = plt.subplots(figsize=(16, 9))
ax.set_xlim(0, 16); ax.set_ylim(0, 9); ax.axis('off')
fig.patch.set_facecolor('#E8F5E9')
ax.text(8, 8.6, 'Management Principles by Diagnosis',
ha='center', fontsize=16, fontweight='bold', color='#1B5E20')
ax.plot([0.5, 15.5], [8.3, 8.3], color='#2E7D32', lw=2)
mgmt = [
('FIBROMYOMA', '#C62828', '#FFEBEE',
'CONSERVATIVE\n▸ Observation (small, asymptomatic)\n▸ GnRH agonists (preoperative)\n▸ Levonorgestrel IUS (Mirena)\nSURGICAL\n▸ Myomectomy (fertility-sparing)\n▸ Hysterectomy (definitive)\n▸ UAE (uterine artery embolisation)'),
('ADENOMYOSIS', '#AD1457', '#FCE4EC',
'MEDICAL\n▸ NSAIDs → dysmenorrhoea\n▸ GnRH agonists\n▸ Mirena LNG-IUS (menorrhagia/pain)\n▸ Danazol\nSURGICAL\n▸ Total hysterectomy (definitive)\n▸ Adenomyomectomy (young/fertility)\nEMERGING: MRgFUS'),
('TOA', '#E65100', '#FFF3E0',
'MEDICAL\n▸ IV broad-spectrum antibiotics\n▸ Doxycycline + Metronidazole x14d\n▸ Clindamycin alternative\nDRAINAGE\n▸ If ≥8 cm or no improvement 48-72h\n▸ Ultrasound/CT-guided (TVS/TA)\nSURGICAL\n▸ Laparoscopy/laparotomy if ruptured'),
('HYDROSALPINX', '#5E35B1', '#EDE7F6',
'EXPECTANT\n▸ Asymptomatic + family complete\nSURGICAL\n▸ Salpingectomy (treatment of choice)\n▸ Neosalpingostomy (mild disease,\n fertility desired; up to 80% PR)\n▸ IVF preferred for severe tubal\n disease\n▸ Laparoscopic adhesiolysis'),
('OVARIAN CYST\n(Benign)', '#4527A0', '#EDE7F6',
'FUNCTIONAL CYSTS\n▸ <7 cm simple: USG follow-up\n▸ Spontaneous regression expected\n▸ COCPs to suppress\nNEOPLASTIC BENIGN\n▸ Ovarian cystectomy (young)\n▸ Unilateral oophorectomy (large)\n▸ BSO (postmenopausal)\n▸ Emergency surgery if torsion'),
('OVARIAN Ca\n(Malignant)', '#B71C1C', '#FFEBEE',
'SURGERY\n▸ TAH + BSO + Omentectomy\n▸ Peritoneal washings\n▸ Fertility-sparing (Stage Ia, young)\nCHEMOTHERAPY\n▸ Carboplatin + Paclitaxel\n▸ BEP (germ cell tumours)\nTARGETED\n▸ Bevacizumab; PARP inhibitors\nRADIOTHERAPY: dysgerminoma / palliation'),
]
cols = 3
for i, (hdr, ec, fc, body) in enumerate(mgmt):
col = i % cols
row = i // cols
x0 = 0.2 + col * 5.25
y0 = 7.65 - row * 3.7
w = 5.0
mbox(ax, x0 + w/2, y0, w, 0.6, hdr,
fc=ec, ec=ec, tc='white', fs=10, bold=True)
rect = FancyBboxPatch((x0, y0 - 2.85), w, 2.75,
boxstyle="round,pad=0.1",
facecolor=fc, edgecolor=ec, linewidth=1.2, zorder=2)
ax.add_patch(rect)
lines = body.split('\n')
for j, line in enumerate(lines):
ax.text(x0 + 0.15, y0 - 0.25 - j * 0.33, line,
fontsize=8, color='#212121', zorder=4)
ax.text(8, 0.22,
'NON-GYNAE: Bladder → Catheterise + Urology · Appendicular mass → Ochsner-Sherren + interval appendectomy · Bowel Ca → Resection · TB mass → ATT',
ha='center', fontsize=7.5, color='#37474F', style='italic')
return fig
# ══════════════════════════════════════════════════════════════════
# BUILD PPTX
# ══════════════════════════════════════════════════════════════════
def add_image_slide(prs, fig_fn, slide_title):
"""Render a matplotlib figure, embed in a blank slide."""
fig = fig_fn()
img_stream = fig_to_stream(fig)
plt.close(fig)
blank = prs.slide_layouts[6]
slide = prs.slides.add_slide(blank)
# Slide background
background = slide.background
fill = background.fill
fill.solid()
fill.fore_color.rgb = rgb('#FFFFFF')
# Full-slide image
slide.shapes.add_picture(
img_stream,
left=Inches(0), top=Inches(0),
width=Inches(13.333), height=Inches(7.5)
)
return slide
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
slides_info = [
(make_title_fig, 'Title'),
(make_history_fig, 'History & Examination'),
(make_investigations_fig, 'Investigations'),
(make_main_flow_fig, 'Main Decision Flow'),
(make_uterine_fig, 'Uterine Causes'),
(make_adnexal_fig, 'Adnexal Causes'),
(make_nongynae_fig, 'Non-Gynaecological Causes'),
(make_ovarian_fig, 'Ovarian Mass Algorithm'),
(make_management_fig, 'Management Summary'),
]
print("Building slides...")
for fn, title in slides_info:
print(f" → {title}")
add_image_slide(prs, fn, title)
out_path = '/tmp/workspace/abdominal-mass-flowchart/Diagnostic_Algorithm_Abdominal_Mass_Women.pptx'
prs.save(out_path)
print(f"\nSaved: {out_path}")
cd /tmp/workspace/abdominal-mass-flowchart && python3 build_pptx.py 2>&1
ls -lh /tmp/workspace/abdominal-mass-flowchart/Diagnostic_Algorithm_Abdominal_Mass_Women.pptx
Diagnostic Algorithm Abdominal Mass Women
Presentation · PPTX
| Slide | Content |
|---|---|
| 1 | Title / Overview - Colour-coded topic index for all 6 sections |
| 2 | History & Examination + Bedside Clues - Full history checklist, exam findings, Fothergill's sign, cervical traction tests |
| 3 | Investigations - 4-panel layout: Imaging, Tumour Markers, Special Procedures (laparoscopy/hysteroscopy/HSG), Labs |
| 4 | Main Decision Flow - Flowchart: Start → USG → Gynaecological vs Non-Gynaecological → Uterine vs Adnexal with arrows to detail slides |
| 5 | Uterine Causes - Side-by-side: Fibromyoma / Adenomyosis / Pyometra (clinical features + investigations each) |
| 6 | Adnexal Causes - Hydrosalpinx / TOA / Ovarian Mass (beads-on-a-string, CMT, TVS features) |
| 7 | Non-Gynaecological Causes - Urological / GI / Retroperitoneal-Wall causes with key differentiating signs |
| 8 | Ovarian Mass Sub-Algorithm - Functional (follicular/CL/theca lutein) vs Neoplastic → Benign vs Malignant → FIGO staging |
| 9 | Management Summary - 6-box grid covering all diagnoses (medical, surgical, emerging therapies) |