| <!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 · swipe</div> |
| <div id="deck"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> > 1 amplifies, K<sub>IA</sub> = 1 neutral, K<sub>IA</sub> < 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> |
|
|
| |
| <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 & 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 & 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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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>: \(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> |
|
|
| |
| <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> |
|
|
| |
| <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}\), \(K_{IA} = 1+\rho\,CV_I\,CV_A\); |
| 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"; |
| |
| 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}); |
| |
| |
| 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=>{ |
| 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(",")})`; |
| }; |
| |
| |
| 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";} |
| |
| |
| 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]}, |
| 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"; |
| } |
| |
| |
| 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}); |
| |
| 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"));}; |
| } |
| |
| |
| 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);} |
| |
| |
| 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); |
| } |
| |
| |
| 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=""; |
| 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(){ |
| |
| 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]; |
| |
| 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`; |
| } |
| |
| |
| drawToy();drawShape();setCase("organized");drawGallery();setSat("Cal"); |
| show(0); |
| </script> |
| </body> |
| </html> |
|
|