Paraneoplastic syndrome

Reading File
Finding Sources
Searching PubMed

"paraneoplastic syndrome" AND diagnosis

Finding Sources
Reading File
Finding Sources
Reading File
Reading File
Finding Sources
Reading File
Reading File
Reading File
Reading File
Reading File
Loading Image
Loading Image
I now have comprehensive, high-quality content from Harrison's 22E and Goldman-Cecil to write a detailed answer. Let me compile the full response.

Paraneoplastic Syndromes

Definition

Paraneoplasia ("alongside cancer") refers to a cancer's remote effects that cannot be attributed to direct invasion, metastasis, or common complications such as infection, coagulopathy, or treatment toxicity. These syndromes affect up to 15% of cancer patients (falling to a few percent when cachexia is excluded). Importantly, up to two-thirds arise before the underlying malignancy is diagnosed, making their recognition clinically critical.
  • Harrison's Principles of Internal Medicine 22E, Ch. 99
  • Goldman-Cecil Medicine, Ch. 164

Pathogenesis

Paraneoplastic syndromes arise by several mechanisms:

1. Ectopic Hormone / Substance Production

Tumors secrete biologically active substances - hormones, hormone-like peptides, cytokines, growth factors, or enzymes - that act at distant sites. Examples include ACTH secretion by SCLC causing Cushing syndrome, and PTHrP secretion causing hypercalcemia.

2. Autoimmune / Antibody-Mediated (Neurologic PNDs)

Tumors ectopically express neural proteins, triggering immune responses. Two major mechanisms:
Antibodies against intracellular antigens (e.g., anti-Hu, anti-Yo):
  • Cytotoxic T-cells are the main effectors
  • Antibodies cannot reach intracellular targets
  • Results in irreversible neuronal loss
  • Poor response to immunotherapy
Antibodies against cell-surface / synaptic antigens (e.g., anti-NMDAR, anti-LGI1, anti-CASPR2):
  • Antibodies directly bind and alter receptor/channel function
  • Neuronal loss is less severe
  • Better response to immunotherapy and tumor treatment
