skyan1002's picture
Update to exact intensity-area decomposition: I/A/K_IA components, log-ratio D, ln(x)~x-1 note, corrected region
db7e822 verified
Raw
History Blame Contribute Delete
50.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Why Averages Lie About Rain — slides</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'\\(',right:'\\)',display:false}]});"></script>
<script src="assets/case_data.js"></script>
<script src="assets/sat_data.js"></script>
<style>
:root{
--ink:#1a2233; --soft:#5c6779; --bg:#f7f8fb; --card:#fff;
--orange:#D55E00; --blue:#0072B2; --green:#009E73; --purple:#7570B3;
--accent:#2456e6; --warnbg:#fff6e8;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
line-height:1.55;font-size:16.5px;overflow:hidden}
#deck{position:fixed;inset:0}
.slide{position:absolute;inset:0;padding:46px 7vw 86px;overflow-y:auto;
opacity:0;transform:translateX(46px);pointer-events:none;
transition:opacity .35s ease,transform .35s ease}
.slide.active{opacity:1;transform:none;pointer-events:auto}
.slide.prev{transform:translateX(-46px)}
.inner{max-width:980px;margin:0 auto}
.kicker{font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;
color:var(--accent);font-weight:700}
h1{font-size:2.5rem;line-height:1.2;margin:.2em 0}
h2{font-size:1.7rem;margin:.15em 0 .45em;color:#10204d}
p{margin:.6em 0}
.soft{color:var(--soft)}
.big-eq{text-align:center;font-size:1.18rem;margin:16px 0;padding:13px;
background:#eef3ff;border-radius:12px}
.hl{background:var(--warnbg);border-radius:10px;padding:12px 18px;margin:14px 0;
border-left:4px solid var(--orange)}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
gap:13px;margin:16px 0}
.card{border-radius:11px;padding:13px 15px;border-top:4px solid #ccc;background:#fff;
box-shadow:0 1px 4px rgba(20,30,60,.07)}
.card h3{margin:0 0 3px;font-size:1rem}
.card .sym{font-size:1.35rem;font-weight:800}
.card p{font-size:.87rem;margin:.25em 0;color:var(--soft)}
.c-T{border-color:#888}.c-r{border-color:var(--orange)}
.c-A{border-color:var(--blue)}.c-O{border-color:var(--green)}
.toy-table{width:100%;border-collapse:collapse;margin:12px 0;font-size:.95rem;background:#fff}
.toy-table th,.toy-table td{border:1px solid #e3e7f0;padding:6px 10px;text-align:center}
.toy-table th{background:#eef2fb}
.btnrow{display:flex;gap:9px;flex-wrap:wrap;margin:10px 0;align-items:center}
button{border:1.5px solid var(--accent);background:#fff;color:var(--accent);
border-radius:999px;padding:7px 17px;font-size:.9rem;font-weight:600;
cursor:pointer;transition:all .15s}
button:hover{background:var(--accent);color:#fff}
button.active{background:var(--accent);color:#fff}
svg.fig{width:100%;height:auto;display:block;background:#fff;
border:1px solid #e7eaf2;border-radius:12px}
.readout{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));
gap:10px;margin:12px 0}
.stat{background:#fff;border:1px solid #e7eaf2;border-radius:10px;padding:8px 12px;text-align:center}
.stat .v{font-size:1.3rem;font-weight:800}
.stat .l{font-size:.74rem;color:var(--soft)}
figure{margin:14px 0;text-align:center}
figure img{max-width:100%;max-height:56vh;border-radius:12px;border:1px solid #e7eaf2}
figcaption{font-size:.84rem;color:var(--soft);margin-top:7px;text-align:left}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start}
@media(max-width:760px){.grid2{grid-template-columns:1fr}h1{font-size:1.9rem}}
/* expandable derivation */
details.derive{background:#fff;border:1px solid #e7eaf2;border-radius:11px;
margin:14px 0;overflow:hidden}
details.derive summary{cursor:pointer;padding:11px 18px;font-weight:600;
color:var(--accent);list-style:none;user-select:none}
details.derive summary::-webkit-details-marker{display:none}
details.derive summary::before{content:"▸ "}
details.derive[open] summary::before{content:"▾ "}
details.derive summary:hover{background:#f4f7ff}
.dbody{padding:8px 20px 14px;border-top:1px solid #edf0f7}
.dstep{display:flex;gap:14px;margin:10px 0;align-items:flex-start}
.dnum{flex:0 0 26px;height:26px;border-radius:50%;background:#eef3ff;
color:var(--accent);font-weight:800;display:flex;align-items:center;
justify-content:center;font-size:.85rem;margin-top:2px}
.dstep .katex-display{margin:7px 0}
.dstep>div:last-child{flex:1;font-size:.95rem}
/* navigation chrome */
#nav{position:fixed;bottom:0;left:0;right:0;height:64px;display:flex;
align-items:center;justify-content:center;gap:18px;
background:linear-gradient(transparent,rgba(247,248,251,.95) 40%);z-index:50}
.navbtn{width:44px;height:44px;border-radius:50%;font-size:1.2rem;display:flex;
align-items:center;justify-content:center;padding:0}
#dots{display:flex;gap:7px}
.dot{width:9px;height:9px;border-radius:50%;background:#c7cedd;cursor:pointer;
transition:all .2s}
.dot.on{background:var(--accent);transform:scale(1.35)}
#counter{font-size:.85rem;color:var(--soft);min-width:54px;text-align:center}
#bar{position:fixed;top:0;left:0;height:4px;background:var(--accent);z-index:60;
transition:width .3s}
#tip{position:fixed;display:none;background:#10204d;color:#fff;
padding:4px 10px;border-radius:7px;font-size:.8rem;pointer-events:none;
white-space:nowrap;z-index:99}
.titleslide{display:flex;flex-direction:column;justify-content:center;
text-align:center;background:linear-gradient(140deg,#10204d 0%,#2456e6 100%);
color:#fff}
.titleslide h1{font-size:3rem}
.titleslide .kicker{color:#9db8ff}
.titleslide p{max-width:640px;margin:10px auto;font-size:1.12rem;opacity:.94}
.keyhint{position:fixed;top:14px;right:18px;font-size:.75rem;color:var(--soft);
background:#ffffffcc;border-radius:8px;padding:4px 10px;z-index:50}
ol li,ul li{margin:.45em 0}
input[type=range]{width:100%;accent-color:var(--accent)}
.badge{display:inline-block;border-radius:6px;padding:1px 9px;color:#fff;
font-weight:700;font-size:.85rem}
a{color:var(--accent)}
</style>
</head>
<body>
<div id="bar"></div>
<div id="tip"></div>
<div class="keyhint">← → arrow keys&nbsp;·&nbsp;swipe</div>
<div id="deck">
<!-- 1 ─ TITLE -->
<section class="slide titleslide">
<div class="kicker">An interactive explainer</div>
<h1>Why Averages Lie About Rain</h1>
<p style="font-size:1.3rem;font-weight:300">…and the exact formula that catches them</p>
<p>A storm has an average strength and an average size. Multiply them and you should
get the total rain — right? Not quite. The missing piece measures something real
about storms, and it can expose how satellites get rain wrong while looking right.</p>
<p style="opacity:.75;font-size:.95rem">Built on real NOAA weather-radar data · 11 slides · use → to begin</p>
</section>
<!-- 2 ─ PUZZLE -->
<section class="slide"><div class="inner">
<div class="kicker">1 · The puzzle</div>
<h2>One storm, two answers</h2>
<p>Watch a small storm for three half-hour snapshots, recording how
<b style="color:var(--orange)">hard</b> it rains (mm/hr) and how
<b style="color:var(--blue)">big</b> the rainy patch is (km²):</p>
<table class="toy-table">
<tr><th>snapshot</th><th style="color:var(--orange)">rate</th>
<th style="color:var(--blue)">area</th><th>rain made = rate × area × ½h</th></tr>
<tr><td>1 — growing</td><td>2</td><td>100</td><td>100</td></tr>
<tr><td>2 — peak</td><td>6</td><td>300</td><td><b>900</b></td></tr>
<tr><td>3 — fading</td><td>1</td><td>80</td><td>40</td></tr>
</table>
<div class="grid2">
<div class="hl" style="border-color:var(--green);background:#ecf9f4">
<b>Way 1 — add what actually fell:</b><br>100 + 900 + 40 = <b>1040</b> ✓ the truth</div>
<div class="hl">
<b>Way 2 — multiply the averages:</b><br>avg rate 3 × avg area 160 × 1.5 h = <b>720</b></div>
</div>
<p><b>A third of the water vanished</b> — with perfect data and no rounding.
Multiplying averages genuinely loses information. <i>What</i> information?</p>
</div></section>
<!-- 3 ─ MATH FACT -->
<section class="slide"><div class="inner">
<div class="kicker">2 · The math fact</div>
<h2>The average of a product ≠ the product of averages</h2>
<div class="big-eq">$$\overline{a\,b} \;=\; \bar a \cdot \bar b \;+\; \mathrm{cov}(a,b)$$</div>
<p style="margin-top:0">The correction — the <b>covariance</b> — is positive when a and b are big
<b>at the same time</b>, zero when they ignore each other, negative when they oppose. Try it:</p>
<div class="btnrow">
<button id="bsync" class="active" onclick="setToy('sync')">peaks together</button>
<button id="bind" onclick="setToy('ind')">independent</button>
<button id="bopp" onclick="setToy('opp')">opposites</button>
</div>
<svg id="toySvg" viewBox="0 0 940 210" class="fig"></svg>
<div class="readout">
<div class="stat"><div class="v" id="toyTrue"></div><div class="l">true total Σ a·b</div></div>
<div class="stat"><div class="v" id="toyNaive"></div><div class="l">averages × n</div></div>
<div class="stat"><div class="v" id="toyGap"></div><div class="l">the gap</div></div>
<div class="stat"><div class="v" id="toyO"></div><div class="l">true ÷ naive</div></div>
</div>
</div></section>
<!-- 4 ─ THE FORMULA -->
<section class="slide"><div class="inner">
<div class="kicker">3 · The formula</div>
<h2>An exact recipe for total storm rain</h2>
<p>Apply that one fact to a storm, and the total rain volume <b>V</b> splits —
<b>exactly</b> — into meaningful factors:</p>
<div class="big-eq">$$V \;=\; \Delta t \cdot T \;\cdot\; \bar I \;\cdot\; \bar A \;\cdot\; K_{IA}$$</div>
<details class="derive">
<summary>Where does this come from? Open the four-line derivation</summary>
<div class="dbody">
<p class="soft" style="margin-top:4px">Setup: watch for <b>T</b> snapshots, each lasting
Δt. At snapshot <i>t</i> the rain falls at intensity
<b style="color:var(--orange)">I<sub>t</sub></b> over an active area of
<b style="color:var(--blue)">A<sub>t</sub></b>.</p>
<div class="dstep"><div class="dnum">1</div><div>
<b>Add up what fell.</b> Each snapshot contributes intensity × area × Δt,
and a sum is just “how many” × “the average”:
$$V=\Delta t\sum_{t=1}^{T} I_t A_t \;=\; \Delta t\cdot T\cdot\overline{I A}$$</div></div>
<div class="dstep"><div class="dnum">2</div><div>
<b>Use the math fact</b> from the previous slide on that average-of-a-product:
$$\overline{I A}\;=\;\bar I\,\bar A\;+\;\mathrm{cov}(I,A)$$</div></div>
<div class="dstep"><div class="dnum">3</div><div>
<b>Factor out</b> \(\bar I\,\bar A\) and give the bracket a name — the
<b>intensity–area coupling factor</b>:
$$\overline{I A}\;=\;\bar I\,\bar A\,\underbrace{\left(1+\frac{\mathrm{cov}(I,A)}{\bar I\,\bar A}\right)}_{\textstyle K_{IA}}$$</div></div>
<div class="dstep"><div class="dnum">4</div><div>
<b>Put it back.</b> Every line above is an equality — no approximation anywhere:
$$V\;=\;\Delta t\cdot T\cdot \bar I\cdot \bar A\cdot K_{IA}$$</div></div>
<div class="dstep"><div class="dnum"></div><div>
<b>Bonus — the anatomy of \(K_{IA}\).</b> A covariance always splits into
correlation × the two spreads, \(\mathrm{cov}(I,A)=\rho\,\sigma_I\,\sigma_A\).
Divide by \(\bar I\,\bar A\):
$$K_{IA}\;=\;1+\rho\cdot\frac{\sigma_I}{\bar I}\cdot\frac{\sigma_A}{\bar A}
\;=\;1+\rho\cdot CV_I\cdot CV_A$$
synchrony × intensity-swing × size-swing — the three knobs you can play with
two slides ahead.</div></div>
</div>
</details>
<div class="cards">
<div class="card c-T"><div class="sym">Δt·T</div><h3>how long</h3><p>the storm’s duration</p></div>
<div class="card c-r"><div class="sym" style="color:var(--orange)">Ī</div><h3>how strong</h3><p>mean intensity</p></div>
<div class="card c-A"><div class="sym" style="color:var(--blue)">Ā</div><h3>how big</h3><p>mean active area</p></div>
<div class="card c-O"><div class="sym" style="color:var(--green)">K<sub>IA</sub></div><h3>how coupled</h3>
<p>does it rain hardest <b>when</b> it is biggest?</p></div>
</div>
<p><b>K<sub>IA</sub></b> is the covariance from the last slide, dressed as a multiplier:
K<sub>IA</sub>&nbsp;&gt;&nbsp;1 amplifies, K<sub>IA</sub>&nbsp;=&nbsp;1 neutral, K<sub>IA</sub>&nbsp;&lt;&nbsp;1 reduces.
Our toy storm: K<sub>IA</sub> = 1040 / 720 ≈ <b>1.44</b>.</p>
<div class="hl" style="border-color:var(--accent);background:#eef3ff">
<b>Why “exact” matters:</b> this is an identity, like 12 = 3 × 4 — not a fitted score.
Multiply the factors back and you recover the true total to computer precision,
for any storm, any region, any time window. Every error you find is accountable.</div>
</div></section>
<!-- 5 ─ O EXPLAINED -->
<section class="slide"><div class="inner">
<div class="kicker">4 · Coupling, dissected</div>
<h2>Shape a storm — watch \(K_{IA}\) respond</h2>
<div class="big-eq" style="margin-top:4px">$$K_{IA} = 1 + \rho \cdot CV_I \cdot CV_A$$</div>
<details class="derive">
<summary>Where do the three knobs come from? Open the derivation</summary>
<div class="dbody">
<div class="dstep"><div class="dnum">1</div><div>
<b>Start where the last slide ended.</b> \(K_{IA}\) is one plus the covariance,
measured relative to the averages it corrects:
$$K_{IA}\;=\;1+\frac{\mathrm{cov}(I,A)}{\bar I\,\bar A}$$</div></div>
<div class="dstep"><div class="dnum">2</div><div>
<b>Split the covariance.</b> By the definition of correlation,
\(\rho=\mathrm{cov}(I,A)/(\sigma_I\,\sigma_A)\), every covariance is
“how in-step” × the two spreads:
$$\mathrm{cov}(I,A)\;=\;\rho\cdot\sigma_I\cdot\sigma_A$$</div></div>
<div class="dstep"><div class="dnum">3</div><div>
<b>Pair each spread with its own average.</b> Dividing by
\(\bar I\,\bar A\) turns the absolute spreads into <i>relative</i> swings —
the coefficients of variation:
$$\frac{\mathrm{cov}(I,A)}{\bar I\,\bar A}
\;=\;\rho\cdot\underbrace{\frac{\sigma_I}{\bar I}}_{\textstyle CV_I}
\cdot\underbrace{\frac{\sigma_A}{\bar A}}_{\textstyle CV_A}$$</div></div>
<div class="dstep"><div class="dnum">4</div><div>
<b>Read off the result.</b>
$$K_{IA}\;=\;1+\rho\cdot CV_I\cdot CV_A$$
And the formula polices itself: ρ can never leave −1…+1, and if either
swing is zero the product dies — so \(K_{IA}=1\) unless <i>all three</i> knobs
are turned. That is exactly what the presets below demonstrate.</div></div>
</div>
</details>
<p style="margin-top:0"><b>ρ</b>: do intensity &amp; size rise together? ·
<b>CV<sub>I</sub></b>: does intensity swing? · <b>CV<sub>A</sub></b>: does size swing?
You need <b>all three</b>.</p>
<div class="btnrow">
<button id="p0" class="active" onclick="setShape(0)">grow &amp; fade together</button>
<button id="p1" onclick="setShape(1)">steady drizzle</button>
<button id="p2" onclick="setShape(2)">strong first, big later</button>
<button id="p3" onclick="setShape(3)">two pulses</button>
</div>
<svg id="shapeSvg" viewBox="0 0 940 200" class="fig"></svg>
<div class="readout">
<div class="stat"><div class="v" id="sRho"></div><div class="l">ρ synchrony</div></div>
<div class="stat"><div class="v" id="sCVr"></div><div class="l">CV<sub>I</sub></div></div>
<div class="stat"><div class="v" id="sCVA"></div><div class="l">CV<sub>A</sub></div></div>
<div class="stat"><div class="v" id="sO"></div><div class="l">K<sub>IA</sub></div></div>
</div>
<p class="soft" id="shapeLesson" style="min-height:2.4em"></p>
</div></section>
<!-- 6 ─ STORM PLAYER -->
<section class="slide"><div class="inner">
<div class="kicker">5 · Real weather, live</div>
<h2>Replay four real storms</h2>
<p style="margin-top:0">Genuine NOAA radar measurements over the southeastern US, every 30 minutes.
Press play and watch the decomposition assemble itself — hover any dot for its exact reading.</p>
<div class="btnrow">
<button id="cb0" class="active" onclick="setCase('organized')">organized · K 1.45</button>
<button id="cb1" onclick="setCase('twopulse')">two pulses · K 1.55</button>
<button id="cb2" onclick="setCase('scattered')">popcorn · K 1.00</button>
<button id="cb3" onclick="setCase('anti')">weakening · K 0.90</button>
</div>
<svg id="playerSvg" viewBox="0 0 940 280" class="fig"></svg>
<div class="btnrow">
<button class="navbtn" id="playBtn" onclick="togglePlay()" style="width:40px;height:40px"></button>
<input type="range" id="scrub" min="0" max="49" value="0" style="flex:1"
oninput="setFrame(+this.value)">
</div>
<div class="readout">
<div class="stat"><div class="v" id="cName" style="font-size:.95rem"></div><div class="l" id="cDate"></div></div>
<div class="stat"><div class="v" id="cNow" style="font-size:.95rem"></div><div class="l">right now</div></div>
<div class="stat"><div class="v" id="cRho"></div><div class="l">ρ (whole storm)</div></div>
<div class="stat"><div class="v" id="cO"></div><div class="l">K<sub>IA</sub> (whole storm)</div></div>
</div>
<p class="soft" id="caseLesson" style="min-height:2.2em"></p>
</div></section>
<!-- 7 ─ SHAPE GALLERY -->
<section class="slide"><div class="inner">
<div class="kicker">6 · The shape of coupling</div>
<h2>You can read \(K_{IA}\) straight off the dots</h2>
<p>Same four storms, each point one snapshot — size vs intensity, both scaled by
their own averages, colored by storm lifetime. <b>The shape of the cloud is the
diagnosis</b> — hover any dot to interrogate a snapshot.</p>
<svg id="gallerySvg" viewBox="0 0 940 270" class="fig"></svg>
<p class="soft">A falling staircase = anti-coupled (fierce while small).
A shapeless blob = independent. A rising diagonal = coupled — the
steeper and tighter, the bigger \(K_{IA}\). No formula needed to <i>see</i> it;
the formula just makes it a number.</p>
</div></section>
<!-- 8 ─ RADAR VIEW -->
<section class="slide"><div class="inner">
<div class="kicker">7 · What the radar saw</div>
<h2>The organized storm, frame by frame</h2>
<figure>
<img src="assets/fig1p_organized.gif" alt="Animated radar storm with live decomposition">
<figcaption><b>November 24–25, 2022.</b> Left: the radar map every 30 minutes.
Right: intensity (orange) and area (blue) climbing and falling <b>together</b>, while
the dots assemble the rising diagonal you just learned to read — \(K_{IA}\) = 1.45,
a 45% intensity–area coupling bonus over the product of the means.</figcaption>
</figure>
<p class="soft" style="font-size:.85rem">Full report cards for all four storms:
<a href="assets/fig1p_caseC.png" target="_blank">organized</a> ·
<a href="assets/fig1p_caseD.png" target="_blank">two pulses</a> ·
<a href="assets/fig1p_caseB.png" target="_blank">popcorn</a> ·
<a href="assets/fig1p_caseA.png" target="_blank">weakening</a></p>
</div></section>
<!-- 9 ─ SATELLITE -->
<section class="slide"><div class="inner">
<div class="kicker">8 · Why it matters</div>
<h2>Catching a satellite hiding its mistakes</h2>
<p style="margin-top:0">NASA’s <b>IMERG</b> satellite product estimates rain for the whole planet.
Over our radar-covered region its 2-year rain <b>total</b> was off by just
<b>+1.2%</b>. Sounds perfect — until you decompose it. The satellite is built
in three stages; pick one:</p>
<div class="hl" style="border-color:var(--accent);background:#eef3ff">
<b>Reading the differences with log-ratios.</b> For any factor \(X\) we compare a
product \(P\) to the radar reference \(R\) with the natural-log ratio
\(D_X=\ln\!\big(X^{(P)}/X^{(R)}\big)\). Because the recipe <i>multiplies</i>, the logs
simply <b>add</b>:&nbsp; \(D_V=D_I+D_A+D_K\) — the total-volume difference is exactly the
sum of the intensity, area, and coupling contributions. \(D_X=0\) means “matches the
radar”; the exact relative difference is \(e^{D_X}-1\). And because
\(\ln x \approx x-1\) when \(x\) is near 1, a small \(D_X\) is essentially the fractional
(percent) difference — so these log-contributions read almost like additive percentages
whenever the biases are modest.</div>
<div class="btnrow">
<button id="sb2" class="active" onclick="setSat('Cal')">final product</button>
<button id="sb1" onclick="setSat('Uncal')">before gauge correction</button>
<button id="sb0" onclick="setSat('IR')">infrared-only (first stage)</button>
<span style="flex:1"></span>
<button id="vb0" class="active" onclick="setSatView('summary')">2-year accounting</button>
<button id="vb1" onclick="setSatView('monthly')">month by month</button>
</div>
<div class="btnrow" id="compRow" style="display:none">
<span class="soft" style="font-size:.85rem">component:</span>
<button id="kbI" onclick="setSatComp('I')">intensity Ī</button>
<button id="kbA" class="active" onclick="setSatComp('A')">area Ā</button>
<button id="kbK" onclick="setSatComp('K')">coupling K<sub>IA</sub></button>
<button id="kbV" onclick="setSatComp('V')">total V</button>
</div>
<svg id="satSvg" viewBox="0 0 940 260" class="fig"></svg>
<p class="soft" id="satLesson" style="min-height:3.2em"></p>
</div></section>
<!-- 10 ─ TAKEAWAY -->
<section class="slide"><div class="inner">
<div class="kicker">9 · Take-away</div>
<h2>Three things to remember</h2>
<ol style="font-size:1.12rem">
<li><b>Averages multiply wrong</b> whenever two things vary together.
The gap is covariance — information, not noise.</li>
<li><b>Total rain = duration × intensity × area × coupling. Exactly.</b>
Intensity–area coupling asks one question: does it rain hardest when it rains biggest?</li>
<li><b>A perfect total can hide big errors.</b> Exact decomposition finds
compensating mistakes — and in logs they add, \(D_V=D_I+D_A+D_K\), so the
cancellation is laid bare — in satellites, climate models, or anywhere a
product of averages stands in for reality.</li>
</ol>
<div class="hl" style="border-color:var(--green);background:#ecf9f4">
The same identity works for any “episodes that have an intensity and
a size”: heat waves, wildfires, floods, even customer traffic in a shop.
Wherever totals are built from intensity × extent, intensity–area coupling is hiding inside.</div>
</div></section>
<!-- 11 ─ CREDITS -->
<section class="slide titleslide">
<h1 style="font-size:2rem">Thanks for scrolling the storm</h1>
<p>Method: exact intensity–area decomposition of precipitation volume<br>
\(V=\Delta t\,T\,\bar I\,\bar A\,K_{IA}\), &nbsp; \(K_{IA} = 1+\rho\,CV_I\,CV_A\); &nbsp;
differences \(D_X=\ln(X^{(P)}/X^{(R)})\), \(D_V=D_I+D_A+D_K\) — S. Yan, 2026</p>
<p style="opacity:.8;font-size:.95rem">Yan, S.: <i>Diagnosing Satellite Precipitation Differences with an
Exact Intensity–Area Decomposition</i> (JHM, in prep.)<br>
Data: NOAA MRMS gauge-corrected radar QPE · NASA GPM IMERG V07B<br>
2022–2023, southeastern US study region (29.85–34.75°N, 86.65–81.75°W)<br>
All storm replays use the real measured series. Page assembled with Claude Code.</p>
</section>
</div>
<div id="nav">
<button class="navbtn" onclick="go(-1)"></button>
<div id="dots"></div>
<div id="counter">1 / ?</div>
<button class="navbtn" onclick="go(1)"></button>
</div>
<script>
"use strict";
/* ═══ deck navigation ═══ */
const slides=[...document.querySelectorAll(".slide")];
let cur=0;
const dots=document.getElementById("dots");
slides.forEach((_,i)=>{const d=document.createElement("div");d.className="dot";
d.onclick=()=>show(i);dots.appendChild(d);});
function show(i){
cur=Math.max(0,Math.min(slides.length-1,i));
slides.forEach((s,k)=>{s.classList.toggle("active",k===cur);
s.classList.toggle("prev",k<cur);});
[...dots.children].forEach((d,k)=>d.classList.toggle("on",k===cur));
document.getElementById("counter").textContent=(cur+1)+" / "+slides.length;
document.getElementById("bar").style.width=((cur+1)/slides.length*100)+"%";
tipHide();
if(playing) togglePlay();
}
function go(d){show(cur+d);}
document.addEventListener("keydown",e=>{
if(e.key==="ArrowRight"||e.key==="PageDown"||e.key===" ") {go(1);e.preventDefault();}
if(e.key==="ArrowLeft"||e.key==="PageUp") {go(-1);e.preventDefault();}
});
let tx=null;
document.addEventListener("touchstart",e=>tx=e.touches[0].clientX,{passive:true});
document.addEventListener("touchend",e=>{
if(tx===null)return;
const dx=e.changedTouches[0].clientX-tx;
if(Math.abs(dx)>60) go(dx<0?1:-1);
tx=null;},{passive:true});
/* ═══ helpers ═══ */
const mean=v=>v.reduce((s,x)=>s+x,0)/v.length;
const sd=v=>{const m=mean(v);return Math.sqrt(mean(v.map(x=>(x-m)**2)));};
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function decomp(r,A){
const rm=mean(r),Am=mean(A);
const cov=mean(r.map((x,i)=>(x-rm)*(A[i]-Am)));
const sr=sd(r),sA=sd(A);
const rho=(sr>1e-9&&sA>1e-9)?cov/(sr*sA):0;
return {rm,Am,rho,cvr:sr/rm,cva:sA/Am,O:1+cov/(rm*Am)};
}
const vCol=t=>{ // viridis-ish
const c=[[68,1,84],[59,82,139],[33,145,140],[94,201,98],[253,231,37]];
const x=Math.max(0,Math.min(1,t))*(c.length-1),i=Math.floor(x),f=x-i;
const a=c[i],b=c[Math.min(i+1,c.length-1)];
return `rgb(${a.map((v,k)=>Math.round(v+(b[k]-v)*f)).join(",")})`;
};
/* ═══ SVG + tooltip plumbing (shared by every interactive figure) ═══ */
const SVGNS="http://www.w3.org/2000/svg";
const svgEl=(tag,at)=>{const e=document.createElementNS(SVGNS,tag);
for(const k in at)e.setAttribute(k,at[k]);return e;};
function ensure(svg,tag,id,at,trans){
let e=svg.querySelector("#"+id);
if(!e){e=svgEl(tag,Object.assign({id},at||{}));
if(trans)e.style.transition=trans;svg.appendChild(e);}
return e;}
function barRect(svg,id){
return ensure(svg,"rect",id,{rx:3},"y .55s ease,height .55s ease,fill .3s");}
function barText(svg,id){
return ensure(svg,"text",id,{"text-anchor":"middle"},"y .55s ease");}
const tip=document.getElementById("tip");
function tipShow(ev,txt){
tip.textContent=txt;tip.style.display="block";
const tw=tip.offsetWidth||170;
let x=ev.clientX+14,y=ev.clientY-12;
if(x+tw>window.innerWidth-8)x=ev.clientX-tw-14;
tip.style.left=x+"px";tip.style.top=y+"px";}
function tipHide(){tip.style.display="none";}
/* ═══ widget: covariance toy (slide 3, interactive SVG) ═══ */
const TOY={sync:{a:[1,2,5,6,3,1],b:[2,3,7,8,4,2]},
ind:{a:[5,2,6,1,4,3],b:[1,2,7,6,3,5]}, /* cov = 0 exactly */
opp:{a:[6,5,3,2,1,1],b:[1,2,3,5,6,7]}};
const TOY_W=940,TOY_H=210;
let toyMode="sync";
function setToy(m){toyMode=m;
["bsync","bind","bopp"].forEach(id=>document.getElementById(id).classList.remove("active"));
document.getElementById({sync:"bsync",ind:"bind",opp:"bopp"}[m]).classList.add("active");
drawToy();}
function drawToy(){
const {a,b}=TOY[toyMode],n=a.length;
const trueT=a.reduce((s,x,i)=>s+x*b[i],0);
const naive=mean(a)*mean(b)*n, gap=trueT-naive;
document.getElementById("toyTrue").textContent=trueT.toFixed(0);
document.getElementById("toyNaive").textContent=naive.toFixed(0);
const g=document.getElementById("toyGap");
g.textContent=(gap>0?"+":"")+gap.toFixed(0);
g.style.color=gap>2?"#009E73":(gap<-2?"#D55E00":"#5c6779");
document.getElementById("toyO").textContent=(trueT/naive).toFixed(2);
const svg=document.getElementById("toySvg");
const m=36,bw=(TOY_W-2*m)/n;
ensure(svg,"line","toy_base",{x1:m,x2:TOY_W-m,y1:TOY_H-30,y2:TOY_H-30,
stroke:"#e3e7f0","stroke-width":1.5});
for(let i=0;i<n;i++){
const x=m+i*bw,ha=a[i]/9*(TOY_H-52),hb=b[i]/9*(TOY_H-52);
const txt=`t${i+1} · rate ${a[i]} × size ${b[i]} = ${a[i]*b[i]}`;
const ra=barRect(svg,"toy_a_"+i);
ra.setAttribute("x",x+bw*.18);ra.setAttribute("width",bw*.27);
ra.setAttribute("y",TOY_H-30-ha);ra.setAttribute("height",Math.max(ha,1.5));
ra.setAttribute("fill","rgba(213,94,0,.85)");
ra.onmousemove=ev=>tipShow(ev,txt);ra.onmouseleave=tipHide;
const rb=barRect(svg,"toy_b_"+i);
rb.setAttribute("x",x+bw*.52);rb.setAttribute("width",bw*.27);
rb.setAttribute("y",TOY_H-30-hb);rb.setAttribute("height",Math.max(hb,1.5));
rb.setAttribute("fill","rgba(0,114,178,.85)");
rb.onmousemove=ev=>tipShow(ev,txt);rb.onmouseleave=tipHide;
const pv=barText(svg,"toy_p_"+i);
pv.setAttribute("x",x+bw*.485);
pv.setAttribute("y",TOY_H-38-Math.max(ha,hb));
pv.setAttribute("font-size","12.5");pv.setAttribute("font-weight","700");
pv.setAttribute("fill","#10204d");
pv.textContent=a[i]*b[i];
const lt=barText(svg,"toy_t_"+i);
lt.setAttribute("x",x+bw/2);lt.setAttribute("y",TOY_H-12);
lt.setAttribute("font-size","12");lt.setAttribute("fill","#5c6779");
lt.textContent="t"+(i+1);}
const l1=ensure(svg,"text","toy_l1",{x:m,y:17,"font-size":"13",fill:"#D55E00"});
l1.textContent="■ rate";
const l2=ensure(svg,"text","toy_l2",{x:m+58,y:17,"font-size":"13",fill:"#0072B2"});
l2.textContent="■ size";
const l3=ensure(svg,"text","toy_l3",{x:TOY_W-m,y:17,"font-size":"12",
fill:"#5c6779","text-anchor":"end"});
l3.textContent="number above each pair = that snapshot’s product — hover the bars";
}
/* ═══ widget: shape-a-storm (slide 5, interactive SVG) ═══ */
const N=24,tt=[...Array(N).keys()];
const bell=(c,s)=>tt.map(x=>Math.exp(-0.5*((x-c)/s)**2));
const SHAPES=[
{r:bell(12,4),A:bell(12,4).map(v=>Math.pow(v,0.55)),
note:"Strength and size rise and fall together (ρ≈0.99) and both swing hard — squall lines and big storm systems beat the averages this way."},
{r:tt.map(()=>1),A:bell(12,6).map(v=>0.4+v),
note:"The intensity barely changes, so CV_I ≈ 0 — without intensity swings, even perfect synchrony couples nothing. K_IA ≈ 1."},
{r:bell(7,3.5),A:bell(17,3.5),
note:"Fierce early while small, big later while weak — peaks miss each other, ρ goes negative, K_IA drops below 1: the averages overcount."},
{r:bell(6,2.5).map((v,i)=>v+bell(18,2.5)[i]),
A:bell(6,2.5).map((v,i)=>Math.pow(v+bell(18,2.5)[i],0.55)),
note:"Two synced bursts with a quiet gap — the on/off rhythm itself creates coupling. This is why weekly rain windows show big K_IA in nature."}];
const SH_W=940,SH_H=200,SH_M=38;
let shapeIdx=0,shapeCur=null,shapeAnim=null;
const shY=(v,mx)=>SH_H-24-(v/mx)*(SH_H-58);
const shX=i=>SH_M+i*(SH_W-2*SH_M)/(N-1);
function shapePts(v){
const mx=Math.max(...v)*1.15;
return v.map((y,i)=>shX(i)+","+shY(y,mx)).join(" ");}
function renderShapeCurves(){
const svg=document.getElementById("shapeSvg");
ensure(svg,"polyline","sh_r",{fill:"none",stroke:"#D55E00","stroke-width":3})
.setAttribute("points",shapePts(shapeCur.r));
ensure(svg,"polyline","sh_A",{fill:"none",stroke:"#0072B2","stroke-width":3,
"stroke-dasharray":"7 5"}).setAttribute("points",shapePts(shapeCur.A));}
function tweenShape(to){
if(shapeAnim)cancelAnimationFrame(shapeAnim);
const from={r:[...shapeCur.r],A:[...shapeCur.A]},t0=performance.now(),dur=450;
const step=now=>{
const u=Math.min(1,(now-t0)/dur),e=u*u*(3-2*u);
shapeCur={r:from.r.map((v,i)=>v+(to.r[i]-v)*e),
A:from.A.map((v,i)=>v+(to.A[i]-v)*e)};
renderShapeCurves();
if(u<1)shapeAnim=requestAnimationFrame(step);else shapeAnim=null;};
shapeAnim=requestAnimationFrame(step);}
function setShape(i){shapeIdx=i;
for(let k=0;k<4;k++)document.getElementById("p"+k).classList.remove("active");
document.getElementById("p"+i).classList.add("active");drawShape();}
function drawShape(){
const {r,A,note}=SHAPES[shapeIdx],d=decomp(r,A);
document.getElementById("sRho").textContent=(d.rho>=0?"+":"")+d.rho.toFixed(2);
document.getElementById("sCVr").textContent=d.cvr.toFixed(2);
document.getElementById("sCVA").textContent=d.cva.toFixed(2);
const o=document.getElementById("sO");o.textContent=d.O.toFixed(2);
o.style.color=d.O>1.05?"#009E73":(d.O<0.95?"#D55E00":"#1a2233");
document.getElementById("shapeLesson").textContent=note;
const svg=document.getElementById("shapeSvg");
const l1=ensure(svg,"text","sh_l1",{x:SH_M,y:16,"font-size":"13",fill:"#D55E00"});
l1.textContent="— intensity";
const l2=ensure(svg,"text","sh_l2",{x:SH_M+62,y:16,"font-size":"13",fill:"#0072B2"});
l2.textContent="- - size";
const l3=ensure(svg,"text","sh_l3",{x:SH_W/2,y:SH_H-6,"font-size":"12",
fill:"#5c6779","text-anchor":"middle"});
l3.textContent="storm lifetime → (hover the curves)";
if(!shapeCur){shapeCur={r:[...r],A:[...A]};renderShapeCurves();}
else tweenShape({r,A});
/* hover guide: vertical line + a dot on each curve */
const gl=ensure(svg,"line","sh_gl",{stroke:"#cfd6e4","stroke-width":1.2,
"stroke-dasharray":"3 4",y1:20,y2:SH_H-24,visibility:"hidden",
"pointer-events":"none"});
const dr=ensure(svg,"circle","sh_dr",{r:5,fill:"#D55E00",
visibility:"hidden","pointer-events":"none"});
const dA=ensure(svg,"circle","sh_dA",{r:5,fill:"#0072B2",
visibility:"hidden","pointer-events":"none"});
const hov=ensure(svg,"rect","sh_hov",{x:0,y:0,width:SH_W,height:SH_H,
fill:"transparent"});
hov.onmousemove=ev=>{
const bb=svg.getBoundingClientRect();
const fx=(ev.clientX-bb.left)/bb.width*SH_W;
const i=clamp(Math.round((fx-SH_M)/((SH_W-2*SH_M)/(N-1))),0,N-1);
const s=SHAPES[shapeIdx];
const mr=Math.max(...s.r),mA=Math.max(...s.A),x=shX(i);
gl.setAttribute("x1",x);gl.setAttribute("x2",x);
gl.setAttribute("visibility","visible");
dr.setAttribute("cx",x);dr.setAttribute("cy",shY(s.r[i],mr*1.15));
dr.setAttribute("visibility","visible");
dA.setAttribute("cx",x);dA.setAttribute("cy",shY(s.A[i],mA*1.15));
dA.setAttribute("visibility","visible");
tipShow(ev,`step ${i+1}/${N} · intensity at ${(s.r[i]/mr*100).toFixed(0)}% of its peak · size at ${(s.A[i]/mA*100).toFixed(0)}%`);};
hov.onmouseleave=()=>{tipHide();
[gl,dr,dA].forEach(e=>e.setAttribute("visibility","hidden"));};
}
/* ═══ storm player (slide 6, real data, interactive SVG) ═══ */
const CASE_NOTES={
organized:"It rains hardest exactly when it covers the most ground (ρ=+0.87) — the dots climb a rising diagonal. Coupling added +45% water.",
twopulse:"Two bursts with a lull between: the on/off rhythm counts as coupling too — +55%. Nature’s biggest K_IA values come from this rhythm.",
scattered:"Afternoon popcorn storms: intensity and size both vary, but out of step — the dot cloud is shapeless and K_IA lands at 1.00.",
anti:"A dying storm: fiercest while small, then wide but weak (ρ=−0.21). The staircase falls — the averages overcount by 10%."};
const CASE_BTN={organized:"cb0",twopulse:"cb1",scattered:"cb2",anti:"cb3"};
const PL_W=940,PL_H=280;
let caseKey="organized",frame=0,playing=false,timer=null,pl=null;
function buildPlayer(){
const svg=document.getElementById("playerSvg");
svg.innerHTML="";
const c=CASE_DATA[caseKey],r=c.r,A=c.A,n=r.length,d=decomp(r,A);
const L={x:46,y:24,w:PL_W*0.55-60,h:PL_H-62};
const R={x:PL_W*0.55+40,y:24,w:PL_W-(PL_W*0.55+40)-16,h:PL_H-62};
const mr=Math.max(...r)*1.12,mA=Math.max(...A)*1.12;
const yR=v=>L.y+L.h-(v/mr)*L.h, yA=v=>L.y+L.h-(v/mA)*L.h;
const sxp=v=>R.x+(v/mA)*R.w, syp=v=>R.y+R.h-(v/mr)*R.h;
svg.appendChild(svgEl("rect",{x:L.x,y:L.y,width:L.w,height:L.h,
fill:"none",stroke:"#e7eaf2"}));
svg.appendChild(svgEl("rect",{x:R.x,y:R.y,width:R.w,height:R.h,
fill:"none",stroke:"#e7eaf2"}));
svg.appendChild(svgEl("line",{x1:L.x,x2:L.x+L.w,y1:yR(d.rm),y2:yR(d.rm),
stroke:"rgba(213,94,0,.5)","stroke-dasharray":"4 4"}));
svg.appendChild(svgEl("line",{x1:L.x,x2:L.x+L.w,y1:yA(d.Am),y2:yA(d.Am),
stroke:"rgba(0,114,178,.5)","stroke-dasharray":"4 4"}));
svg.appendChild(svgEl("line",{x1:sxp(d.Am),x2:sxp(d.Am),y1:R.y,y2:R.y+R.h,
stroke:"#cfd6e4","stroke-dasharray":"3 4"}));
svg.appendChild(svgEl("line",{x1:R.x,x2:R.x+R.w,y1:syp(d.rm),y2:syp(d.rm),
stroke:"#cfd6e4","stroke-dasharray":"3 4"}));
const t1=svgEl("text",{x:L.x,y:16,"font-size":"12.5",fill:"#D55E00"});
t1.textContent="— intensity (dashed: its average)";svg.appendChild(t1);
const t2=svgEl("text",{x:L.x+200,y:16,"font-size":"12.5",fill:"#0072B2"});
t2.textContent="— area";svg.appendChild(t2);
const t3=svgEl("text",{x:R.x+R.w/2,y:PL_H-12,"font-size":"12.5",
fill:"#5c6779","text-anchor":"middle"});
t3.textContent="size →";svg.appendChild(t3);
const t4=svgEl("text",{"font-size":"12.5",fill:"#5c6779","text-anchor":"middle",
transform:`translate(${R.x-10},${R.y+R.h/2}) rotate(-90)`});
t4.textContent="intensity →";svg.appendChild(t4);
const lr=svgEl("polyline",{fill:"none",stroke:"#D55E00","stroke-width":2.6});
const lA=svgEl("polyline",{fill:"none",stroke:"#0072B2","stroke-width":2.6});
svg.appendChild(lr);svg.appendChild(lA);
const dots=[];
for(let i=0;i<n;i++){
const dot=svgEl("circle",{cx:sxp(A[i]),cy:syp(r[i]),r:4.5,
fill:vCol(i/(n-1)),visibility:"hidden"});
dot.onmousemove=ev=>tipShow(ev,
`hour ${(i*0.5).toFixed(1)} · ${r[i].toFixed(1)} mm/h · ${A[i].toFixed(0)}k km²`);
dot.onmouseleave=tipHide;
svg.appendChild(dot);dots.push(dot);}
const ring=svgEl("circle",{r:7,fill:"none",stroke:"red","stroke-width":2});
svg.appendChild(ring);
const tl=svgEl("text",{x:L.x+L.w/2,y:PL_H-12,"font-size":"12.5",
fill:"#5c6779","text-anchor":"middle"});
svg.appendChild(tl);
const hov=svgEl("rect",{x:L.x,y:L.y,width:L.w,height:L.h,fill:"transparent"});
hov.onmousemove=ev=>{
const bb=svg.getBoundingClientRect();
const fx=(ev.clientX-bb.left)/bb.width*PL_W;
const i=clamp(Math.round((fx-L.x)/(L.w/(n-1))),0,n-1);
tipShow(ev,`hour ${(i*0.5).toFixed(1)} · intensity ${r[i].toFixed(1)} mm/h · area ${A[i].toFixed(0)}k km²`);};
hov.onmouseleave=tipHide;
svg.appendChild(hov);
pl={lr,lA,dots,ring,tl,L,mr,mA,sxp,syp,n,r,A};
}
function updatePlayer(){
const p=pl;
document.getElementById("cNow").textContent=
p.r[frame].toFixed(1)+" mm/h · "+p.A[frame].toFixed(0)+"k km²";
const px=i=>p.L.x+i*p.L.w/(p.n-1);
p.lr.setAttribute("points",p.r.slice(0,frame+1)
.map((v,i)=>px(i)+","+(p.L.y+p.L.h-(v/p.mr)*p.L.h)).join(" "));
p.lA.setAttribute("points",p.A.slice(0,frame+1)
.map((v,i)=>px(i)+","+(p.L.y+p.L.h-(v/p.mA)*p.L.h)).join(" "));
p.dots.forEach((d,i)=>d.setAttribute("visibility",i<=frame?"visible":"hidden"));
p.ring.setAttribute("cx",p.sxp(p.A[frame]));
p.ring.setAttribute("cy",p.syp(p.r[frame]));
p.tl.textContent="hour "+(frame*0.5).toFixed(1)+" of "+((p.n-1)*0.5).toFixed(0);
}
function setCase(k){
caseKey=k;frame=0;
Object.values(CASE_BTN).forEach(id=>document.getElementById(id).classList.remove("active"));
document.getElementById(CASE_BTN[k]).classList.add("active");
const c=CASE_DATA[k],d=decomp(c.r,c.A);
document.getElementById("cName").textContent=c.name;
document.getElementById("cDate").textContent=c.date+" · MRMS radar";
document.getElementById("cRho").textContent=(d.rho>=0?"+":"")+d.rho.toFixed(2);
const o=document.getElementById("cO");o.textContent=d.O.toFixed(2);
o.style.color=d.O>1.05?"#009E73":(d.O<0.95?"#D55E00":"#1a2233");
document.getElementById("caseLesson").textContent=CASE_NOTES[k];
const sc=document.getElementById("scrub");sc.max=c.r.length-1;sc.value=0;
buildPlayer();updatePlayer();}
function setFrame(f){frame=f;updatePlayer();}
function togglePlay(){
playing=!playing;
document.getElementById("playBtn").textContent=playing?"❚❚":"▶";
if(playing){timer=setInterval(()=>{
const c=CASE_DATA[caseKey];
frame=(frame+1)%c.r.length;
if(frame===c.r.length-1){togglePlay();}
document.getElementById("scrub").value=frame;
updatePlayer();},130);}
else clearInterval(timer);}
/* ═══ shape gallery (slide 7, real data, interactive SVG) ═══ */
function drawGallery(){
const svg=document.getElementById("gallerySvg");
svg.innerHTML="";
const order=["anti","scattered","organized","twopulse"];
const W=940,H=270,pw=W/4,mx=3.4;
order.forEach((k,col)=>{
const c=CASE_DATA[k],d=decomp(c.r,c.A),n=c.r.length;
const P={x:col*pw+34,y:42,w:pw-48,h:H-86};
svg.appendChild(svgEl("rect",{x:P.x,y:P.y,width:P.w,height:P.h,
fill:"none",stroke:"#e7eaf2"}));
svg.appendChild(svgEl("line",{x1:P.x+P.w/mx,x2:P.x+P.w/mx,y1:P.y,y2:P.y+P.h,
stroke:"#d8dee9","stroke-dasharray":"3 4"}));
svg.appendChild(svgEl("line",{x1:P.x,x2:P.x+P.w,y1:P.y+P.h-P.h/mx,
y2:P.y+P.h-P.h/mx,stroke:"#d8dee9","stroke-dasharray":"3 4"}));
for(let i=0;i<n;i++){
const x=P.x+Math.min(c.A[i]/d.Am,mx)/mx*P.w;
const y=P.y+P.h-Math.min(c.r[i]/d.rm,mx)/mx*P.h;
const dot=svgEl("circle",{cx:x,cy:y,r:4,fill:vCol(i/(n-1))});
dot.onmousemove=ev=>tipShow(ev,
`hour ${(i*0.5).toFixed(1)} · intensity ${(c.r[i]/d.rm).toFixed(1)}× its average · size ${(c.A[i]/d.Am).toFixed(1)}×`);
dot.onmouseleave=tipHide;
svg.appendChild(dot);}
const ot=svgEl("text",{x:P.x+P.w/2,y:20,"font-size":"13","font-weight":"700",
"text-anchor":"middle",
fill:d.O>1.05?"#009E73":(d.O<0.95?"#D55E00":"#5c6779")});
ot.textContent="K = "+d.O.toFixed(2);svg.appendChild(ot);
const nt=svgEl("text",{x:P.x+P.w/2,y:34,"font-size":"12",
"text-anchor":"middle",fill:"#1a2233"});
nt.textContent=c.name.replace(" system","").replace(" sequence","");
svg.appendChild(nt);
const rt=svgEl("text",{x:P.x+P.w/2,y:P.y+P.h+16,"font-size":"11",
"text-anchor":"middle",fill:"#5c6779"});
rt.textContent="ρ = "+(d.rho>=0?"+":"")+d.rho.toFixed(2);svg.appendChild(rt);
});
const cap=svgEl("text",{x:W/2,y:H-6,"font-size":"12","text-anchor":"middle",
fill:"#5c6779"});
cap.textContent="size / average → (each panel: strength / average ↑, dashes mark the averages, color = lifetime)";
svg.appendChild(cap);
}
/* ═══ satellite chart (slide 9, interactive SVG, two views) ═══ */
const SAT={
IR:{I:0.762,A:1.436,K:0.913,V:0.999, DI:-0.272,DA:0.362,DK:-0.091,DV:-0.001,
note:"Infrared-only first stage: intensity far too weak (D_I=−0.27, −24%), rain smeared over far too much area (D_A=+0.36, +44%) — over a third of its rain falls where the radar sees none — and slightly under-coupled (D_K=−0.09). The three nearly cancel, so the total is almost spot-on (D_V≈0)."},
Uncal:{I:0.812,A:1.178,K:1.002,V:0.958, DI:-0.208,DA:0.164,DK:0.002,DV:-0.042,
note:"Adding microwave passes repairs much of the area excess and makes coupling essentially perfect (D_K≈0) — but intensity is still 19% low, leaving the total about 4% short (D_V=−0.04)."},
Cal:{I:0.861,A:1.204,K:0.976,V:1.012, DI:-0.150,DA:0.186,DK:-0.025,DV:0.011,
note:"The final gauge correction lands the total within +1.2% (D_V=+0.01) — but look how: intensity stays 14% low while area is 20% too large. The near-perfect total is a sizeable intensity deficit and area excess cancelling."}};
const COMP_META={I:["intensity Ī","#D55E00"],A:["area Ā","#0072B2"],
K:["coupling K","#009E73"],V:["TOTAL V","#1a2233"]};
const SAT_DKEY={I:"DI",A:"DA",K:"DK",V:"DV"};
const pctOf=d=>(Math.exp(d)-1)*100;
let satKey="Cal",satView="summary",satComp="A";
function setSat(k){satKey=k;
["sb0","sb1","sb2"].forEach(id=>document.getElementById(id).classList.remove("active"));
document.getElementById({IR:"sb0",Uncal:"sb1",Cal:"sb2"}[k]).classList.add("active");
document.getElementById("satLesson").textContent=SAT[k].note;
renderSat();}
function setSatView(v){satView=v;
document.getElementById("vb0").classList.toggle("active",v==="summary");
document.getElementById("vb1").classList.toggle("active",v==="monthly");
document.getElementById("compRow").style.display=v==="monthly"?"flex":"none";
document.getElementById("satSvg").innerHTML=""; // rebuild fresh per view
renderSat();}
function setSatComp(c){satComp=c;
["kbI","kbA","kbK","kbV"].forEach(id=>document.getElementById(id).classList.remove("active"));
document.getElementById("kb"+c).classList.add("active");
renderSat();}
const SAT_W=940,SAT_H=260;
function renderSat(){
if(satView==="summary")renderSatSummary();else renderSatMonthly();}
function axisLine(svg,y,label){
if(!svg.querySelector("#base")){
const ln=svgEl("line",{id:"base",x1:34,x2:SAT_W-14,y1:y,y2:y,
stroke:"#1a2233","stroke-width":1.6});
svg.appendChild(ln);
const t=svgEl("text",{x:SAT_W-16,y:y-7,"text-anchor":"end",
"font-size":"12",fill:"#5c6779"});
t.textContent=label;svg.appendChild(t);}}
function renderSatSummary(){
// additive log-ratio accounting for all three stages: D_V = D_I + D_A + D_K
const svg=document.getElementById("satSvg");svg.innerHTML="";
const stages=["IR","Uncal","Cal"];
const comps=[["DI","intensity (D_I)","#D55E00"],["DA","area (D_A)","#0072B2"],
["DK","coupling (D_K)","#009E73"]];
let mx=0.05;
for(const p of stages)for(const c of ["DI","DA","DK","DV"])mx=Math.max(mx,Math.abs(SAT[p][c]));
mx*=1.2;
const top=46,bot=SAT_H-28,mid=(top+bot)/2,half=(bot-top)/2;
const y=d=>mid-(d/mx)*half;
svg.appendChild(svgEl("line",{x1:58,x2:SAT_W-16,y1:y(0),y2:y(0),
stroke:"#1a2233","stroke-width":1.4}));
const za=svgEl("text",{x:SAT_W-16,y:y(0)-6,"text-anchor":"end","font-size":"11.5",fill:"#5c6779"});
za.textContent="radar reference Dₓ = 0";svg.appendChild(za);
const gw=(SAT_W-80)/stages.length;
stages.forEach((p,si)=>{
const cx=64+si*gw+gw*0.5,bw=46;let posb=0,negb=0;
comps.forEach(([key,lab,col])=>{
const d=SAT[p][key],base=d>=0?posb:negb;
const rect=svgEl("rect",{x:cx-bw/2,y:Math.min(y(base),y(base+d)),width:bw,
height:Math.max(Math.abs(y(base+d)-y(base)),1),fill:col+"dd",stroke:"#fff","stroke-width":0.6});
rect.onmousemove=ev=>tipShow(ev,
`${p} · ${lab}: D=${d>=0?"+":""}${d.toFixed(3)} (${pctOf(d)>=0?"+":""}${pctOf(d).toFixed(0)}%)`);
rect.onmouseleave=tipHide;svg.appendChild(rect);
if(d>=0)posb+=d;else negb+=d;});
const dv=SAT[p].DV;
svg.appendChild(svgEl("line",{x1:cx-bw/2-7,x2:cx+bw/2+7,y1:y(dv),y2:y(dv),
stroke:"#D81B9A","stroke-width":3.2}));
const tv=svgEl("text",{x:cx,y:(dv>=0?y(Math.max(posb,dv))-8:y(Math.min(negb,dv))+16),
"text-anchor":"middle","font-size":"12.5","font-weight":"700",fill:"#D81B9A"});
tv.textContent="D_V="+(dv>=0?"+":"")+dv.toFixed(3);svg.appendChild(tv);
const nm=svgEl("text",{x:cx,y:SAT_H-9,"text-anchor":"middle","font-size":"13",
"font-weight":"600",fill:"#1a2233"});
nm.textContent=p;svg.appendChild(nm);});
const lg=[["intensity D_I","#D55E00"],["area D_A","#0072B2"],
["coupling D_K","#009E73"],["total D_V","#D81B9A"]];
lg.forEach((e,i)=>{const x=60+i*156;
svg.appendChild(svgEl("rect",{x:x,y:15,width:13,height:10,fill:e[1]+"dd"}));
const t=svgEl("text",{x:x+18,y:24,"font-size":"11.5",fill:"#1a2233"});
t.textContent=e[0];svg.appendChild(t);});
}
function renderSatMonthly(){
const svg=document.getElementById("satSvg");
const vals=SAT_MONTHLY[satKey][satComp],D=SAT_MONTHLY[satKey][SAT_DKEY[satComp]],months=SAT_MONTHLY.months;
const [lab,col]=COMP_META[satComp];
// fixed per-component range across products for comparability
let lo=1e9,hi=-1e9;
for(const p of ["IR","Uncal","Cal"])
for(const v of SAT_MONTHLY[p][satComp]){lo=Math.min(lo,v);hi=Math.max(hi,v);}
lo=Math.min(lo,0.95);hi=Math.max(hi,1.05);
const pad=(hi-lo)*0.08;lo-=pad;hi+=pad;
const y=v=>22+(hi-v)/(hi-lo)*(SAT_H-66);
axisLine(svg,y(1),"radar = 1.0");
svg.querySelector("#base").setAttribute("y1",y(1));
svg.querySelector("#base").setAttribute("y2",y(1));
const bw=(SAT_W-90)/24;
months.forEach((m,i)=>{
const v=vals[i],x=44+i*bw;
const warm=+m.slice(5)>=5&&+m.slice(5)<=9;
const rect=barRect(svg,"mbar_"+i);
rect.setAttribute("x",x+bw*0.12);rect.setAttribute("width",bw*0.76);
rect.setAttribute("y",Math.min(y(v),y(1)));
rect.setAttribute("height",Math.max(Math.abs(y(v)-y(1)),1.5));
rect.setAttribute("fill",col+(warm?"ff":"99"));
rect.onmousemove=ev=>tipShow(ev,`${m}: ${lab} ratio ${v.toFixed(2)} · D=${D[i]>=0?"+":""}${D[i].toFixed(3)} (${v>=1?"+":""}${((v-1)*100).toFixed(0)}%)${warm?" · warm season":""}`);
rect.onmouseleave=tipHide;
if(+m.slice(5)%3===1){
const t=barText(svg,"mlab_"+i);
t.setAttribute("x",x+bw/2);t.setAttribute("y",SAT_H-8);
t.setAttribute("font-size","10");t.setAttribute("fill","#5c6779");
t.textContent=m;}});
let hdr=svg.querySelector("#hdr");
if(!hdr){hdr=svgEl("text",{id:"hdr",x:36,y:14,"font-size":"12",fill:"#5c6779"});
svg.appendChild(hdr);}
hdr.textContent=`${lab} — satellite ÷ radar, each of the 24 months (solid = warm season May–Sep) — hover for values`;
}
/* init */
drawToy();drawShape();setCase("organized");drawGallery();setSat("Cal");
show(0);
</script>
</body>
</html>