Hyponatremia workup algorithm
hyponatremia workup diagnostic algorithm serum osmolality urine sodium 2024
| Test | Purpose |
|---|---|
| Serum sodium, BMP | Confirm hyponatremia, assess K+, glucose, BUN/Cr |
| Serum osmolality | First branch point - classify type |
| Urine osmolality | Assess ADH activity / renal concentrating ability |
| Urine sodium (spot) | Assess renal sodium handling / volume status |
| TSH | Rule out hypothyroidism |
| Morning cortisol | Rule out adrenal insufficiency |
| Serum lipids, protein | If pseudohyponatremia suspected |
Serum Osmolality
├── Normal (275-295) or HIGH (>295) → NOT true hypotonic hyponatremia
│ ├── Normal Posm: Pseudohyponatremia
│ │ • Marked hyperlipidemia
│ │ • Marked hyperproteinemia (e.g., multiple myeloma)
│ └── High Posm: Translocational hyponatremia
│ • Hyperglycemia (Na drops ~1.6 mEq/L per 100 mg/dL glucose rise)
│ • Mannitol, sorbitol, glycine (urologic irrigants)
│
└── LOW (<275) → TRUE hypotonic hyponatremia → PROCEED TO STEP 2
Note: Hyperglycemia correction formula: corrected Na = measured Na + 1.6 × [(glucose - 100) / 100]

| Finding | Diagnosis to Consider |
|---|---|
| Thiazide use | Thiazide-induced hyponatremia |
| Urine Osm < 100 mOsm/kg | Water intoxication: psychogenic polydipsia, beer potomania, ecstasy, exercise-associated hyponatremia (EAH) |
| Marked hyperglycemia | Hyperglycemic-induced hyponatremia (translocational) |
| Markedly elevated creatinine | Kidney failure |
| Hyperkalemia + unexplained hypoglycemia | Primary adrenal insufficiency |
| Normal/elevated serum osmolality | Pseudohyponatremia |
| Recent marathon / endurance event | Exercise-associated hyponatremia |
| Recent "party"/rave | Ecstasy (MDMA) use |
TRUE Hypotonic Hyponatremia (Posm <275)
│
├── HYPOVOLEMIC (low volume)
│ Signs: Dry mucous membranes, decreased skin turgor,
│ tachycardia, orthostasis, low JVP
│
├── EUVOLEMIC (normal volume)
│ Signs: No edema, no orthostasis
│
└── HYPERVOLEMIC (excess volume)
Signs: Edema, ascites, elevated JVP
| Urine Na | Etiology |
|---|---|
| < 20-30 mEq/L (renal Na conserving) | Extra-renal losses: vomiting, diarrhea, sweating, third-spacing, burns |
| > 20-30 mEq/L (renal Na wasting) | Diuretics (thiazides > loop), salt-wasting nephropathy, cerebral salt wasting (CSW), primary adrenal insufficiency, mineralocorticoid deficiency |
Key distinguisher: CSW vs SIADH - both have elevated urine Na and elevated urine Osm, but CSW is hypovolemic while SIADH is euvolemic.
| Urine Na | Etiology |
|---|---|
| < 20 mEq/L (low effective arterial blood volume) | Heart failure, cirrhosis, nephrotic syndrome |
| > 20 mEq/L | Advanced renal failure (GFR < 5-10 mL/min) |
Euvolemia
│
├── Urine Osm < 100 mOsm/kg → Maximally dilute urine, ADH suppressed
│ • Primary polydipsia (psychogenic)
│ • Beer potomania
│ • Ecstasy / MDMA
│ • Low-solute diet ("tea and toast")
│
└── Urine Osm > 100 mOsm/kg → Inappropriately concentrated, ADH active
│
├── Check Urine Na
│ • UNa < 20-30 → Consider subtle hypovolemia
│ (re-classify as hypovolemic)
│ • UNa > 30 → Proceed to hormonal workup
│
└── Check TSH and Morning Cortisol (Figure 24-4)