Mechanism of PND: intracellular vs. cell-surface antigen encephalitis
Figure 99-1 (Harrison's 22E): Left: cytotoxic T-cell-mediated neuronal destruction with intracellular antigens (Hu antibodies, panels C/E/G/H). Right: antibody-mediated dysfunction with cell-surface antigens (NMDAR antibodies, panels D/F/I/J).

Classification by System

I. Neurologic Paraneoplastic Disorders (PNDs)

Clinically disabling PNDs occur in 0.5-1% of all cancer patients, but in 2-3% with neuroblastoma or SCLC and 30-50% with thymoma. In 60% of patients, neurologic symptoms precede the cancer diagnosis.
Only 60-70% of CNS PNDs and <20% of peripheral PNDs have detectable neuronal antibodies.
SyndromeKey FeaturesAssociated TumorAntibody
Lambert-Eaton Myasthenic Syndrome (LEMS)Proximal lower limb weakness, strength improves with sustained contraction; autonomic featuresSCLC (>50%)Anti-VGCC, anti-SOX1
Paraneoplastic Encephalomyelitis / Subacute Sensory NeuropathyAsymmetric numbness, burning paresthesias, sensory ataxia, loss of proprioception/vibrationSCLCAnti-Hu (ANNA-1)
Paraneoplastic Cerebellar Degeneration (PCD)Abrupt onset dysarthria, ataxia, oculomotor dysfunction; Purkinje cell loss; cerebellar atrophy on MRIBreast, ovarian, SCLC, Hodgkin lymphomaAnti-Yo (PCA-1), anti-Hu, anti-Ri, anti-Tr
Limbic EncephalitisSubacute amnesia, affective disorders, seizures; mesial temporal FLAIR signal on MRISCLC, testicular (NMDAR), thymomaAnti-Hu, anti-Ma2, anti-LGI1, anti-CASPR2, anti-AMPAR
Stiff-Person SyndromeProgressive muscle stiffness, rigidity, painful spasmsBreast cancerAnti-GAD65, anti-amphiphysin
Paraneoplastic Opsoclonus-MyoclonusChaotic multidirectional eye movements + myoclonusBreast cancer (adults), neuroblastoma (children)Anti-Ri
Paraneoplastic NeuropathySensorimotor neuropathy, demyelinating variantsMultipleAnti-CRMP5 (CV2), anti-Hu
Dermatomyositis/PolymyositisProximal muscle weakness, rash (DM)Lung, ovarian, colorectal, bladderAnti-Jo-1, anti-Mi-2
Brain MRI in paraneoplastic and autoimmune encephalitis
Figure 99-2 (Harrison's 22E): A = Limbic encephalitis with bilateral mesial temporal FLAIR hyperintensity. B = Anti-NMDAR encephalitis (often normal MRI). C = Anti-GABA-B encephalitis with cortical-subcortical FLAIR changes. D = MOG antibody cortical encephalitis in a child.

II. Endocrine / Metabolic Paraneoplastic Syndromes

SyndromeMechanismAssociated Tumor
Humoral hypercalcemia of malignancyPTHrP secretionSquamous cell lung, breast, renal cell, bladder
SIADH / HyponatremiaEctopic ADHSCLC, CNS tumors, head and neck cancers
Cushing SyndromeEctopic ACTHSCLC, pancreatic NET, bronchial carcinoid, medullary thyroid cancer
HypoglycemiaEctopic IGF-2Mesenchymal tumors, hepatocellular carcinoma
AcromegalyEctopic GHRHPancreatic islet, carcinoid tumors
Carcinoid SyndromeSerotonin, bradykininGI carcinoids with liver mets, bronchial carcinoid
VIPoma (Watery diarrhea)Ectopic VIPPancreatic islet cell tumors

III. Hematologic Paraneoplastic Syndromes

  • Erythrocytosis - ectopic EPO from renal cell carcinoma, hepatocellular carcinoma, cerebellar hemangioblastoma
  • Granulocytosis / Leukocytosis - ectopic G-CSF, GM-CSF (lung, bladder, GI tumors)
  • Thrombocytosis - tumor-secreted IL-6, thrombopoietin (various solid tumors)
  • Autoimmune hemolytic anemia - lymphomas, CLL
  • DIC - mucin-secreting adenocarcinomas (pancreatic, gastric), promyelocytic leukemia
  • Trousseau syndrome - migratory thrombophlebitis (pancreatic, lung, GI cancers)

IV. Dermatologic Paraneoplastic Syndromes

Skin FindingAssociated Malignancy
Acanthosis nigricansGI adenocarcinoma (especially gastric)
DermatomyositisOvarian, lung, GI cancers
Bazex syndrome (acrokeratosis paraneoplastica)Head and neck squamous cell carcinoma
Erythema gyratum repensLung, breast, esophageal cancers
Necrolytic migratory erythemaGlucagonoma
Sweet syndromeAML, myelodysplastic syndrome
Paraneoplastic pemphigusNHL, CLL, thymoma, Castleman disease
Leser-Trelat sign (sudden eruption of seborrheic keratoses)GI adenocarcinomas

V. Rheumatologic / Musculoskeletal Paraneoplastic Syndromes

  • Hypertrophic osteoarthropathy (periosteal new bone formation, clubbing) - lung cancer, pleural mesothelioma
  • Palmar fasciitis and polyarthritis syndrome - ovarian cancer
  • Polymyalgia-like syndrome - various solid tumors
  • Carcinomatous polyarthritis - large joint oligoarthritis/polyarthritis preceding cancer diagnosis

VI. Renal and Hepatic Paraneoplastic Syndromes

  • Nephrotic syndrome (membranous nephropathy) - colon, lung, gastric cancers; Hodgkin lymphoma
  • Minimal change disease - Hodgkin lymphoma
  • Stauffer syndrome (non-metastatic hepatic dysfunction) - renal cell carcinoma

Key Antibodies in Neurologic PNDs

"High-risk" antibodies (strongly predict cancer - usually intracellular targets, T-cell mediated, poor prognosis):
  • Anti-Hu (ANNA-1) - SCLC; encephalomyelitis, sensory neuropathy
  • Anti-Yo (PCA-1) - breast, ovarian; cerebellar degeneration
  • Anti-Ri (ANNA-2) - breast; opsoclonus-myoclonus
  • Anti-Ma2 - testicular; limbic/brainstem encephalitis
  • Anti-CV2 (CRMP5) - SCLC; encephalomyelitis, neuropathy
  • Anti-amphiphysin - SCLC, breast; stiff-person syndrome
"Lower-risk" or surface-antigen antibodies (less often paraneoplastic, better prognosis):
  • Anti-NMDAR - ovarian teratoma (especially young women); encephalitis
  • Anti-LGI1 - thymoma, SCLC; limbic encephalitis, faciobrachial dystonic seizures
  • Anti-CASPR2 - thymoma; Morvan syndrome, limbic encephalitis
  • Anti-GABA-B - SCLC; limbic encephalitis with prominent seizures
  • Anti-AMPAR - thymoma, SCLC, breast; limbic encephalitis
  • Anti-VGCC - SCLC; LEMS, cerebellar degeneration

Diagnosis

A stepwise approach (Goldman-Cecil Table 164-6):
  1. Characterize the abnormality and obtain appropriate labs/biopsy
  2. Exclude common causes (metastatic disease, infection, medication toxicity, metabolic derangement)
  3. Consider paraneoplastic syndrome if no obvious etiology
  4. Screen for malignancy: physical exam (including breast, gynecologic, prostate), CBC, CMP, urinalysis, CXR, mammogram; CT abdomen/pelvis or whole-body PET-CT for suspicious findings
  5. Order antibody panel (for neurologic syndromes); skin biopsy as indicated
  6. For germ cell tumors: scrotal/transvaginal ultrasound + pelvic MRI (PET may miss)
  7. If PND antibodies negative: diagnosis relies on demonstrating cancer and excluding other disorders
  8. CSF (in PNDs of CNS): mild pleocytosis (<200 mononuclear cells), elevated protein, variable oligoclonal bands

Treatment

Core principle: Effective treatment of the underlying tumor is the most important step and often produces improvement or resolution of the syndrome. Return of systemic symptoms may herald cancer recurrence.
Neurologic PNDs:
  • Intracellular antigen-associated (Hu, Yo, etc.): Aggressive tumor treatment is priority. Limited benefit from immunotherapy. Many have irreversible deficits due to T-cell-mediated neuronal loss.
  • Cell-surface antigen-associated (NMDAR, LGI1, etc.): Respond better to immunotherapy. Standard escalation: glucocorticoids + IVIg + plasma exchange first line; rituximab or cyclophosphamide second line. Tumor treatment (if applicable) is also key.
Endocrine/metabolic:
  • Treat underlying malignancy
  • Specific management: bisphosphonates/denosumab/cinacalcet for hypercalcemia; demeclocycline/tolvaptan/fluid restriction for SIADH; metyrapone/ketoconazole/osilodrostat for ectopic Cushing
LEMS: Pyridostigmine + 3,4-diaminopyridine (3,4-DAP) for symptomatic relief; tumor treatment

Key Clinical Pearls

  • LEMS strength improves with repeated activity (opposite of myasthenia gravis - EMG shows incremental response at >10 Hz stimulation)
  • Cerebellar degeneration from PND is often abrupt and severe, unlike many other cerebellar disorders
  • Anti-NMDAR encephalitis classically affects young women with ovarian teratoma and follows a stereotyped course: psychiatric symptoms → seizures → movement disorders → decreased consciousness → autonomic instability
  • The syndrome course parallels the tumor - effective treatment leads to improvement; recurrence signals tumor relapse
  • Periodic tumor surveillance is needed even after negative initial workup (e.g., repeat screening q6 months x 3 years for LEMS given high SCLC risk)

Sources:
  • Harrison's Principles of Internal Medicine, 22E (2025) - Chapters 99 (Paraneoplastic Neurologic Syndromes) and 98 (Oncologic Emergencies)
  • Goldman-Cecil Medicine - Chapter 164 (Approach to the Patient with Cancer)
  • Recent PubMed: Oral manifestations in paraneoplastic syndromes - systematic review and meta-analysis (PMID 39415566, Oral Dis 2025); Nephrotic syndrome with solid malignancies - systematic review (PMID 38965515, BMC Nephrol 2024)

Create a diagnostic flowchart for suspected paraneoplastic syndrome

Asking for Preferences
Reading File
Running Command

mkdir -p /home/daytona/workspace/paraneoplastic-flowchart && ls /home/daytona/workspace/paraneoplastic-flowchart/

Writing File

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

cd /home/daytona/workspace/paraneoplastic-flowchart && python build_flowchart.py 2>&1

Running Command

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}') "

Running Command

cd /home/daytona/workspace/paraneoplastic-flowchart && convert -density 120 paraneoplastic_diagnostic_flowchart.pdf -flatten preview.png 2>&1 && ls -lh preview.png

Running Command

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

Running Command

cd /home/daytona/workspace/paraneoplastic-flowchart && pdftoppm -r 120 -png paraneoplastic_diagnostic_flowchart.pdf preview && ls -lh preview*.png 2>&1

Reading File
Writing File

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

cd /home/daytona/workspace/paraneoplastic-flowchart && python build_flowchart_v2.py 2>&1

Running Command

cd /home/daytona/workspace/paraneoplastic-flowchart && pdftoppm -r 130 -png paraneoplastic_diagnostic_flowchart.pdf preview2 && ls -lh preview2-1.png

Reading File
Running Command

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)') "

