Paraneoplastic syndrome
"paraneoplastic syndrome" AND diagnosis

| Syndrome | Key Features | Associated Tumor | Antibody |
|---|---|---|---|
| Lambert-Eaton Myasthenic Syndrome (LEMS) | Proximal lower limb weakness, strength improves with sustained contraction; autonomic features | SCLC (>50%) | Anti-VGCC, anti-SOX1 |
| Paraneoplastic Encephalomyelitis / Subacute Sensory Neuropathy | Asymmetric numbness, burning paresthesias, sensory ataxia, loss of proprioception/vibration | SCLC | Anti-Hu (ANNA-1) |
| Paraneoplastic Cerebellar Degeneration (PCD) | Abrupt onset dysarthria, ataxia, oculomotor dysfunction; Purkinje cell loss; cerebellar atrophy on MRI | Breast, ovarian, SCLC, Hodgkin lymphoma | Anti-Yo (PCA-1), anti-Hu, anti-Ri, anti-Tr |
| Limbic Encephalitis | Subacute amnesia, affective disorders, seizures; mesial temporal FLAIR signal on MRI | SCLC, testicular (NMDAR), thymoma | Anti-Hu, anti-Ma2, anti-LGI1, anti-CASPR2, anti-AMPAR |
| Stiff-Person Syndrome | Progressive muscle stiffness, rigidity, painful spasms | Breast cancer | Anti-GAD65, anti-amphiphysin |
| Paraneoplastic Opsoclonus-Myoclonus | Chaotic multidirectional eye movements + myoclonus | Breast cancer (adults), neuroblastoma (children) | Anti-Ri |
| Paraneoplastic Neuropathy | Sensorimotor neuropathy, demyelinating variants | Multiple | Anti-CRMP5 (CV2), anti-Hu |
| Dermatomyositis/Polymyositis | Proximal muscle weakness, rash (DM) | Lung, ovarian, colorectal, bladder | Anti-Jo-1, anti-Mi-2 |