| Finding | Diagnosis |
|---|---|
| TSH markedly elevated | Severe hypothyroidism |
| Morning cortisol < 16.3 mcg/dL | Evaluate for adrenal insufficiency (requires cosyntropin stimulation test to confirm) |
| Other etiologies excluded + Uosm elevated + UNa elevated | SIADH (diagnosis of exclusion) |
| Volume Status | UNa | Urine Osm | Likely Etiology |
|---|---|---|---|
| Hypovolemic | < 20 | > 100 | GI/skin losses, third-spacing |
| Hypovolemic | > 20 | > 100 | Diuretics, salt-wasting nephropathy, adrenal insufficiency |
| Hypervolemic | < 20 | > 100 | HF, cirrhosis, nephrotic syndrome |
| Hypervolemic | > 20 | Variable | Advanced renal failure |
| Euvolemic | < 20 | < 100 | Primary polydipsia, beer potomania |
| Euvolemic | > 40 | > 100 | SIADH, hypothyroidism, adrenal insufficiency |
Rule of thumb (Tintinalli): UNa < 10 mEq/L is found only in edematous syndromes (HF, cirrhosis) and in vomiting/diarrhea.
| Clinical Scenario | Max Correction Rate |
|---|---|
| Severe symptoms (seizures, obtundation, coma) | Raise by 4-6 mEq/L acutely with 3% NaCl bolus (100 mL over ~15 min), then reassess |
| Chronic / asymptomatic | ≤ 6-8 mEq/L per 24 hours (high-risk patients) |
| Chronic / mild-moderate symptoms | ≤ 10-12 mEq/L per 24 hours (low-risk patients) |
| 48-hour limit | ≤ 18 mEq/L total over 48 hours |
Create a quick reference PDF with the hyponatremia workup algorithm and correction limits
ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or not foun…
mkdir -p /home/daytona/workspace/hyponatremia-pdf && python3 -c "import reportla…
~/hyponatremia-pdf/build_pdf.py
#!/usr/bin/env python3
"""Build a polished 2-page quick-reference PDF for hyponatremia workup."""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus.flowables import Flowable
import reportlab.rl_config
OUTPUT = "/home/daytona/workspace/hyponatremia-pdf/Hyponatremia_Quick_Reference.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
C_NAVY = colors.HexColor("#1a3a5c") # header backgrounds
C_TEAL = colors.HexColor("#1a7a7a") # section banners
C_BLUE_L = colors.HexColor("#ddeeff") # light blue fills
C_TEAL_L = colors.HexColor("#d4f0f0") # light teal fills
C_AMBER = colors.HexColor("#fff3cd") # warning boxes
C_RED = colors.HexColor("#c0392b") # urgent text
C_GREEN = colors.HexColor("#1d6a30") # safe/normal
C_ORANGE = colors.HexColor("#d35400")
C_GREY = colors.HexColor("#f5f6fa")
C_BORDER = colors.HexColor("#aabbcc")
C_WHITE = colors.white
# ── Style helpers ─────────────────────────────────────────────────────────────
def S(name, **kw):
base = {
"fontName": "Helvetica",
"fontSize": 9,
"leading": 12,
"textColor": colors.black,
"spaceAfter": 0,
"spaceBefore": 0,
}
base.update(kw)
return ParagraphStyle(name, **base)
TITLE = S("title", fontName="Helvetica-Bold", fontSize=16, textColor=C_WHITE,
alignment=TA_CENTER, leading=20)
SUBTITLE= S("sub", fontName="Helvetica", fontSize=9, textColor=C_BLUE_L,
alignment=TA_CENTER)
SEC_HDR = S("sechdr", fontName="Helvetica-Bold", fontSize=10, textColor=C_WHITE,
alignment=TA_LEFT, leading=13)
BODY = S("body", fontSize=8.5, leading=12)
BODYB = S("bodyb", fontName="Helvetica-Bold", fontSize=8.5, leading=12)
SMALL = S("small", fontSize=7.5, leading=10, textColor=colors.HexColor("#444"))
NOTE = S("note", fontSize=7.5, leading=10, textColor=C_RED, fontName="Helvetica-Oblique")
TH = S("th", fontName="Helvetica-Bold", fontSize=8, textColor=C_WHITE,
alignment=TA_CENTER, leading=11)
TD = S("td", fontSize=8, leading=11, alignment=TA_LEFT)
TDC = S("tdc", fontSize=8, leading=11, alignment=TA_CENTER)
TDCB = S("tdcb", fontName="Helvetica-Bold", fontSize=8, leading=11,
alignment=TA_CENTER, textColor=C_NAVY)
ARROW = S("arrow", fontName="Helvetica-Bold", fontSize=11, textColor=C_TEAL,
alignment=TA_CENTER)
def sp(h=3):
return Spacer(1, h*mm)
def hr(color=C_BORDER, thickness=0.5):
return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=2, spaceBefore=2)
def section_banner(text, bg=C_NAVY):
data = [[Paragraph(text, SEC_HDR)]]
t = Table(data, colWidths=["100%"])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [3,3,3,3]),
]))
return t
def make_table(headers, rows, col_widths, row_colors=None, header_bg=C_NAVY):
header_row = [Paragraph(h, TH) for h in headers]
body_rows = []
for r in rows:
body_rows.append([Paragraph(str(c), TD) for c in r])
data = [header_row] + body_rows
t = Table(data, colWidths=col_widths, repeatRows=1)
style = [
("BACKGROUND", (0,0), (-1,0), header_bg),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GREY]),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("FONTSIZE", (0,1), (-1,-1), 8),
]
if row_colors:
for (row_idx, col_start, col_end, bg) in row_colors:
style.append(("BACKGROUND", (col_start, row_idx), (col_end, row_idx), bg))
t.setStyle(TableStyle(style))
return t
# ── Arrow / box flowchart helper ──────────────────────────────────────────────
def algo_box(label, sublabel="", bg=C_BLUE_L, border=C_NAVY, text_color=C_NAVY,
bold=True):
fn = "Helvetica-Bold" if bold else "Helvetica"
sty = S("box", fontName=fn, fontSize=8.5, textColor=text_color,
alignment=TA_CENTER, leading=12)
sty2 = S("boxsub", fontName="Helvetica", fontSize=7.5,
textColor=colors.HexColor("#555"), alignment=TA_CENTER, leading=10)
content = [[Paragraph(label, sty)]]
if sublabel:
content.append([Paragraph(sublabel, sty2)])
t = Table(content, colWidths=["100%"])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1.2, border),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
return t
def down_arrow():
return Paragraph("▼", ARROW)
# ══════════════════════════════════════════════════════════════════════════════
# BUILD DOCUMENT
# ══════════════════════════════════════════════════════════════════════════════
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=15*mm, rightMargin=15*mm,
topMargin=12*mm, bottomMargin=12*mm,
title="Hyponatremia Quick Reference",
author="Orris Clinical Reference",
subject="Hyponatremia Workup Algorithm & Correction Limits",
)
W = A4[0] - 30*mm # usable width
story = []
# ── PAGE HEADER ───────────────────────────────────────────────────────────────
hdr_data = [
[Paragraph("HYPONATREMIA", TITLE),
Paragraph("Quick Reference | Workup Algorithm & Correction Limits", SUBTITLE)]
]
hdr_table = Table([[
Paragraph("HYPONATREMIA", S("t2", fontName="Helvetica-Bold", fontSize=18,
textColor=C_WHITE, alignment=TA_CENTER, leading=22)),
Paragraph("Quick Reference\nWorkup Algorithm & Correction Limits",
S("sub2", fontSize=9, textColor=C_BLUE_L, alignment=TA_LEFT, leading=13))
]], colWidths=[60*mm, W-60*mm])
hdr_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(hdr_table)
story.append(sp(3))
# ── DEFINITION ROW ────────────────────────────────────────────────────────────
def_data = [
[Paragraph("<b>Definition:</b> Serum [Na<sup>+</sup>] < 135 mEq/L", BODY),
Paragraph("<b>Prevalence:</b> Most common electrolyte disorder in hospitalized patients (~15-30%)", BODY),
Paragraph("<b>Key risk:</b> Osmotic demyelination syndrome (ODS) with overly rapid correction", BODY)]
]
def_table = Table(def_data, colWidths=[W/3]*3)
def_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_BLUE_L),
("BOX", (0,0), (-1,-1), 0.5, C_BORDER),
("INNERGRID", (0,0), (-1,-1), 0.3, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(def_table)
story.append(sp(4))
# ── INITIAL LABS ─────────────────────────────────────────────────────────────
story.append(section_banner("STEP 0 | INITIAL LABS TO ORDER"))
story.append(sp(2))
labs_rows = [
["Serum BMP (Na, K, Cl, HCO3, BUN, Cr, Glucose)", "Confirm hyponatremia; K elevation suggests adrenal insufficiency/renal failure; glucose for correction"],
["Serum Osmolality", "FIRST BRANCH POINT - determines type of hyponatremia (normal 275-295 mOsm/kg)"],
["Urine Osmolality", "Assess ADH activity and renal diluting ability (<100 = ADH suppressed; >100 = ADH active)"],
["Spot Urine Sodium (UNa)", "Assess renal Na handling (<20 = Na conserving; >20-30 = Na wasting)"],
["TSH", "Rule out hypothyroidism (euvolemic hyponatremia)"],
["Morning Cortisol (8 AM)", "Rule out adrenal insufficiency (cortisol <16.3 mcg/dL warrants cosyntropin stim test)"],
["Serum Lipids / Total Protein", "If normal/elevated Posm - rule out pseudohyponatremia"],
]
labs_t = make_table(
["Test", "Rationale"],
labs_rows,
[70*mm, W-70*mm],
header_bg=C_TEAL
)
story.append(labs_t)
story.append(sp(4))
# ── STEP 1: SERUM OSMOLALITY ──────────────────────────────────────────────────
story.append(section_banner("STEP 1 | SERUM OSMOLALITY - FIRST BRANCH POINT"))
story.append(sp(2))
osm_rows = [
["NORMAL / HIGH\n(≥275 mOsm/kg)",
"Normal Posm\n(275-295)",
"Pseudohyponatremia\n• Marked hyperlipidemia\n• Marked hyperproteinemia\n(e.g., multiple myeloma)"],
["", "High Posm\n(>295)",
"Translocational Hyponatremia\n• Hyperglycemia: Na drops ~1.6 mEq/L per 100 mg/dL rise in glucose\n• Mannitol, sorbitol, glycine (urologic irrigants)"],
["LOW\n(<275 mOsm/kg)\n= TRUE hypotonic\nhyponatremia",
"Proceed to\nStep 2 & 3", "Assess for specific clues (Step 2) then volume status (Step 3)"],
]
sty_osm = [
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("TEXTCOLOR", (0,0), (-1,0), C_WHITE),
("FONTSIZE", (0,0), (-1,-1), 8),
("BACKGROUND", (0,1), (0,1), colors.HexColor("#ffe0b2")),
("BACKGROUND", (0,2), (0,2), colors.HexColor("#ffe0b2")),
("BACKGROUND", (0,3), (0,3), colors.HexColor("#c8e6c9")),
("BACKGROUND", (1,3), (-1,3), colors.HexColor("#c8e6c9")),
("BACKGROUND", (1,1), (-1,1), C_GREY),
("BACKGROUND", (1,2), (-1,2), C_WHITE),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
("SPAN", (0,1), (0,2)),
]
osm_headers = [["Serum Osm Result", "Sub-category", "Diagnosis / Action"]]
osm_data_full = osm_headers + osm_rows
# Use Paragraph cells for proper wrapping
def p(txt, bold=False, color=colors.black, center=False):
fn = "Helvetica-Bold" if bold else "Helvetica"
aln = TA_CENTER if center else TA_LEFT
return Paragraph(txt.replace("\n","<br/>"),
S("_", fontName=fn, fontSize=8, textColor=color,
alignment=aln, leading=11))
osm_table_data = [
[p("Serum Osm Result",True,C_WHITE,True), p("Sub-category",True,C_WHITE,True), p("Diagnosis / Action",True,C_WHITE,True)],
[p("NORMAL or HIGH\n(≥275 mOsm/kg)",True,C_ORANGE), p("Normal (275-295)"), p("Pseudohyponatremia\n• Marked hyperlipidemia\n• Marked hyperproteinemia (e.g., myeloma)")],
["", p("High (>295)"), p("Translocational Hyponatremia\n• Hyperglycemia: Na drops ~1.6 mEq/L per 100 mg/dL glucose rise\n• Mannitol, sorbitol, glycine")],
[p("LOW (<275 mOsm/kg)\nTRUE Hypotonic\nHyponatremia",True,C_GREEN), p("→ Proceed to Steps 2 & 3",True,C_NAVY,True), p("Assess specific clues then volume status")],
]
osm_t = Table(osm_table_data, colWidths=[38*mm, 38*mm, W-76*mm])
osm_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("BACKGROUND", (0,1), (-1,2), colors.HexColor("#fff3e0")),
("BACKGROUND", (0,3), (-1,3), colors.HexColor("#e8f5e9")),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("SPAN", (0,1), (0,2)),
]))
story.append(osm_t)
story.append(sp(4))
# ── STEP 2: SPECIFIC CLUES ────────────────────────────────────────────────────
story.append(section_banner("STEP 2 | SEARCH FOR SPECIFIC DIAGNOSTIC CLUES (Quick Screen)"))
story.append(sp(2))
clue_rows = [
["Thiazide use", "Thiazide-induced hyponatremia (re-evaluate if Na fails to improve after stopping)"],
["Urine Osm < 100 mOsm/kg", "Water intoxication: psychogenic polydipsia, beer potomania, Ecstasy/MDMA, low-solute diet"],
["Urine Osm < 100 + marathon/exercise", "Exercise-associated hyponatremia (EAH) - over-hydration with hypotonic fluids"],
["Marked hyperglycemia", "Hyperglycemic-induced (translocational) - already addressed in Step 1"],
["Markedly elevated creatinine", "Renal failure-associated hyponatremia"],
["Hyperkalemia", "Renal failure or primary adrenal insufficiency (Addison's)"],
["Unexplained hypoglycemia + hyperK", "Primary adrenal insufficiency (Addison's disease)"],
["Normal or elevated serum osmolality", "Pseudohyponatremia (hyperlipidemia, hyperproteinemia)"],
["Recent 'party' / rave attendance", "Consider MDMA/Ecstasy use - causes SIADH + excessive free water intake"],
]
clue_t = make_table(
["Clinical / Lab Finding", "Diagnosis to Consider"],
clue_rows,
[72*mm, W-72*mm],
header_bg=C_TEAL
)
story.append(clue_t)
story.append(sp(2))
story.append(Paragraph(
"<i><b>If none of the above clues present</b> - proceed to Step 3 (volume status assessment).</i>",
S("n2", fontSize=8, textColor=C_NAVY, fontName="Helvetica-Oblique")
))
story.append(sp(4))
# ── STEP 3+4: VOLUME STATUS + UNa/UOsm ───────────────────────────────────────
story.append(section_banner("STEPS 3 & 4 | VOLUME STATUS + URINE SODIUM & OSMOLALITY"))
story.append(sp(2))
# Three-column layout: HYPOVOLEMIC | EUVOLEMIC | HYPERVOLEMIC
def vol_box(title, signs, ddx_items, bg, border_c, title_bg):
rows = [[Paragraph(title, S("vt", fontName="Helvetica-Bold", fontSize=9,
textColor=C_WHITE, alignment=TA_CENTER, leading=12))]]
rows.append([Paragraph("<i>Signs:</i> " + signs,
S("vs", fontSize=7.5, leading=10, textColor=colors.HexColor("#333")))])
for item in ddx_items:
rows.append([Paragraph("• " + item,
S("vi", fontSize=7.5, leading=10))])
t = Table(rows, colWidths=["100%"])
ts = [
("BACKGROUND", (0,0), (-1,0), title_bg),
("BACKGROUND", (0,1), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1.2, border_c),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]
t.setStyle(TableStyle(ts))
return t
hypo_box = vol_box(
"HYPOVOLEMIC",
"Dry mucous membranes, decreased turgor, tachycardia, orthostasis, low JVP",
[
"UNa < 20: Extra-renal losses",
" - Vomiting / diarrhea",
" - Skin losses (burns, sweating)",
" - Third-spacing",
"UNa > 20: Renal Na wasting",
" - Thiazide/loop diuretics",
" - Salt-wasting nephropathy",
" - Primary adrenal insufficiency",
" - Cerebral salt wasting (CSW)",
" - Mineralocorticoid deficiency",
],
colors.HexColor("#fff8e1"), C_ORANGE, C_ORANGE
)
eu_box = vol_box(
"EUVOLEMIC",
"No edema, no orthostasis, no signs of volume depletion or overload",
[
"UOsm < 100: ADH suppressed",
" - Psychogenic polydipsia",
" - Beer potomania",
" - Ecstasy / MDMA",
" - Low-solute diet",
"UOsm > 100: ADH active",
" UNa < 20: Subtle hypovolemia",
" UNa > 30: Check TSH & cortisol",
" TSH elevated → Hypothyroidism",
" Cortisol <16.3 → Adrenal insuff.",
" All excluded → SIADH",
],
colors.HexColor("#e8f5e9"), C_GREEN, C_GREEN
)
hyper_box = vol_box(
"HYPERVOLEMIC",
"Edema, ascites, elevated JVP, S3 gallop, pulmonary crackles",
[
"UNa < 20: Low effective volume",
" - Heart failure",
" - Cirrhosis",
" - Nephrotic syndrome",
"UNa > 20: Renal Na loss",
" - Advanced renal failure",
" (GFR < 5-10 mL/min)",
"",
"Note: HF/cirrhosis - low effective",
" arterial volume despite edema",
" triggers both ADH + Na retention",
],
colors.HexColor("#fce4ec"), C_NAVY, C_NAVY
)
col_w = (W - 4*mm) / 3
vol_table = Table([[hypo_box, eu_box, hyper_box]], colWidths=[col_w]*3,
rowHeights=None)
vol_table.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 2),
("RIGHTPADDING", (0,0), (-1,-1), 2),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(vol_table)
story.append(sp(4))
# ── SIADH DIAGNOSTIC CRITERIA ─────────────────────────────────────────────────
story.append(section_banner("SIADH DIAGNOSTIC CRITERIA (Diagnosis of Exclusion)"))
story.append(sp(2))
siadh_cols = W / 2 - 2*mm
siadh_crit = [
"Hypotonic hyponatremia (Posm < 275 mOsm/kg)",
"Urine osmolality inappropriately elevated (>100 mOsm/kg, usually >200)",
"Urine sodium > 20-40 mEq/L (ongoing renal Na excretion)",
"Clinical euvolemia (no edema, no volume depletion)",
"Normal adrenal, renal, cardiac, hepatic, and thyroid function",
]
siadh_causes_left = [
"<b>CNS:</b> Stroke, trauma, infection, hemorrhage, mass",
"<b>Pulmonary:</b> Pneumonia, TB, respiratory failure, abscess",
"<b>Malignancy:</b> Small cell lung (10-15%), head/neck, lymphoma",
"<b>Drugs:</b> SSRIs, carbamazepine, cyclophosphamide, vincristine, NSAIDs, opioids",
]
siadh_causes_right = [
"<b>Postoperative:</b> Pain/nausea trigger non-osmotic ADH",
"<b>HIV/AIDS</b>",
"<b>Exercise</b> with excessive hypotonic fluid intake",
"<b>Idiopathic</b> (diagnosis of exclusion after all causes excluded)",
]
crit_data = [[Paragraph("• " + c, S("sc", fontSize=8, leading=11))] for c in siadh_crit]
crit_t = Table(crit_data, colWidths=["100%"])
crit_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_TEAL_L),
("BOX", (0,0), (-1,-1), 0.8, C_TEAL),
("INNERGRID", (0,0), (-1,-1), 0.2, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
left_causes = [[Paragraph("• " + c, S("cc", fontSize=7.5, leading=10))] for c in siadh_causes_left]
right_causes = [[Paragraph("• " + c, S("cc2", fontSize=7.5, leading=10))] for c in siadh_causes_right]
lc_t = Table(left_causes, colWidths=["100%"])
lc_t.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 3),
]))
rc_t = Table(right_causes, colWidths=["100%"])
rc_t.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 3),
]))
siadh_hdr_l = Paragraph("<b>All 5 criteria required:</b>",
S("sh", fontName="Helvetica-Bold", fontSize=8.5, textColor=C_TEAL))
siadh_hdr_c = Paragraph("<b>Common Causes (Left):</b>",
S("sh2", fontName="Helvetica-Bold", fontSize=8.5, textColor=C_NAVY))
siadh_hdr_r = Paragraph("<b>Common Causes (Right):</b>",
S("sh3", fontName="Helvetica-Bold", fontSize=8.5, textColor=C_NAVY))
siadh_layout = Table(
[[siadh_hdr_l, siadh_hdr_c, siadh_hdr_r],
[crit_t, lc_t, rc_t]],
colWidths=[W*0.38, W*0.31, W*0.31]
)
siadh_layout.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 3),
("RIGHTPADDING", (0,0), (-1,-1), 3),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(siadh_layout)
story.append(sp(4))
# ── SUMMARY TABLE ─────────────────────────────────────────────────────────────
story.append(section_banner("DIAGNOSTIC SUMMARY TABLE | UNa + Urine Osm by Category"))
story.append(sp(2))
sum_data = [
[p("Volume Status",True,C_WHITE,True), p("UNa (mEq/L)",True,C_WHITE,True),
p("Urine Osm",True,C_WHITE,True), p("Likely Etiology",True,C_WHITE,True)],
[p("Hypovolemic"), p("< 20",True,C_GREEN), p("> 100"), p("GI/skin losses, third-spacing, burns")],
[p("Hypovolemic"), p("> 20",True,C_RED), p("> 100"), p("Diuretics, CSW, salt-wasting, adrenal insufficiency")],
[p("Hypervolemic"), p("< 20",True,C_GREEN), p("> 100"), p("Heart failure, cirrhosis, nephrotic syndrome")],
[p("Hypervolemic"), p("> 20",True,C_RED), p("Variable"),p("Advanced renal failure (GFR < 5-10 mL/min)")],
[p("Euvolemic"), p("< 20",True,C_GREEN), p("< 100",True,C_ORANGE), p("Primary polydipsia, beer potomania, Ecstasy, low-solute diet")],
[p("Euvolemic"), p("> 40",True,C_RED), p("> 100",True,C_RED), p("SIADH, hypothyroidism, adrenal insufficiency")],
]
sum_t = Table(sum_data, colWidths=[32*mm, 22*mm, 22*mm, W-76*mm])
sum_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GREY]),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(sum_t)
story.append(sp(2))
story.append(Paragraph(
"<i>Rule of thumb: UNa < 10 mEq/L found almost exclusively in edematous syndromes (HF, cirrhosis) and GI losses with vomiting/diarrhea.</i>",
S("note2", fontSize=7.5, textColor=colors.HexColor("#555"), fontName="Helvetica-Oblique")
))
story.append(sp(4))
# ── CORRECTION LIMITS ─────────────────────────────────────────────────────────
story.append(section_banner("CORRECTION LIMITS & TREATMENT (CRITICAL - Risk of ODS)", bg=colors.HexColor("#8b1a1a")))
story.append(sp(2))
corr_rows = [
["Severe symptoms\n(seizures, obtundation,\ncoma, respiratory arrest)",
"3% NaCl 100 mL IV bolus over ~10 min\nRepeat x2 PRN (max 3 boluses)\nTarget: raise Na by 4-6 mEq/L to break symptoms",
"No absolute max in acute phase;\nstop once symptoms resolve;\nthen resume standard limits"],
["Acute hyponatremia\n(onset < 24-48 h)",
"Correct underlying cause\n3% NaCl if symptomatic",
"Up to 1-2 mEq/L/hr initially;\nno strict 24h cap in truly acute settings"],
["Chronic / unknown\nduration - LOW risk\n(no liver disease,\nno alcoholism,\nno malnutrition)",
"Fluid restriction (SIADH)\nAddress underlying cause\nLoop diuretics + saline (hypovolemic)\nTolvaptan (SIADH, selected cases)",
"≤ 10-12 mEq/L per 24 hours\n≤ 18 mEq/L per 48 hours"],
["Chronic / unknown\nduration - HIGH risk\n(cirrhosis, alcoholism,\nmalnutrition, K < 3,\nNa < 105 mEq/L)",
"Same as above but more cautious;\nconsider prophylactic desmopressin\nto prevent overcorrection",
"≤ 6-8 mEq/L per 24 hours\n≤ 14-18 mEq/L per 48 hours"],
]
def corr_p(txt, bold=False, color=colors.black):
fn = "Helvetica-Bold" if bold else "Helvetica"
return Paragraph(txt.replace("\n","<br/>"), S("cp", fontName=fn, fontSize=8,
textColor=color, leading=11))
corr_data = [
[corr_p("Clinical Scenario", True, C_WHITE),
corr_p("Treatment Approach", True, C_WHITE),
corr_p("Correction Rate Limit", True, C_WHITE)],
[corr_p("SEVERE SYMPTOMS\n(seizures, obtundation,\ncoma, cardiorespiratory\narrest)", True, C_RED),
corr_p("3% NaCl 100 mL IV bolus over ~10-15 min\nMay repeat x2 (max 3 boluses, 300 mL)\nTarget: raise Na by 4-6 mEq/L to abort symptoms"),
corr_p("No hard cap during acute rescue;\nstop once symptoms resolve;\nthen resume standard rate limits", True, C_RED)],
[corr_p("ACUTE\n(onset < 24-48 h,\nclearly documented)"),
corr_p("Treat underlying cause\n3% NaCl if symptomatic"),
corr_p("Up to 1-2 mEq/L/hr initially;\ncorrect to ~125 mEq/L then slow")],
[corr_p("CHRONIC or UNKNOWN\nLOW risk patient"),
corr_p("Fluid restriction (SIADH)\nCausal treatment\nLoop diuretics + NS (hypovolemic)\nTolvaptan (selected SIADH cases)"),
corr_p("≤ 10-12 mEq/L / 24 h\n≤ 18 mEq/L / 48 h", True, C_NAVY)],
[corr_p("CHRONIC or UNKNOWN\nHIGH risk patient\n(cirrhosis, alcoholism,\nmalnutrition, K+ < 3,\nNa+ < 105 mEq/L)", True, C_ORANGE),
corr_p("Same as above\nConsider prophylactic desmopressin\n(1-2 mcg IV/SC q6-8h) to prevent\naccidental overcorrection"),
corr_p("≤ 6-8 mEq/L / 24 h\n≤ 14 mEq/L / 48 h", True, C_RED)],
]
corr_t = Table(corr_data, colWidths=[42*mm, W*0.42, W*0.32])
corr_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#8b1a1a")),
("BACKGROUND", (0,1), (-1,1), colors.HexColor("#fdecea")),
("BACKGROUND", (0,2), (-1,2), colors.HexColor("#fff9f0")),
("BACKGROUND", (0,3), (-1,3), C_GREY),
("BACKGROUND", (0,4), (-1,4), colors.HexColor("#fff3cd")),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(corr_t)
story.append(sp(3))
# ODS warning box
ods_data = [[
Paragraph(
"<b>⚠ OSMOTIC DEMYELINATION SYNDROME (ODS) WARNING</b><br/>"
"ODS (formerly central pontine myelinolysis) is caused by overly RAPID correction of chronic hyponatremia. "
"Risk is highest with Na <105, liver disease, alcoholism, and malnutrition. "
"Symptoms (dysarthria, dysphagia, quadriplegia, locked-in syndrome) appear 2-6 days after correction. "
"If overcorrection occurs: stop hypertonic saline, give desmopressin (DDAVP) 1-2 mcg IV + free water to re-lower Na.",
S("ods", fontName="Helvetica", fontSize=8, textColor=colors.HexColor("#5d0000"),
leading=12)
)
]]
ods_t = Table(ods_data, colWidths=[W])
ods_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#fdecea")),
("BOX", (0,0), (-1,-1), 1.5, C_RED),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(ods_t)
story.append(sp(3))
# ── OVERCORRECTION MANAGEMENT ─────────────────────────────────────────────────
story.append(section_banner("IF OVERCORRECTION OCCURS | Rescue Protocol", bg=C_TEAL))
story.append(sp(2))
over_rows = [
["Stop all hypertonic saline and any high-Na infusions immediately"],
["Give Desmopressin (DDAVP) 1-2 mcg IV or SC every 6-8 hours"],
["Give free water: D5W 3 mL/kg IV or oral water to re-lower serum Na"],
["Target: bring Na back down by ~2 mEq/L to within correction limits"],
["Monitor serum Na every 2-4 hours until stable"],
["Consult nephrology for ongoing management"],
]
over_data = [[Paragraph("• " + r[0], S("or", fontSize=8.5, leading=12))] for r in over_rows]
over_t = Table(over_data, colWidths=[W])
over_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_TEAL_L),
("INNERGRID", (0,0), (-1,-1), 0.2, C_BORDER),
("BOX", (0,0), (-1,-1), 0.8, C_TEAL),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(over_t)
story.append(sp(3))
# ── FOOTER ─────────────────────────────────────────────────────────────────────
story.append(hr(C_NAVY, 1))
story.append(Paragraph(
"Sources: Harrison's Principles of Internal Medicine 22e | "
"Tintinalli's Emergency Medicine | "
"Symptom to Diagnosis 4e | "
"Brenner & Rector's The Kidney | "
"ROSEN's Emergency Medicine | "
"Generated by Orris Clinical Reference",
S("foot", fontSize=7, textColor=colors.HexColor("#888"), alignment=TA_CENTER)
))
# ── BUILD ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved: {OUTPUT}")
cd /home/daytona/workspace/hyponatremia-pdf && python3 build_pdf.py 2>&1
ls -lh /home/daytona/workspace/hyponatremia-pdf/Hyponatremia_Quick_Reference.pdf
Hyponatremia Quick Reference
PDF Document · PDF