Editing File

~/paraneoplastic-flowchart/build_flowchart_v2.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/paraneoplastic-flowchart/build_flowchart_v2.py'
Editing File

~/paraneoplastic-flowchart/build_flowchart_v2.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/paraneoplastic-flowchart/build_flowchart_v2.py'
Running Command

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

Reading File
The flowchart looks clean and well-organized. The layout flows clearly from top to bottom, the two columns are well separated, all major content is readable, and the monitoring oval terminates the flow at the bottom. The file is ready.

Your diagnostic flowchart PDF is ready for download:

paraneoplastic diagnostic flowchart

PDF Document · PDF

What's included in the flowchart:
The chart is A3 landscape and walks through 6 structured steps:
  1. Start - Patient with unexplained symptoms
  2. Step 1 - Initial assessment (history, labs, imaging)
  3. Step 2 - Exclude common causes (metastasis, infection, drug toxicity, metabolic)
  4. Decision diamond - Cause identified? YES (treat) / NO (suspect PNS)
  5. Step 3 - Classify dominant syndrome system → splits into two parallel columns:
    • Left (Neurologic): Full workup (MRI, CSF, antibody panel), "Antibody Positive?" decision, confirmed vs probable PNS, neurologic treatment protocol
    • Right (Non-neurologic): Targeted labs per syndrome, "Tumour Identified?" decision, tumour treatment vs symptomatic Rx, non-neurologic treatment protocol
  6. Central reference panels - Key antibodies (high-risk vs surface-antigen) and non-neurologic PNS examples
  7. Monitoring oval at the bottom - reassess every 3-6 months; PNS recurrence signals tumour relapse
All content is sourced from Harrison's Principles of Internal Medicine 22E (2025) and Goldman-Cecil Medicine.
This is a shared conversation. Sign in to Orris to start your own chat.