| Syndrome | Mechanism | Associated Tumor |
|---|---|---|
| Humoral hypercalcemia of malignancy | PTHrP secretion | Squamous cell lung, breast, renal cell, bladder |
| SIADH / Hyponatremia | Ectopic ADH | SCLC, CNS tumors, head and neck cancers |
| Cushing Syndrome | Ectopic ACTH | SCLC, pancreatic NET, bronchial carcinoid, medullary thyroid cancer |
| Hypoglycemia | Ectopic IGF-2 | Mesenchymal tumors, hepatocellular carcinoma |
| Acromegaly | Ectopic GHRH | Pancreatic islet, carcinoid tumors |
| Carcinoid Syndrome | Serotonin, bradykinin | GI carcinoids with liver mets, bronchial carcinoid |
| VIPoma (Watery diarrhea) | Ectopic VIP | Pancreatic islet cell tumors |
| Skin Finding | Associated Malignancy |
|---|---|
| Acanthosis nigricans | GI adenocarcinoma (especially gastric) |
| Dermatomyositis | Ovarian, lung, GI cancers |
| Bazex syndrome (acrokeratosis paraneoplastica) | Head and neck squamous cell carcinoma |
| Erythema gyratum repens | Lung, breast, esophageal cancers |
| Necrolytic migratory erythema | Glucagonoma |
| Sweet syndrome | AML, myelodysplastic syndrome |
| Paraneoplastic pemphigus | NHL, CLL, thymoma, Castleman disease |
| Leser-Trelat sign (sudden eruption of seborrheic keratoses) | GI adenocarcinomas |
Create a diagnostic flowchart for suspected paraneoplastic syndrome
mkdir -p /home/daytona/workspace/paraneoplastic-flowchart && ls /home/daytona/workspace/paraneoplastic-flowchart/
~/paraneoplastic-flowchart/build_flowchart.py
"""
Diagnostic Flowchart for Suspected Paraneoplastic Syndrome
Generated with ReportLab - professional A3 landscape PDF
"""
from reportlab.lib.pagesizes import A3, landscape
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
import math
# ── Colour palette ──────────────────────────────────────────────────────────
C_TITLE_BG = HexColor("#1A3A5C") # deep navy
C_TITLE_FG = HexColor("#FFFFFF")
C_START = HexColor("#2E86AB") # teal (start/end ovals)
C_PROCESS = HexColor("#1A3A5C") # navy (process rectangles)
C_DECISION = HexColor("#F18F01") # amber (diamond)
C_ACTION = HexColor("#A7C4BC") # sage green (action boxes)
C_ALARM = HexColor("#C0392B") # red (urgent)
C_OUTCOME_POS = HexColor("#27AE60") # green (positive outcomes)
C_OUTCOME_NEU = HexColor("#7F8C8D") # grey (neutral)
C_BOX_TEXT = HexColor("#FFFFFF")
C_EDGE = HexColor("#444444")
C_ARROW = HexColor("#333333")
C_FOOTNOTE = HexColor("#555555")
C_SUBBOX_BG = HexColor("#EAF2F8")
C_SUBBOX_BOR = HexColor("#2E86AB")
PAGE_W, PAGE_H = landscape(A3) # 420 × 297 mm (1190 × 842 pt)
# ── Helper: wrapped text in a box ────────────────────────────────────────────
def wrap_text(c, text, x, y, max_width, font_name, font_size, line_height, align="center", color=colors.black):
c.setFont(font_name, font_size)
c.setFillColor(color)
words = text.split()
lines = []
current = ""
for w in words:
test = (current + " " + w).strip()
if c.stringWidth(test, font_name, font_size) <= max_width:
current = test
else:
if current:
lines.append(current)
current = w
if current:
lines.append(current)
total_h = len(lines) * line_height
start_y = y + total_h / 2 - line_height * 0.8
for i, line in enumerate(lines):
ly = start_y - i * line_height
if align == "center":
lx = x - c.stringWidth(line, font_name, font_size) / 2
elif align == "left":
lx = x
else:
lx = x - c.stringWidth(line, font_name, font_size)
c.drawString(lx, ly, line)
def draw_rect_box(c, cx, cy, w, h, fill, stroke, text, font="Helvetica-Bold",
fsize=8.5, text_color=C_BOX_TEXT, radius=4, line_height=11):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(1.2)
c.roundRect(cx - w/2, cy - h/2, w, h, radius, fill=1, stroke=1)
wrap_text(c, text, cx, cy, w - 8, font, fsize, line_height, color=text_color)
def draw_diamond(c, cx, cy, w, h, fill, stroke, text, fsize=8, text_color=C_BOX_TEXT):
"""Draw a diamond (decision) shape."""
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(1.2)
path = c.beginPath()
path.moveTo(cx, cy + h/2)
path.lineTo(cx + w/2, cy)
path.lineTo(cx, cy - h/2)
path.lineTo(cx - w/2, cy)
path.close()
c.drawPath(path, fill=1, stroke=1)
wrap_text(c, text, cx, cy, w - 12, "Helvetica-Bold", fsize, 10, color=text_color)
def draw_oval(c, cx, cy, w, h, fill, stroke, text, fsize=9, text_color=C_BOX_TEXT):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(1.5)
c.ellipse(cx - w/2, cy - h/2, cx + w/2, cy + h/2, fill=1, stroke=1)
wrap_text(c, text, cx, cy, w - 10, "Helvetica-Bold", fsize, 11, color=text_color)
def arrow(c, x1, y1, x2, y2, label="", label_side="right", color=C_ARROW, label_color=HexColor("#C0392B")):
"""Draw arrow from (x1,y1) to (x2,y2) with optional label."""
c.setStrokeColor(color)
c.setFillColor(color)
c.setLineWidth(1.3)
c.line(x1, y1, x2, y2)
# arrowhead
angle = math.atan2(y2 - y1, x2 - x1)
size = 6
ax1 = x2 - size * math.cos(angle - math.pi/7)
ay1 = y2 - size * math.sin(angle - math.pi/7)
ax2 = x2 - size * math.cos(angle + math.pi/7)
ay2 = y2 - size * math.sin(angle + math.pi/7)
c.setFillColor(color)
path = c.beginPath()
path.moveTo(x2, y2)
path.lineTo(ax1, ay1)
path.lineTo(ax2, ay2)
path.close()
c.drawPath(path, fill=1, stroke=0)
if label:
c.setFillColor(label_color)
c.setFont("Helvetica-Bold", 7.5)
mx, my = (x1+x2)/2, (y1+y2)/2
if label_side == "right":
c.drawString(mx + 3, my, label)
elif label_side == "left":
c.drawString(mx - c.stringWidth(label, "Helvetica-Bold", 7.5) - 3, my, label)
elif label_side == "top":
c.drawCentredString(mx, my + 4, label)
elif label_side == "bottom":
c.drawCentredString(mx, my - 10, label)
def elbow_arrow(c, x1, y1, x2, y2, mid_x=None, mid_y=None, label="", color=C_ARROW, label_color=HexColor("#C0392B")):
"""L-shaped connector: vertical then horizontal (or custom midpoint)."""
c.setStrokeColor(color)
c.setLineWidth(1.3)
if mid_x is not None and mid_y is None:
# horizontal then vertical
c.line(x1, y1, mid_x, y1)
c.line(mid_x, y1, mid_x, y2)
c.line(mid_x, y2, x2, y2)
end_x, end_y = x2, y2
pre_x, pre_y = mid_x, y2
elif mid_y is not None and mid_x is None:
c.line(x1, y1, x1, mid_y)
c.line(x1, mid_y, x2, mid_y)
c.line(x2, mid_y, x2, y2)
end_x, end_y = x2, y2
pre_x, pre_y = x2, mid_y
else:
c.line(x1, y1, x2, y2)
end_x, end_y = x2, y2
pre_x, pre_y = x1, y1
# arrowhead at end
angle = math.atan2(end_y - pre_y, end_x - pre_x)
size = 6
ax1 = end_x - size * math.cos(angle - math.pi/7)
ay1 = end_y - size * math.sin(angle - math.pi/7)
ax2 = end_x - size * math.cos(angle + math.pi/7)
ay2 = end_y - size * math.sin(angle + math.pi/7)
c.setFillColor(color)
path = c.beginPath()
path.moveTo(end_x, end_y)
path.lineTo(ax1, ay1)
path.lineTo(ax2, ay2)
path.close()
c.drawPath(path, fill=1, stroke=0)
if label:
c.setFillColor(label_color)
c.setFont("Helvetica-Bold", 7.5)
c.drawString((x1+end_x)/2 + 3, (y1+end_y)/2, label)
# ════════════════════════════════════════════════════════════════════════════
# MAIN BUILD
# ════════════════════════════════════════════════════════════════════════════
out_path = "/home/daytona/workspace/paraneoplastic-flowchart/paraneoplastic_diagnostic_flowchart.pdf"
c = canvas.Canvas(out_path, pagesize=landscape(A3))
c.setTitle("Diagnostic Flowchart: Suspected Paraneoplastic Syndrome")
c.setAuthor("Orris Medical AI")
c.setSubject("Paraneoplastic Syndrome - Clinical Diagnostic Algorithm")
W, H = PAGE_W, PAGE_H # 841.9 x 595.3 pt
# ─── TITLE BANNER ────────────────────────────────────────────────────────────
c.setFillColor(C_TITLE_BG)
c.rect(0, H - 42, W, 42, fill=1, stroke=0)
c.setFillColor(C_TITLE_FG)
c.setFont("Helvetica-Bold", 15)
c.drawCentredString(W/2, H - 16, "Diagnostic Algorithm: Suspected Paraneoplastic Syndrome")
c.setFont("Helvetica", 8.5)
c.setFillColor(HexColor("#A8C8E8"))
c.drawCentredString(W/2, H - 30, "Based on Harrison's Principles of Internal Medicine 22E (2025) & Goldman-Cecil Medicine | Orris Medical AI")
# ─── FOOTNOTE ─────────────────────────────────────────────────────────────
c.setFillColor(C_TITLE_BG)
c.rect(0, 0, W, 18, fill=1, stroke=0)
c.setFillColor(HexColor("#A8C8E8"))
c.setFont("Helvetica", 7)
c.drawCentredString(W/2, 6, "PNS = Paraneoplastic Syndrome | SCLC = Small Cell Lung Cancer | PET = Positron Emission Tomography | CSF = Cerebrospinal Fluid | IVIg = Intravenous Immunoglobulin | LEMS = Lambert-Eaton Myasthenic Syndrome")
# ════════════════════════════════════════════════════════════════════════════
# LAYOUT — two-column flow left-to-right, top-to-bottom
# Usable area: x: 20..W-20, y: 22..H-48
# ════════════════════════════════════════════════════════════════════════════
# Column centres
LX = 148 # left column centre
MX = W/2 # middle column centre (~421)
RX = W - 148 # right column centre
# Vertical positions (top → bottom)
Y1 = H - 68 # START oval
Y2 = H - 110 # Step 1: Initial Presentation
Y3 = H - 155 # Step 2: Exclude common causes
Y4 = H - 200 # Decision: Cause found?
Y5a = H - 248 # YES branch → Treat accordingly
Y5b = H - 248 # NO branch → Consider PNS
Y6 = H - 295 # Classify dominant syndrome
Y7 = H - 345 # two sub-boxes: Neurologic vs Non-neurologic
Y8 = H - 415 # Antibody testing / Tumour screen
Y9 = H - 475 # Decision: Antibody +? / Tumour found?
Y10 = H - 525 # Confirm & treat
Y11 = H - 565 # Monitor
BOX_W = 200
BOX_H = 36
DIA_W = 140
DIA_H = 44
# ─── START ───────────────────────────────────────────────────────────────────
draw_oval(c, MX, Y1, 200, 28, C_START, C_TITLE_BG, "PATIENT PRESENTS WITH UNEXPLAINED SYMPTOMS", fsize=8.5)
# ─── STEP 1: Initial assessment ──────────────────────────────────────────────
arrow(c, MX, Y1 - 14, MX, Y2 + BOX_H/2)
draw_rect_box(c, MX, Y2, 340, BOX_H, C_PROCESS, C_TITLE_BG,
"STEP 1 — Initial Assessment History · Examination · Basic labs (CBC, CMP, LFTs, TFTs) · Imaging (CXR, CT) · Urinalysis", fsize=7.8)
# ─── STEP 2: Exclude common causes ───────────────────────────────────────────
arrow(c, MX, Y2 - BOX_H/2, MX, Y3 + BOX_H/2)
draw_rect_box(c, MX, Y3, 340, BOX_H, C_PROCESS, C_TITLE_BG,
"STEP 2 — Exclude Common Causes Infection · Metastatic disease · Drug toxicity · Metabolic derangement · Organ failure", fsize=7.8)
# ─── DECISION: cause found? ──────────────────────────────────────────────────
arrow(c, MX, Y3 - BOX_H/2, MX, Y4 + DIA_H/2)
draw_diamond(c, MX, Y4, DIA_W + 60, DIA_H, C_DECISION, C_TITLE_BG,
"Cause Identified?", fsize=9)
# YES → treat accordingly (left)
elbow_arrow(c, MX - (DIA_W+60)/2, Y4, LX + BOX_W/2, Y5a + BOX_H/2,
mid_y=Y5a + BOX_H/2, label="YES", label_color=C_OUTCOME_POS)
draw_rect_box(c, LX, Y5a, BOX_W, BOX_H, C_OUTCOME_POS, HexColor("#1E8449"),
"Treat Underlying Cause — Continue monitoring for cancer", fsize=8)
# NO → consider PNS (right)
elbow_arrow(c, MX + (DIA_W+60)/2, Y4, RX - BOX_W/2, Y5b + BOX_H/2,
mid_y=Y5b + BOX_H/2, label="NO", label_color=C_ALARM)
draw_rect_box(c, RX, Y5b, BOX_W, BOX_H, C_ALARM, HexColor("#922B21"),
"CONSIDER Paraneoplastic Syndrome", fsize=8.5)
# ─── STEP 3: Classify dominant syndrome ──────────────────────────────────────
# Arrow from NO box down to classification
arrow(c, RX, Y5b - BOX_H/2, RX, Y6 + BOX_H/2)
# Also bring YES branch back via elbow (dashed-ish)
# Connect both to classify via elbow
elbow_arrow(c, LX, Y5a - BOX_H/2, MX, Y6 + BOX_H/2, mid_y=Y6 + BOX_H/2 + 8, label="Also screen", label_color=C_OUTCOME_NEU)
elbow_arrow(c, RX, Y6 + BOX_H/2, MX + 30, Y6 + BOX_H/2, mid_y=None, mid_x=MX + 30)
draw_rect_box(c, MX, Y6, 340, BOX_H, C_PROCESS, C_TITLE_BG,
"STEP 3 — Classify Dominant Syndrome Neurologic · Endocrine/Metabolic · Hematologic · Dermatologic · Rheumatologic · Renal/Hepatic",
fsize=7.6)
# ─── TWO BRANCHES: Neurologic vs Non-neurologic ──────────────────────────────
# Split arrows
arrow(c, MX - 50, Y6 - BOX_H/2, LX, Y7 + 48)
arrow(c, MX + 50, Y6 - BOX_H/2, RX, Y7 + 44)
# NEUROLOGIC BOX (left)
NL_W, NL_H = 230, 88
c.setFillColor(C_SUBBOX_BG)
c.setStrokeColor(C_SUBBOX_BOR)
c.setLineWidth(1.2)
c.roundRect(LX - NL_W/2, Y7 - NL_H/2, NL_W, NL_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(LX, Y7 + NL_H/2 - 10, "NEUROLOGIC PNS")
c.setFillColor(HexColor("#1A3A5C"))
c.setFont("Helvetica", 7.2)
neuro_lines = [
"• Limbic encephalitis (memory, seizures, mood)",
"• Cerebellar degeneration (ataxia, dysarthria)",
"• Sensory neuropathy (numbness, sensory ataxia)",
"• LEMS (proximal weakness ↑ with effort)",
"• Opsoclonus-myoclonus",
"• Stiff-person syndrome",
"• Anti-NMDAR encephalitis"
]
for i, line in enumerate(neuro_lines):
c.drawString(LX - NL_W/2 + 6, Y7 + NL_H/2 - 24 - i * 9.5, line)
# NON-NEUROLOGIC BOX (right)
NN_W, NN_H = 230, 88
c.setFillColor(C_SUBBOX_BG)
c.setStrokeColor(C_SUBBOX_BOR)
c.setLineWidth(1.2)
c.roundRect(RX - NN_W/2, Y7 - NN_H/2, NN_W, NN_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(RX, Y7 + NN_H/2 - 10, "NON-NEUROLOGIC PNS")
c.setFillColor(HexColor("#1A3A5C"))
c.setFont("Helvetica", 7.2)
nonnl_lines = [
"• Endocrine: SIADH, ectopic ACTH, hypercalcemia",
"• Hematologic: Trousseau, AIHA, DIC, polycythemia",
"• Dermatologic: Acanthosis nigricans, DM, PNP",
"• Rheumatologic: Hypertrophic osteoarthropathy",
"• Renal: Membranous nephropathy, MCD",
"• Hepatic: Stauffer syndrome (RCC)"
]
for i, line in enumerate(nonnl_lines):
c.drawString(RX - NN_W/2 + 6, RX - NN_W/2 + 6, "") # reset
c.drawString(RX - NN_W/2 + 6, Y7 + NN_H/2 - 24 - i * 9.8, line)
# ─── STEP 4: INVESTIGATIONS ───────────────────────────────────────────────────
arrow(c, LX, Y7 - NL_H/2, LX, Y8 + 58)
arrow(c, RX, Y7 - NN_H/2, RX, Y8 + 44)
# Neurologic investigation box
NI_W, NI_H = 230, 108
c.setFillColor(HexColor("#EBF5FB"))
c.setStrokeColor(C_PROCESS)
c.setLineWidth(1.2)
c.roundRect(LX - NI_W/2, Y8 - NI_H/2, NI_W, NI_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(LX, Y8 + NI_H/2 - 10, "NEUROLOGIC WORKUP")
c.setFont("Helvetica", 7.2)
ni_lines = [
"1. MRI brain (FLAIR: mesial temporal, cortical)",
"2. CSF: cells, protein, oligoclonal bands",
"3. Antibody panel (serum + CSF):",
" Anti-Hu, Yo, Ri, Ma2, CV2, amphiphysin",
" Anti-NMDAR, LGI1, CASPR2, GABA-B",
" Anti-AMPAR, VGCC (LEMS)",
"4. PET-CT / CT chest-abdomen-pelvis",
"5. Testicular / ovarian ultrasound if indicated",
]
for i, line in enumerate(ni_lines):
c.drawString(LX - NI_W/2 + 6, Y8 + NI_H/2 - 24 - i * 10.5, line)
# Non-neuro investigation box
RI_W, RI_H = 230, 108
c.setFillColor(HexColor("#EBF5FB"))
c.setStrokeColor(C_PROCESS)
c.setLineWidth(1.2)
c.roundRect(RX - RI_W/2, Y8 - RI_H/2, RI_W, RI_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(RX, Y8 + RI_H/2 - 10, "NON-NEUROLOGIC WORKUP")
c.setFont("Helvetica", 7.2)
ri_lines = [
"1. Directed labs per syndrome:",
" PTHrP, cortisol, ACTH, serum/urine osmol",
" CBC diff, coags, LDH, EPO, haptoglobin",
"2. Hormone levels: ACTH, ADH, GH, IGF-2",
"3. Skin biopsy (if dermatologic manifestation)",
"4. Renal biopsy (if nephrotic syndrome)",
"5. CT chest/abdomen/pelvis + PET-CT",
"6. Mammogram, PSA, tumour markers as indicated"
]
for i, line in enumerate(ri_lines):
c.drawString(RX - RI_W/2 + 6, Y8 + RI_H/2 - 24 - i * 10.5, line)
# ─── DECISION DIAMONDS ────────────────────────────────────────────────────────
arrow(c, LX, Y8 - NI_H/2, LX, Y9 + DIA_H/2)
arrow(c, RX, Y8 - RI_H/2, RX, Y9 + DIA_H/2)
draw_diamond(c, LX, Y9, 170, DIA_H, C_DECISION, C_TITLE_BG, "Antibody\nPositive?", fsize=8.5)
draw_diamond(c, RX, Y9, 170, DIA_H, C_DECISION, C_TITLE_BG, "Tumour\nIdentified?", fsize=8.5)
# ─── OUTCOMES FROM DECISIONS ──────────────────────────────────────────────────
# Neuro YES (antibody positive)
arrow(c, LX, Y9 - DIA_H/2, LX, Y10 + BOX_H/2, label="YES →", label_side="right")
draw_rect_box(c, LX, Y10, 190, BOX_H+4, C_OUTCOME_POS, HexColor("#1E8449"),
"Confirmed PNS — Start Immunotherapy + Tumour Treatment", fsize=7.5)
# Neuro NO (antibody negative) — elbow to middle
NEURO_NO_X = LX - 95
elbow_arrow(c, LX - 170/2, Y9, NEURO_NO_X, Y10 + (BOX_H+4)/2, mid_y=Y10 + (BOX_H+4)/2, label="NO", label_color=C_ALARM)
draw_rect_box(c, NEURO_NO_X - 10, Y10, 145, BOX_H+4, C_OUTCOME_NEU, HexColor("#566573"),
"Probable PNS if Cancer Found — Re-screen in 3–6 months", fsize=7.2, text_color=C_BOX_TEXT)
# Non-neuro YES (tumour found)
arrow(c, RX, Y9 - DIA_H/2, RX, Y10 + BOX_H/2, label="YES →", label_side="right")
draw_rect_box(c, RX, Y10, 200, BOX_H+4, C_OUTCOME_POS, HexColor("#1E8449"),
"Treat Primary Tumour — Syndrome resolves with tumour control", fsize=7.5)
# Non-neuro NO — elbow
NN_NO_X = RX + 100
elbow_arrow(c, RX + 170/2, Y9, NN_NO_X + 10, Y10 + (BOX_H+4)/2, mid_y=Y10 + (BOX_H+4)/2, label="NO", label_color=C_ALARM)
draw_rect_box(c, NN_NO_X + 10, Y10, 150, BOX_H+4, C_OUTCOME_NEU, HexColor("#566573"),
"Symptomatic Rx — Repeat screen q6mo × 2 yrs", fsize=7.5, text_color=C_BOX_TEXT)
# ─── TREATMENT BOXES ──────────────────────────────────────────────────────────
arrow(c, LX, Y10 - (BOX_H+4)/2, LX, Y11 + BOX_H/2)
arrow(c, RX, Y10 - (BOX_H+4)/2, RX, Y11 + BOX_H/2)
TX_W, TX_H = 225, 64
# Neuro treatment
c.setFillColor(HexColor("#EBF5FB"))
c.setStrokeColor(C_PROCESS)
c.setLineWidth(1.2)
c.roundRect(LX - TX_W/2, Y11 - TX_H/2, TX_W, TX_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8)
c.drawCentredString(LX, Y11 + TX_H/2 - 9, "NEUROLOGIC PNS TREATMENT")
c.setFont("Helvetica", 7)
tx_neuro = [
"1st Line: Steroids + IVIg + Plasma Exchange",
"2nd Line: Rituximab or Cyclophosphamide",
"Intracellular Ab (Hu/Yo): tumour Rx priority",
"Surface Ab (NMDAR/LGI1): better immunotherapy response",
"LEMS: 3,4-DAP + Pyridostigmine"
]
for i, line in enumerate(tx_neuro):
c.drawString(LX - TX_W/2 + 6, Y11 + TX_H/2 - 22 - i * 9, line)
# Non-neuro treatment
c.setFillColor(HexColor("#EBF5FB"))
c.setStrokeColor(C_PROCESS)
c.setLineWidth(1.2)
c.roundRect(RX - TX_W/2, Y11 - TX_H/2, TX_W, TX_H, 5, fill=1, stroke=1)
c.setFillColor(C_PROCESS)
c.setFont("Helvetica-Bold", 8)
c.drawCentredString(RX, Y11 + TX_H/2 - 9, "NON-NEUROLOGIC PNS TREATMENT")
c.setFont("Helvetica", 7)
tx_nonnl = [
"Endocrine: bisphosphonates (Ca²⁺), octreotide (carcinoid)",
"Hematologic: anticoagulate Trousseau; transfuse AIHA",
"Dermatologic: immunosuppression (DM/PM steroids)",
"Renal: treat tumour; steroids for MCD",
"All: treat primary cancer — syndromes follow tumour course"
]
for i, line in enumerate(tx_nonnl):
c.drawString(RX - TX_W/2 + 6, Y11 + TX_H/2 - 22 - i * 9, line)
# ─── KEY ANTIBODY LEGEND ─────────────────────────────────────────────────────
# Middle column legend box
LEG_X = MX
LEG_Y = Y7
LEG_W = 130
LEG_H = 165
c.setFillColor(HexColor("#FDFEFE"))
c.setStrokeColor(C_TITLE_BG)
c.setLineWidth(1.2)
c.roundRect(LEG_X - LEG_W/2, LEG_Y - LEG_H/2, LEG_W, LEG_H, 5, fill=1, stroke=1)
c.setFillColor(C_TITLE_BG)
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(LEG_X, LEG_Y + LEG_H/2 - 9, "KEY ANTIBODIES")
c.setFont("Helvetica-Bold", 6.5)
c.setFillColor(C_ALARM)
c.drawString(LEG_X - LEG_W/2 + 5, LEG_Y + LEG_H/2 - 19, "High-risk (intracellular):")
c.setFont("Helvetica", 6.5)
c.setFillColor(colors.black)
ab_lines_hi = [
"Anti-Hu → SCLC, sensory neuropathy",
"Anti-Yo → Breast/ovarian, PCD",
"Anti-Ri → Breast, opsoclonus",
"Anti-Ma2 → Testicular, limbic",
"Anti-CV2 → SCLC, encephalomyelitis",
]
for i, line in enumerate(ab_lines_hi):
c.drawString(LEG_X - LEG_W/2 + 5, LEG_Y + LEG_H/2 - 30 - i * 8.5, line)
c.setFont("Helvetica-Bold", 6.5)
c.setFillColor(C_OUTCOME_POS)
c.drawString(LEG_X - LEG_W/2 + 5, LEG_Y + LEG_H/2 - 30 - 5*8.5 - 3, "Surface antigen (better Rx):")
c.setFont("Helvetica", 6.5)
c.setFillColor(colors.black)
ab_lines_lo = [
"Anti-NMDAR → Ovarian teratoma",
"Anti-LGI1 → Thymoma / SCLC",
"Anti-CASPR2→ Thymoma",
"Anti-GABA-B→ SCLC, seizures",
"Anti-VGCC → SCLC (LEMS)",
]
for i, line in enumerate(ab_lines_lo):
c.drawString(LEG_X - LEG_W/2 + 5, LEG_Y + LEG_H/2 - 30 - (5+1)*8.5 - 3 - i * 8.5, line)
# ─── MONITORING OVAL (bottom centre) ─────────────────────────────────────────
Y_MON = Y11 - TX_H/2 - 22
draw_oval(c, MX, Y_MON, 260, 22, C_START, C_TITLE_BG,
"ONGOING MONITORING — Reassess every 3–6 months; PNS recurrence = tumour relapse signal", fsize=7.5)
# Connect treatment boxes to monitor oval
elbow_arrow(c, LX, Y11 - TX_H/2, LX, Y_MON + 11, mid_y=Y_MON + 11)
elbow_arrow(c, RX, Y11 - TX_H/2, RX, Y_MON + 11, mid_y=Y_MON + 11)
elbow_arrow(c, LX, Y_MON, MX - 130, Y_MON, mid_x=None, mid_y=None)
elbow_arrow(c, RX, Y_MON, MX + 130, Y_MON, mid_x=None, mid_y=None)
c.save()
print(f"PDF saved to: {out_path}")
cd /home/daytona/workspace/paraneoplastic-flowchart && python build_flowchart.py 2>&1
ls -lh /home/daytona/workspace/paraneoplastic-flowchart/paraneoplastic_diagnostic_flowchart.pdf && python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/paraneoplastic-flowchart/paraneoplastic_diagnostic_flowchart.pdf') print(f'Pages: {len(r.pages)}') p = r.pages[0] print(f'Page size: {p.mediabox.width:.0f} x {p.mediabox.height:.0f} pts') print(f'Title: {r.metadata.title}') "
cd /home/daytona/workspace/paraneoplastic-flowchart && convert -density 120 paraneoplastic_diagnostic_flowchart.pdf -flatten preview.png 2>&1 && ls -lh preview.png
cd /home/daytona/workspace/paraneoplastic-flowchart && python3 -c " import fitz # pymupdf doc = fitz.open('paraneoplastic_diagnostic_flowchart.pdf') page = doc[0] mat = fitz.Matrix(1.5, 1.5) pix = page.get_pixmap(matrix=mat) pix.save('preview.png') print(f'Preview: {pix.width}x{pix.height}') " 2>&1 && ls -lh preview.png
cd /home/daytona/workspace/paraneoplastic-flowchart && pdftoppm -r 120 -png paraneoplastic_diagnostic_flowchart.pdf preview && ls -lh preview*.png 2>&1
~/paraneoplastic-flowchart/build_flowchart_v2.py
"""
Diagnostic Flowchart for Suspected Paraneoplastic Syndrome - v2
Clean, well-spaced A3 landscape layout
"""
from reportlab.lib.pagesizes import A3, landscape
from reportlab.lib import colors
from reportlab.lib.colors import HexColor
from reportlab.pdfgen import canvas
import math
# ── Colours ───────────────────────────────────────────────────────────────
C_NAVY = HexColor("#1A3A5C")
C_TEAL = HexColor("#2E86AB")
C_AMBER = HexColor("#E67E22")
C_RED = HexColor("#C0392B")
C_GREEN = HexColor("#27AE60")
C_GREY = HexColor("#607D8B")
C_LTBLUE = HexColor("#D6EAF8")
C_LTGREEN = HexColor("#D5F5E3")
C_LTRED = HexColor("#FADBD8")
C_LTYELLOW = HexColor("#FEF9E7")
C_WHITE = HexColor("#FFFFFF")
C_DARK_TEXT = HexColor("#1A1A2E")
C_SUBHEAD = HexColor("#2C3E50")
W, H = landscape(A3) # 1190.6 x 841.9 pts
def centre_text(c, text, cx, cy, font, size, color=C_WHITE):
c.setFont(font, size)
c.setFillColor(color)
tw = c.stringWidth(text, font, size)
c.drawString(cx - tw/2, cy, text)
def multiline_text(c, lines, cx, top_y, font, size, line_h, color=C_DARK_TEXT, align="center"):
c.setFont(font, size)
c.setFillColor(color)
for i, line in enumerate(lines):
y = top_y - i * line_h
if align == "center":
tw = c.stringWidth(line, font, size)
c.drawString(cx - tw/2, y, line)
elif align == "left":
c.drawString(cx, y, line)
def wrap_lines(c, text, max_w, font, size):
words = text.split()
lines, cur = [], ""
for w in words:
test = (cur + " " + w).strip()
if c.stringWidth(test, font, size) <= max_w:
cur = test
else:
if cur: lines.append(cur)
cur = w
if cur: lines.append(cur)
return lines
def draw_rounded_rect(c, cx, cy, w, h, fill, stroke, radius=5, lw=1.2):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(lw)
c.roundRect(cx-w/2, cy-h/2, w, h, radius, fill=1, stroke=1)
def draw_box_text(c, cx, cy, w, h, fill, stroke, title, body_lines=None,
title_font="Helvetica-Bold", title_size=8.5, title_color=C_WHITE,
body_font="Helvetica", body_size=7.2, body_color=C_DARK_TEXT,
radius=5, lw=1.2, line_h=9.5):
draw_rounded_rect(c, cx, cy, w, h, fill, stroke, radius, lw)
if body_lines:
# title at top, body below
top = cy + h/2 - 2
# title bar
c.setFillColor(stroke)
c.roundRect(cx-w/2, cy+h/2-16, w, 16, radius, fill=1, stroke=0)
centre_text(c, title, cx, cy+h/2-13, title_font, title_size, title_color)
# body lines
c.setFont(body_font, body_size)
c.setFillColor(body_color)
for i, line in enumerate(body_lines):
c.drawString(cx - w/2 + 6, cy + h/2 - 24 - i*line_h, line)
else:
# centered text
lines = wrap_lines(c, title, w-10, title_font, title_size)
total = len(lines) * (title_size + 2)
start = cy + total/2 - 2
c.setFont(title_font, title_size)
c.setFillColor(title_color)
for i, line in enumerate(lines):
tw = c.stringWidth(line, title_font, title_size)
c.drawString(cx - tw/2, start - i*(title_size+2), line)
def draw_diamond(c, cx, cy, w, h, fill, stroke, text, fsize=8.5, lw=1.5):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(lw)
p = c.beginPath()
p.moveTo(cx, cy+h/2); p.lineTo(cx+w/2, cy)
p.lineTo(cx, cy-h/2); p.lineTo(cx-w/2, cy)
p.close()
c.drawPath(p, fill=1, stroke=1)
lines = wrap_lines(c, text, w*0.7, "Helvetica-Bold", fsize)
total = len(lines)*(fsize+2)
start = cy + total/2 - 2
c.setFont("Helvetica-Bold", fsize)
c.setFillColor(C_WHITE)
for i, line in enumerate(lines):
tw = c.stringWidth(line, "Helvetica-Bold", fsize)
c.drawString(cx-tw/2, start-i*(fsize+2), line)
def draw_oval(c, cx, cy, w, h, fill, stroke, text, fsize=9, lw=2):
c.setFillColor(fill)
c.setStrokeColor(stroke)
c.setLineWidth(lw)
c.ellipse(cx-w/2, cy-h/2, cx+w/2, cy+h/2, fill=1, stroke=1)
lines = wrap_lines(c, text, w*0.85, "Helvetica-Bold", fsize)
total = len(lines)*(fsize+2)
start = cy + total/2 - 1
c.setFont("Helvetica-Bold", fsize)
c.setFillColor(C_WHITE)
for i, line in enumerate(lines):
tw = c.stringWidth(line, "Helvetica-Bold", fsize)
c.drawString(cx-tw/2, start-i*(fsize+2), line)
def arrow(c, x1, y1, x2, y2, color=C_NAVY, lw=1.3):
c.setStrokeColor(color); c.setFillColor(color); c.setLineWidth(lw)
c.line(x1, y1, x2, y2)
_arrowhead(c, x2, y2, math.atan2(y2-y1, x2-x1), color)
def _arrowhead(c, x, y, angle, color, size=7):
c.setFillColor(color)
ax1 = x - size*math.cos(angle-math.pi/7)
ay1 = y - size*math.sin(angle-math.pi/7)
ax2 = x - size*math.cos(angle+math.pi/7)
ay2 = y - size*math.sin(angle+math.pi/7)
p = c.beginPath()
p.moveTo(x,y); p.lineTo(ax1,ay1); p.lineTo(ax2,ay2); p.close()
c.drawPath(p, fill=1, stroke=0)
def label_arrow(c, x, y, text, color=C_RED, font="Helvetica-Bold", size=7.5):
c.setFont(font, size); c.setFillColor(color)
tw = c.stringWidth(text, font, size)
c.drawString(x - tw/2, y, text)
def elbow_h_then_v(c, x1, y1, x2, y2, color=C_NAVY, lw=1.3):
"""Go horizontal from x1,y1 then vertical to x2,y2"""
c.setStrokeColor(color); c.setLineWidth(lw)
c.line(x1, y1, x2, y1)
c.line(x2, y1, x2, y2)
_arrowhead(c, x2, y2, math.atan2(y2-y1, 0), color)
def elbow_v_then_h(c, x1, y1, x2, y2, color=C_NAVY, lw=1.3):
"""Go vertical from x1,y1 then horizontal to x2,y2"""
c.setStrokeColor(color); c.setLineWidth(lw)
c.line(x1, y1, x1, y2)
c.line(x1, y2, x2, y2)
_arrowhead(c, x2, y2, math.atan2(0, x2-x1), color)
# ══════════════════════════════════════════════════════════
# CANVAS
# ══════════════════════════════════════════════════════════
out = "/home/daytona/workspace/paraneoplastic-flowchart/paraneoplastic_diagnostic_flowchart.pdf"
cv = canvas.Canvas(out, pagesize=landscape(A3))
cv.setTitle("Diagnostic Flowchart: Suspected Paraneoplastic Syndrome")
cv.setAuthor("Orris Medical AI")
cv.setSubject("Paraneoplastic Syndrome - Diagnostic Algorithm")
# ── TITLE BANNER ──────────────────────────────────────────
cv.setFillColor(C_NAVY); cv.rect(0, H-44, W, 44, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 16); cv.setFillColor(C_WHITE)
cv.drawCentredString(W/2, H-18, "Diagnostic Algorithm: Suspected Paraneoplastic Syndrome")
cv.setFont("Helvetica", 8); cv.setFillColor(HexColor("#A8C8E8"))
cv.drawCentredString(W/2, H-32, "Based on Harrison's Principles of Internal Medicine 22E (2025) & Goldman-Cecil Medicine | Orris Medical AI")
# ── FOOTER ────────────────────────────────────────────────
cv.setFillColor(C_NAVY); cv.rect(0, 0, W, 18, fill=1, stroke=0)
cv.setFont("Helvetica", 6.5); cv.setFillColor(HexColor("#A8C8E8"))
cv.drawCentredString(W/2, 6, "PNS = Paraneoplastic Syndrome | SCLC = Small Cell Lung Cancer | PET-CT = Positron Emission Tomography-CT | CSF = Cerebrospinal Fluid | IVIg = IV Immunoglobulin | LEMS = Lambert-Eaton Myasthenic Syndrome | PCD = Paraneoplastic Cerebellar Degeneration | DM = Dermatomyositis | MCD = Minimal Change Disease")
# ══════════════════════════════════════════════════════════
# LAYOUT GRID
# Usable: x 15..W-15 y 22..H-48
# Strategy: single central spine, then two columns split at step 3
# ══════════════════════════════════════════════════════════
CX = W/2 # centre x
LX = 210 # left column cx
RX = W - 210 # right column cx
# Y coordinates (top to bottom)
Y_START = H - 68 # START oval
Y_S1 = H - 108 # Step 1
Y_S2 = H - 148 # Step 2
Y_D1 = H - 197 # Decision 1: Cause found?
Y_YBOX = H - 248 # YES → Treat cause
Y_NBOX = H - 248 # NO → Consider PNS
Y_S3 = H - 297 # Step 3: Classify
Y_SPLIT = H - 340 # Split into L/R branches
Y_INV = H - 420 # Investigation boxes
Y_D2 = H - 492 # Decision 2 (both columns)
Y_OUTBOX = H - 538 # Outcome boxes
Y_TX = H - 590 # Treatment boxes
Y_MON = H - 630 # Monitoring oval
# Dimensions
OVW, OVH = 260, 28 # oval
BW, BH = 310, 36 # standard box
DBW, DBH = 130, 44 # decision diamond
# ── START ─────────────────────────────────────────────────
draw_oval(cv, CX, Y_START, OVW, OVH, C_TEAL, C_NAVY,
"PATIENT PRESENTS WITH UNEXPLAINED SYMPTOMS", fsize=8.5)
# ── STEP 1 ────────────────────────────────────────────────
arrow(cv, CX, Y_START-OVH/2, CX, Y_S1+BH/2)
draw_box_text(cv, CX, Y_S1, BW, BH, C_NAVY, C_TEAL,
"STEP 1 — Initial Assessment",
["History · Physical examination · Weight loss / constitutional Sx",
"CBC, CMP, LFTs, TFTs, urinalysis · Chest X-ray / CT"],
title_size=8.5, body_size=7.5, line_h=10)
# ── STEP 2 ────────────────────────────────────────────────
arrow(cv, CX, Y_S1-BH/2, CX, Y_S2+BH/2)
draw_box_text(cv, CX, Y_S2, BW, BH, C_NAVY, C_TEAL,
"STEP 2 — Exclude Common Causes",
["Direct metastasis · Infection · Drug toxicity · Metabolic",
"Organ failure · Radiation effects · Nutritional deficiency"],
title_size=8.5, body_size=7.5, line_h=10)
# ── DECISION 1 ────────────────────────────────────────────
arrow(cv, CX, Y_S2-BH/2, CX, Y_D1+DBH/2)
draw_diamond(cv, CX, Y_D1, DBW+70, DBH, C_AMBER, C_NAVY, "Cause Identified?", fsize=9)
# YES → left
left_d1 = CX - (DBW+70)/2
elbow_h_then_v(cv, left_d1, Y_D1, LX+BW/2-10, Y_YBOX+BH/2, color=C_GREEN)
label_arrow(cv, left_d1 - 30, Y_D1 + 3, "YES", C_GREEN)
draw_box_text(cv, LX - 10, Y_YBOX, BW-50, BH, C_GREEN, HexColor("#1E8449"),
"Treat Underlying Cause",
["Continue routine cancer surveillance",
"PNS may still co-exist — monitor"],
title_size=8, body_size=7, line_h=9.5)
# NO → right
right_d1 = CX + (DBW+70)/2
elbow_h_then_v(cv, right_d1, Y_D1, RX-BW/2+30, Y_NBOX+BH/2, color=C_RED)
label_arrow(cv, right_d1 + 28, Y_D1 + 3, "NO", C_RED)
draw_box_text(cv, RX + 10, Y_NBOX, BW-40, BH, C_RED, HexColor("#922B21"),
"SUSPECT Paraneoplastic Syndrome",
["Unexplained syndrome without obvious cause",
"Cancer known or unknown — proceed to Step 3"],
title_size=8, body_size=7, line_h=9.5)
# Both converge at Step 3
elbow_v_then_h(cv, LX-10, Y_YBOX-BH/2, CX-BW/2, Y_S3, color=C_GREEN)
elbow_v_then_h(cv, RX+10, Y_NBOX-BH/2, CX+BW/2, Y_S3, color=C_RED)
# ── STEP 3: CLASSIFY ──────────────────────────────────────
draw_box_text(cv, CX, Y_S3, BW, BH, C_NAVY, C_TEAL,
"STEP 3 — Classify Dominant Syndrome System",
["Neurologic · Endocrine/Metabolic · Hematologic",
"Dermatologic · Rheumatologic · Renal/Hepatic"],
title_size=8.5, body_size=7.5, line_h=10)
# ── SPLIT to two columns ──────────────────────────────────
# Neuro (left) and Non-neuro (right)
arrow(cv, CX, Y_S3-BH/2, CX, Y_SPLIT+6)
cv.setStrokeColor(C_NAVY); cv.setLineWidth(1.3)
cv.line(CX, Y_SPLIT, LX, Y_SPLIT)
cv.line(CX, Y_SPLIT, RX, Y_SPLIT)
_arrowhead(cv, LX, Y_SPLIT, math.pi, C_NAVY)
_arrowhead(cv, RX, Y_SPLIT, 0, C_NAVY)
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(C_NAVY)
cv.drawCentredString(LX, Y_SPLIT+6, "NEUROLOGIC")
cv.drawCentredString(RX, Y_SPLIT+6, "NON-NEUROLOGIC")
# ── INVESTIGATION BOXES ───────────────────────────────────
INV_W, INV_H = 240, 110
INV_Y = Y_INV
# --- NEUROLOGIC WORKUP ---
arrow(cv, LX, Y_SPLIT-6, LX, INV_Y+INV_H/2)
draw_rounded_rect(cv, LX, INV_Y, INV_W, INV_H, C_LTBLUE, C_NAVY, radius=6, lw=1.5)
# Header bar
cv.setFillColor(C_NAVY); cv.roundRect(LX-INV_W/2, INV_Y+INV_H/2-16, INV_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8.5); cv.setFillColor(C_WHITE)
cv.drawCentredString(LX, INV_Y+INV_H/2-11, "NEUROLOGIC WORKUP")
cv.setFont("Helvetica", 7.3); cv.setFillColor(C_DARK_TEXT)
ni_lines = [
"1. MRI brain — FLAIR: mesial temporal lobe signal",
"2. CSF: cells, protein, oligoclonal bands",
"3. Antibody panel (serum + CSF):",
" Anti-Hu, Yo, Ri, Ma2, CV2/CRMP5, amphiphysin",
" Anti-NMDAR, LGI1, CASPR2, GABA-B, AMPAR",
" Anti-VGCC (LEMS), anti-SOX1",
"4. PET-CT whole body / CT chest-abdomen-pelvis",
"5. Testicular / ovarian ultrasound (if applicable)",
]
for i, l in enumerate(ni_lines):
cv.drawString(LX-INV_W/2+6, INV_Y+INV_H/2-26-i*10.2, l)
# --- NON-NEUROLOGIC WORKUP ---
arrow(cv, RX, Y_SPLIT-6, RX, INV_Y+INV_H/2)
draw_rounded_rect(cv, RX, INV_Y, INV_W, INV_H, C_LTBLUE, C_NAVY, radius=6, lw=1.5)
cv.setFillColor(C_NAVY); cv.roundRect(RX-INV_W/2, INV_Y+INV_H/2-16, INV_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8.5); cv.setFillColor(C_WHITE)
cv.drawCentredString(RX, INV_Y+INV_H/2-11, "NON-NEUROLOGIC WORKUP")
cv.setFont("Helvetica", 7.3); cv.setFillColor(C_DARK_TEXT)
ri_lines = [
"1. Targeted labs per syndrome:",
" PTHrP · cortisol · ACTH · serum/urine osmolality",
" CBC diff · coags · LDH · EPO · haptoglobin",
"2. Hormone levels: ACTH, ADH, GH, IGF-2, VIP",
"3. Tumour markers: PSA, CEA, CA-125, AFP, β-hCG",
"4. CT chest / abdomen / pelvis + PET-CT",
"5. Skin biopsy (dermatologic PNS)",
"6. Renal biopsy (if nephrotic syndrome)",
]
for i, l in enumerate(ri_lines):
cv.drawString(RX-INV_W/2+6, INV_Y+INV_H/2-26-i*10.2, l)
# ── DECISION 2: Antibody/Tumour found? ───────────────────
D2W, D2H = 145, 42
arrow(cv, LX, INV_Y-INV_H/2, LX, Y_D2+D2H/2)
arrow(cv, RX, INV_Y-INV_H/2, RX, Y_D2+D2H/2)
draw_diamond(cv, LX, Y_D2, D2W, D2H, C_AMBER, C_NAVY, "Antibody Positive?", fsize=8.5)
draw_diamond(cv, RX, Y_D2, D2W, D2H, C_AMBER, C_NAVY, "Tumour Identified?", fsize=8.5)
# ── OUTCOME BOXES ─────────────────────────────────────────
OB_W, OB_H = 175, 36
# Neuro YES (centre of LX)
arrow(cv, LX, Y_D2-D2H/2, LX, Y_OUTBOX+OB_H/2, color=C_GREEN)
label_arrow(cv, LX+6, (Y_D2-D2H/2+Y_OUTBOX+OB_H/2)/2, "YES", C_GREEN)
draw_box_text(cv, LX, Y_OUTBOX, OB_W, OB_H, C_GREEN, HexColor("#1E8449"),
"PNS CONFIRMED",
["Positive antibody & cancer = confirmed PNS",
"Start immunotherapy + treat tumour"],
title_size=8, body_size=7, line_h=9.5)
# Neuro NO
neuro_no_x = LX - 115
elbow_h_then_v(cv, LX-D2W/2, Y_D2, neuro_no_x, Y_OUTBOX+OB_H/2, color=C_GREY)
label_arrow(cv, LX-D2W/2-28, Y_D2+3, "NO", C_GREY)
draw_box_text(cv, neuro_no_x, Y_OUTBOX, OB_W-30, OB_H, C_GREY, HexColor("#37474F"),
"PROBABLE PNS",
["Antibody-negative does not exclude PNS",
"If cancer found: treat; re-screen q6mo × 3yr"],
title_size=7.5, body_size=6.8, line_h=9)
# Non-neuro YES
arrow(cv, RX, Y_D2-D2H/2, RX, Y_OUTBOX+OB_H/2, color=C_GREEN)
label_arrow(cv, RX+6, (Y_D2-D2H/2+Y_OUTBOX+OB_H/2)/2, "YES", C_GREEN)
draw_box_text(cv, RX, Y_OUTBOX, OB_W, OB_H, C_GREEN, HexColor("#1E8449"),
"TREAT PRIMARY TUMOUR",
["Syndrome follows tumour course",
"Recurrence of PNS signals tumour relapse"],
title_size=8, body_size=7, line_h=9.5)
# Non-neuro NO
nonnl_no_x = RX + 120
elbow_h_then_v(cv, RX+D2W/2, Y_D2, nonnl_no_x, Y_OUTBOX+OB_H/2, color=C_GREY)
label_arrow(cv, RX+D2W/2+22, Y_D2+3, "NO", C_GREY)
draw_box_text(cv, nonnl_no_x, Y_OUTBOX, OB_W-30, OB_H, C_GREY, HexColor("#37474F"),
"SYMPTOMATIC Rx",
["Treat syndrome empirically",
"Repeat tumour screen q6mo × 2yr"],
title_size=7.5, body_size=6.8, line_h=9)
# ── TREATMENT BOXES ───────────────────────────────────────
TX_W, TX_H = 240, 94
arrow(cv, LX, Y_OUTBOX-OB_H/2, LX, Y_TX+TX_H/2)
arrow(cv, RX, Y_OUTBOX-OB_H/2, RX, Y_TX+TX_H/2)
# Neuro treatment
draw_rounded_rect(cv, LX, Y_TX, TX_W, TX_H, C_LTGREEN, C_GREEN, radius=6, lw=1.5)
cv.setFillColor(C_GREEN); cv.roundRect(LX-TX_W/2, Y_TX+TX_H/2-16, TX_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C_WHITE)
cv.drawCentredString(LX, Y_TX+TX_H/2-11, "NEUROLOGIC PNS — TREATMENT")
cv.setFont("Helvetica", 7.2); cv.setFillColor(C_DARK_TEXT)
tx_neuro = [
"1st line: IV Methylprednisolone + IVIg + Plasma exchange",
"2nd line: Rituximab or Cyclophosphamide",
"Intracellular Ab (Hu/Yo/Ri): Tumour Rx is priority",
" — T-cell mediated; often irreversible; poor Ab response",
"Surface Ab (NMDAR/LGI1/GABA-B): Better prognosis",
" — Respond well to immunotherapy + tumour removal",
"LEMS: 3,4-diaminopyridine (3,4-DAP) + pyridostigmine",
]
for i, l in enumerate(tx_neuro):
cv.drawString(LX-TX_W/2+6, Y_TX+TX_H/2-26-i*10, l)
# Non-neuro treatment
draw_rounded_rect(cv, RX, Y_TX, TX_W, TX_H, C_LTGREEN, C_GREEN, radius=6, lw=1.5)
cv.setFillColor(C_GREEN); cv.roundRect(RX-TX_W/2, Y_TX+TX_H/2-16, TX_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C_WHITE)
cv.drawCentredString(RX, Y_TX+TX_H/2-11, "NON-NEUROLOGIC PNS — TREATMENT")
cv.setFont("Helvetica", 7.2); cv.setFillColor(C_DARK_TEXT)
tx_nonnl = [
"Endocrine: Bisphosphonates/denosumab (↑Ca²⁺), tolvaptan (SIADH)",
" Metyrapone / osilodrostat (ectopic Cushing)",
"Hematologic: Anticoagulate Trousseau; steroids for AIHA",
" Rasburicase / allopurinol (tumour lysis prophylaxis)",
"Dermatologic: Steroids ± IVIG for dermatomyositis/pemphigus",
"Renal: Treat primary tumour; steroids for MCD",
"ALL: Effective tumour therapy = best treatment for PNS",
]
for i, l in enumerate(tx_nonnl):
cv.drawString(RX-TX_W/2+6, Y_TX+TX_H/2-26-i*10, l)
# ── KEY ANTIBODIES BOX (centre) ──────────────────────────
AB_X = CX; AB_Y = Y_INV - 5
AB_W = W - LX - INV_W/2 - (W - RX) - INV_W/2 - 20
AB_W = max(AB_W, 200)
AB_W = 190; AB_H = INV_H
draw_rounded_rect(cv, AB_X, AB_Y, AB_W, AB_H, HexColor("#FDFEFE"), C_NAVY, radius=6, lw=2)
cv.setFillColor(C_NAVY); cv.roundRect(AB_X-AB_W/2, AB_Y+AB_H/2-16, AB_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8.5); cv.setFillColor(C_WHITE)
cv.drawCentredString(AB_X, AB_Y+AB_H/2-11, "KEY ANTIBODIES")
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(C_RED)
cv.drawString(AB_X-AB_W/2+5, AB_Y+AB_H/2-26, "High-risk (intracellular antigen):")
cv.setFont("Helvetica", 6.8); cv.setFillColor(C_DARK_TEXT)
ab_hi = [
"Anti-Hu → SCLC; sensory neuropathy, encephalomyelitis",
"Anti-Yo → Breast/ovarian; PCD",
"Anti-Ri → Breast; opsoclonus-myoclonus",
"Anti-Ma2 → Testicular; limbic/brainstem encephalitis",
"Anti-CV2 → SCLC; encephalomyelitis, neuropathy",
]
for i, l in enumerate(ab_hi):
cv.drawString(AB_X-AB_W/2+5, AB_Y+AB_H/2-36-i*9.5, l)
cv.setFont("Helvetica-Bold", 7); cv.setFillColor(C_GREEN)
cv.drawString(AB_X-AB_W/2+5, AB_Y+AB_H/2-36-5*9.5-3, "Surface antigen (better prognosis):")
cv.setFont("Helvetica", 6.8); cv.setFillColor(C_DARK_TEXT)
ab_lo = [
"Anti-NMDAR → Ovarian teratoma; encephalitis",
"Anti-LGI1 → Thymoma/SCLC; limbic encephalitis",
"Anti-CASPR2 → Thymoma; Morvan syndrome",
"Anti-GABA-B → SCLC; limbic encephalitis + seizures",
"Anti-VGCC → SCLC; LEMS, cerebellar degeneration",
]
for i, l in enumerate(ab_lo):
cv.drawString(AB_X-AB_W/2+5, AB_Y+AB_H/2-36-5*9.5-14-i*9.5, l)
# ── SYNDROME LEGEND BOX (centre, below antibody box) ─────
SYN_X = CX; SYN_Y = Y_D2
SYN_W = 190; SYN_H = 86
draw_rounded_rect(cv, SYN_X, SYN_Y, SYN_W, SYN_H, HexColor("#FEF9E7"), C_AMBER, radius=6, lw=1.5)
cv.setFillColor(C_AMBER); cv.roundRect(SYN_X-SYN_W/2, SYN_Y+SYN_H/2-16, SYN_W, 16, 3, fill=1, stroke=0)
cv.setFont("Helvetica-Bold", 8); cv.setFillColor(C_WHITE)
cv.drawCentredString(SYN_X, SYN_Y+SYN_H/2-11, "NON-NEUROLOGIC PNS — EXAMPLES")
cv.setFont("Helvetica", 6.8); cv.setFillColor(C_DARK_TEXT)
syn_lines = [
"Endocrine: SIADH (SCLC), ectopic ACTH, PTHrP-HHM",
"Hematologic: Trousseau (pancreatic), AIHA, DIC, erythrocytosis",
"Dermatologic: Acanthosis nigricans (GI), dermatomyositis,",
" PNP (lymphoma), erythema gyratum repens (lung)",
"Rheumatologic: Hypertrophic osteoarthropathy (lung/pleura)",
"Renal: Membranous GN (colon/lung), Stauffer (RCC)",
]
for i, l in enumerate(syn_lines):
cv.drawString(SYN_X-SYN_W/2+5, SYN_Y+SYN_H/2-26-i*9.5, l)
# ── MONITORING OVAL ───────────────────────────────────────
arrow(cv, LX, Y_TX-TX_H/2, LX, Y_MON+14)
arrow(cv, RX, Y_TX-TX_H/2, RX, Y_MON+14)
# bring to centre
cv.setStrokeColor(C_TEAL); cv.setLineWidth(1.3)
cv.line(LX, Y_MON, CX-140, Y_MON)
cv.line(RX, Y_MON, CX+140, Y_MON)
_arrowhead(cv, CX-140, Y_MON, math.pi, C_TEAL)
_arrowhead(cv, CX+140, Y_MON, 0, C_TEAL)
draw_oval(cv, CX, Y_MON, 280, 26, C_TEAL, C_NAVY,
"ONGOING MONITORING — Reassess every 3–6 months. Return of PNS symptoms = suspect tumour recurrence.", fsize=7.5)
cv.save()
print(f"Saved to: {out}")
cd /home/daytona/workspace/paraneoplastic-flowchart && python build_flowchart_v2.py 2>&1
cd /home/daytona/workspace/paraneoplastic-flowchart && pdftoppm -r 130 -png paraneoplastic_diagnostic_flowchart.pdf preview2 && ls -lh preview2-1.png
cd /home/daytona/workspace/paraneoplastic-flowchart && python3 -c " # Check exact Y values causing overlap # Y_D2 = H - 492, Y_OUTBOX = H - 538, Y_TX = H - 590, Y_MON = H - 630 # H = 841.9 H = 841.9 print('Y_D2 =', H-492) print('Y_OUTBOX=', H-538) print('Y_TX =', H-590) print('Y_MON =', H-630) print('OB_H=36, TX_H=94') print('Gap D2->OUTBOX:', (H-492-42/2) - (H-538+36/2), '(should be >5)') print('Gap OUTBOX->TX:', (H-538-36/2) - (H-590+94/2), '(should be >5)') print('Gap TX->MON:', (H-590-94/2) - (H-630+14), '(should be >5)') "
~/paraneoplastic-flowchart/build_flowchart_v2.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/paraneoplastic-flowchart/build_flowchart_v2.py'
~/paraneoplastic-flowchart/build_flowchart_v2.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/paraneoplastic-flowchart/build_flowchart_v2.py'
cd /home/daytona/workspace/paraneoplastic-flowchart && python build_flowchart_v2.py && pdftoppm -r 130 -png paraneoplastic_diagnostic_flowchart.pdf preview3 && ls -lh preview3-1.png
paraneoplastic diagnostic flowchart
PDF Document · PDF