{"id":8,"date":"2026-01-26T21:36:01","date_gmt":"2026-01-26T21:36:01","guid":{"rendered":"https:\/\/sites.ohio.edu\/greg-springer\/?page_id=8"},"modified":"2026-01-26T21:38:07","modified_gmt":"2026-01-26T21:38:07","slug":"flood-ri-plotter","status":"publish","type":"page","link":"https:\/\/sites.ohio.edu\/greg-springer\/flood-ri-plotter\/","title":{"rendered":"Flood RI Plotter"},"content":{"rendered":"\n<div>\n<style>\n\t:root{\n\t\t--ink:#222;\n\t\t--grid:#9a9a9a;\n\t\t--light:#f7f7f7;\n\t\t--btn:#efefef;\n\t\t--btnb:#666;\n\t}\n\tbody{\n\t\tmargin:0;\n\t\tfont-family: Arial, Helvetica, sans-serif;\n\t\tcolor:var(--ink);\n\t\tbackground:white;\n\t}\n\t.page{\n\t\tmax-width: 1100px;\n\t\tmargin: 18px auto 26px auto;\n\t\tpadding: 0 14px;\n\t}\n\th1{\n\t\tmargin: 4px 0 10px 0;\n\t\tfont-size: 38px;\n\t\tfont-weight: 700;\n\t}\n\t.plotWrap{\n\t\tdisplay:flex;\n\t\tjustify-content:center;\n\t}\n\tcanvas{\n\t\tbackground:white;\n\t\tborder: 0;\n\t\twidth: min(980px, 96vw);\n\t\theight: auto;\n\t\ttouch-action: none;\n\t}\n\t.controls{\n\t\tmargin: 8px 0 10px 0;\n\t\tdisplay:flex;\n\t\talign-items:center;\n\t\tgap: 10px;\n\t\tflex-wrap:wrap;\n\t}\n\t.controls .spacer{\n\t\tflex: 1 1 auto;\n\t}\n\t.btn{\n\t\tfont-family: Arial, Helvetica, sans-serif;\n\t\tfont-size: 18px;\n\t\tpadding: 6px 18px;\n\t\tbackground: var(--btn);\n\t\tborder: 2px solid var(--btnb);\n\t\tcursor: pointer;\n\t\tbox-shadow: 1px 1px 0 rgba(0,0,0,0.12);\n\t}\n\t.btn:active{\n\t\ttransform: translateY(1px);\n\t\tbox-shadow: 0 0 0 rgba(0,0,0,0.0);\n\t}\n\t.rowsBox{\n\t\tdisplay:flex;\n\t\talign-items:center;\n\t\tgap: 10px;\n\t\tfont-size: 18px;\n\t}\n\t.rowsBox input{\n\t\twidth: 70px;\n\t\tfont-size: 18px;\n\t\tpadding: 6px 8px;\n\t\tborder: 2px solid var(--btnb);\n\t}\n\t.msg{\n\t\tmargin-left: 8px;\n\t\tfont-size: 16px;\n\t\tcolor: #b00020;\n\t\tmin-height: 20px;\n\t}\n\ttable{\n\t\tborder-collapse: collapse;\n\t\twidth: 100%;\n\t\ttable-layout: fixed;\n\t\tfont-size: 16px;\n\t}\n\tth, td{\n\t\tborder: 2px solid #333;\n\t\tpadding: 0;\n\t}\n\tth{\n\t\tbackground: #f0f0f0;\n\t\ttext-align: left;\n\t\tpadding: 8px 10px;\n\t\tfont-size: 18px;\n\t}\n\ttd input{\n\t\twidth: 100%;\n\t\tbox-sizing: border-box;\n\t\tborder: none;\n\t\tpadding: 8px 10px;\n\t\tfont-size: 16px;\n\t\tfont-family: Arial, Helvetica, sans-serif;\n\t\tbackground: white;\n\t}\n\ttd input:focus{\n\t\toutline: 2px solid #4a90e2;\n\t\toutline-offset: -2px;\n\t}\n\t.footer{\n\t\tmargin-top: 12px;\n\t\tfont-size: 14px;\n\t\tcolor:#333;\n\t\ttext-align:left;\n\t}\n\t.smallNote{\n\t\tfont-size: 14px;\n\t\tcolor:#444;\n\t\tmargin-top: 8px;\n\t}\n\n\t@media print{\n\t\t.page{max-width: none; margin: 0; padding: 0;}\n\t\t.controls, .msg, .footer{\n\t\tmargin-top: 12px;\n\t\tfont-size: 14px;\n\t\tcolor:#333;\n\t\ttext-align:left;\n\t}\n\t.smallNote{display:none !important;}\n\t\th1{margin:0 0 8px 0;}\n\t\tcanvas{width: 100% !important; height: auto !important;}\n\t\ttable{font-size: 12px;}\n\t\tth{font-size: 12px;}\n\t\ttd input{font-size: 12px;}\n\t\t\/* Make inputs print their values *\/\n\t\ttd input{\n\t\t\t-webkit-print-color-adjust: exact;\n\t\t\tprint-color-adjust: exact;\n\t\t}\n\t}\n<\/style>\n<\/head>\n<body>\n<div class=\"page\">\n\t<h1>Flood Probability Plotter<\/h1>\n\n\t<div class=\"plotWrap\">\n\t\t<canvas id=\"plot\" width=\"980\" height=\"620\" aria-label=\"Probability plot\"><\/canvas>\n\t<\/div>\n\n\t<div class=\"controls\">\n\t\t<div class=\"rowsBox\">\n\t\t\t<button class=\"btn\" id=\"makeBtn\" type=\"button\">Make<\/button>\n\t\t\t<input id=\"nRows\" type=\"number\" min=\"9\" max=\"150\" value=\"9\" \/>\n\t\t\t<span>rows.<\/span>\n\t\t<\/div>\n\n\t\t<div class=\"spacer\"><\/div>\n\n\t\t<button class=\"btn\" id=\"plotBtn\" type=\"button\">Plot<\/button>\n\t\t<button class=\"btn\" id=\"clearBtn\" type=\"button\">Clear<\/button>\n\t\t<button class=\"btn\" id=\"resetBtn\" type=\"button\">Reset<\/button>\n\t\t<button class=\"btn\" id=\"printBtn\" type=\"button\">Print\/PDF<\/button>\n\t<\/div>\n\t<div class=\"msg\" id=\"msg\"><\/div>\n\n\t<table id=\"dataTable\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th style=\"width:12%;\">Year<\/th>\n\t\t\t\t<th style=\"width:16%;\">Stage (feet)<\/th>\n\t\t\t\t<th style=\"width:18%;\">Discharge (cfs)<\/th>\n\t\t\t\t<th style=\"width:12%;\">Rank<\/th>\n\t\t\t\t<th style=\"width:16%;\">Probability<\/th>\n\t\t\t\t<th style=\"width:16%;\">Recurrence Interval<\/th>\n\t\t\t<\/tr>\n\t\t<\/thead>\n\t\t<tbody id=\"tbody\"><\/tbody>\n\t<\/table>\n\n\t<div class=\"smallNote\">\n\t\tRequired columns: Discharge (cfs) and Probability (percent). Probability is exceedance probability in percent (e.g., 10 for 10%).\n\t<\/div>\n\n\t<div class=\"footer\">Made by Dr. G.S. Springer and ChatGPT 5.2<\/div>\n\n\t<\/div>\n\n<script>\n(() => {\n\t\"use strict\";\n\n\tconst MIN_ROWS = 9;\n\tconst MAX_ROWS = 150;\n\n\tconst PROB_TICKS = [99.5, 99, 95, 90, 75, 50, 25, 10, 5, 1, 0.5, 0.1]; \/\/ percent (exceedance)\n\tconst canvas = document.getElementById(\"plot\");\n\tconst ctx = canvas.getContext(\"2d\");\n\tconst tbody = document.getElementById(\"tbody\");\n\tconst nRowsInput = document.getElementById(\"nRows\");\n\tconst msg = document.getElementById(\"msg\");\n\n\tconst makeBtn = document.getElementById(\"makeBtn\");\n\tconst plotBtn = document.getElementById(\"plotBtn\");\n\tconst clearBtn = document.getElementById(\"clearBtn\");\n\tconst resetBtn = document.getElementById(\"resetBtn\");\n\tconst printBtn = document.getElementById(\"printBtn\");\n\n\t\/\/ Plot geometry\n\tconst G = {\n\t\tleft: 120,\n\t\tright: 40,\n\t\ttop: 150,\n\t\tbottom: 120\n\t};\n\n\tlet dataPoints = []; \/\/ {p: percent exceedance, y: discharge cfs}\n\tlet yMin = 1;\n\tlet yMax = 100000;\n\n\t\/\/ Anchors live on the border of the plot rectangle (in pixel coords)\n\tlet anchors = {\n\t\ta: {x: 0, y: 0},\n\t\tb: {x: 0, y: 0}\n\t};\n\tlet dragging = null; \/\/ \"a\" or \"b\"\n\n\tfunction setMessage(s){\n\t\tmsg.textContent = s || \"\";\n\t}\n\n\tfunction clamp(v, lo, hi){\n\t\treturn Math.max(lo, Math.min(hi, v));\n\t}\n\n\t\/\/ Approximation to inverse error function \/ inverse normal CDF\n\t\/\/ Source: Peter John Acklam's approximation (public domain style usage), adapted.\n\tfunction normInv(p){\n\t\t\/\/ p in (0,1)\n\t\tif(!(p > 0 && p < 1)) return NaN;\n\n\t\tconst a = [-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00];\n\t\tconst b = [-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01];\n\t\tconst c = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00];\n\t\tconst d = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00];\n\n\t\tconst plow = 0.02425;\n\t\tconst phigh = 1 - plow;\n\n\t\tlet q, r;\n\t\tif(p < plow){\n\t\t\tq = Math.sqrt(-2 * Math.log(p));\n\t\t\treturn (((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) \/\n\t\t\t\t((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1);\n\t\t}\n\t\tif(p > phigh){\n\t\t\tq = Math.sqrt(-2 * Math.log(1 - p));\n\t\t\treturn -(((((c[0]*q + c[1])*q + c[2])*q + c[3])*q + c[4])*q + c[5]) \/\n\t\t\t\t((((d[0]*q + d[1])*q + d[2])*q + d[3])*q + 1);\n\t\t}\n\t\tq = p - 0.5;\n\t\tr = q*q;\n\t\treturn (((((a[0]*r + a[1])*r + a[2])*r + a[3])*r + a[4])*r + a[5]) * q \/\n\t\t\t(((((b[0]*r + b[1])*r + b[2])*r + b[3])*r + b[4])*r + 1);\n\t}\n\n\tfunction probToX(pct){\n\t\t\/\/ pct is exceedance probability in percent; map to normal deviate scale using nonexceedance = 1 - pct\/100\n\t\tconst p = 1 - (pct\/100);\n\t\tconst z = normInv(p);\n\t\treturn z;\n\t}\n\n\tfunction xToCanvas(z){\n\t\tconst plotL = G.left;\n\t\tconst plotR = canvas.width - G.right;\n\t\tconst zMin = probToX(PROB_TICKS[0]); \/\/ 99.5 -> small pNonExc -> negative\n\t\tconst zMax = probToX(PROB_TICKS[PROB_TICKS.length-1]); \/\/ 0.1 -> ~3.09\n\t\tconst t = (z - zMin) \/ (zMax - zMin);\n\t\treturn plotL + t * (plotR - plotL);\n\t}\n\n\tfunction stageToCanvas(v){\n\t\t\/\/ Log10 y-axis (discharge). v must be > 0.\n\t\tconst plotT = G.top;\n\t\tconst plotB = canvas.height - G.bottom;\n\t\tconst vv = Math.max(v, 1e-12);\n\n\t\tconst lmin = Math.log10(yMin);\n\t\tconst lmax = Math.log10(yMax);\n\t\tconst t = (Math.log10(vv) - lmin) \/ (lmax - lmin);\n\t\treturn plotB - t * (plotB - plotT);\n\t}\n\n\tfunction canvasToStage(y){\n\t\t\/\/ Inverse of stageToCanvas for log10 axis\n\t\tconst plotT = G.top;\n\t\tconst plotB = canvas.height - G.bottom;\n\t\tconst t = (plotB - y) \/ (plotB - plotT);\n\n\t\tconst lmin = Math.log10(yMin);\n\t\tconst lmax = Math.log10(yMax);\n\t\tconst lv = lmin + t * (lmax - lmin);\n\t\treturn Math.pow(10, lv);\n\t}\n\n\tfunction zAtCanvasX(x){\n\t\tconst plotL = G.left;\n\t\tconst plotR = canvas.width - G.right;\n\t\tconst zMin = probToX(PROB_TICKS[0]);\n\t\tconst zMax = probToX(PROB_TICKS[PROB_TICKS.length-1]);\n\t\tconst t = (x - plotL) \/ (plotR - plotL);\n\t\treturn zMin + clamp(t, 0, 1) * (zMax - zMin);\n\t}\n\n\tfunction pctFromZ(z){\n\t\t\/\/ invert probToX mapping: z = normInv(1-pct\/100) => 1-pct\/100 = normCdf(z)\n\t\t\/\/ Use approximation for CDF\n\t\tconst pNonExc = normCdf(z);\n\t\treturn (1 - pNonExc) * 100;\n\t}\n\n\tfunction normCdf(z){\n\t\t\/\/ Abramowitz & Stegun approximation\n\t\tconst t = 1 \/ (1 + 0.2316419 * Math.abs(z));\n\t\tconst d = 0.3989423 * Math.exp(-z*z\/2);\n\t\tlet prob = d*t*(0.3193815 + t*(-0.3565638 + t*(1.781478 + t*(-1.821256 + t*1.330274))));\n\t\tif(z > 0) prob = 1 - prob;\n\t\treturn prob;\n\t}\n\n\tfunction nearestBorderPoint(x, y){\n\t\tconst L = G.left;\n\t\tconst R = canvas.width - G.right;\n\t\tconst T = G.top;\n\t\tconst B = canvas.height - G.bottom;\n\n\t\tconst cx = clamp(x, L, R);\n\t\tconst cy = clamp(y, T, B);\n\n\t\tconst dL = Math.abs(cx - L);\n\t\tconst dR = Math.abs(R - cx);\n\t\tconst dT = Math.abs(cy - T);\n\t\tconst dB = Math.abs(B - cy);\n\n\t\tconst m = Math.min(dL, dR, dT, dB);\n\n\t\tif(m === dL) return {x: L, y: cy};\n\t\tif(m === dR) return {x: R, y: cy};\n\t\tif(m === dT) return {x: cx, y: T};\n\t\treturn {x: cx, y: B};\n\t}\n\n\tfunction anchorHitTest(mx, my){\n\t\tconst r = 10;\n\t\tfor(const k of [\"a\",\"b\"]){\n\t\t\tconst dx = mx - anchors[k].x;\n\t\t\tconst dy = my - anchors[k].y;\n\t\t\tif(dx*dx + dy*dy <= r*r) return k;\n\t\t}\n\t\treturn null;\n\t}\n\n\tfunction defaultAnchors(){\n\t\tconst plotL = G.left;\n\t\tconst plotR = canvas.width - G.right;\n\t\tconst plotT = G.top;\n\t\tconst plotB = canvas.height - G.bottom;\n\n\t\tanchors.a = {x: plotL, y: plotB};   \/\/ bottom-left\n\t\tanchors.b = {x: plotR, y: plotT};   \/\/ top-right\n\t}\n\n\tfunction computeYMax(){\n\t\tlet mn = Infinity;\n\t\tlet mx = 0;\n\t\tfor(const pt of dataPoints){\n\t\t\tif(Number.isFinite(pt.y)){\n\t\t\t\tif(pt.y > 0) mn = Math.min(mn, pt.y);\n\t\t\t\tmx = Math.max(mx, pt.y);\n\t\t\t}\n\t\t}\n\t\tif(!Number.isFinite(mn) || mx <= 0){\n\t\t\tyMin = 1;\n\t\t\treturn 1000;\n\t\t}\n\n\t\t\/\/ Pad the top: max + 65% of max\n\t\tconst paddedMax = mx * 1.65;\n\n\t\t\/\/ Choose log-friendly bounds (powers of 10)\n\t\tyMin = Math.pow(10, Math.floor(Math.log10(mn)));\n\t\tconst topPow = Math.ceil(Math.log10(paddedMax));\n\t\treturn Math.pow(10, topPow);\n\t}\n\n\tfunction niceCeil(v){\n\t\tconst pow = Math.pow(10, Math.floor(Math.log10(v)));\n\t\tconst n = v \/ pow;\n\t\tlet step;\n\t\tif(n <= 1) step = 1;\n\t\telse if(n <= 2) step = 2;\n\t\telse if(n <= 5) step = 5;\n\t\telse step = 10;\n\t\treturn step * pow;\n\t}\n\n\tfunction pointRadius(n){\n\t\t\/\/ bigger for small n, small for large n\n\t\tconst r = 7 - 2.2 * Math.log10(Math.max(n, 1));\n\t\treturn clamp(r, 1.2, 6);\n\t}\n\n\tfunction draw(){\n\t\tctx.clearRect(0,0,canvas.width,canvas.height);\n\n\t\tdrawTitles();\n\t\tdrawAxesAndGrid();\n\t\tdrawPoints();\n\t\tdrawTrendLine();\n\t}\n\n\tfunction drawTitles(){\n\t\tctx.save();\n\t\tctx.fillStyle = \"#222\";\n\t\tctx.textAlign = \"center\";\n\n\t\tctx.font = \"42px Arial\";\n\t\tctx.fillText(\"Recurrence Interval (years)\", canvas.width\/2, 54);\n\n\t\t\/\/ x-axis label\n\t\tctx.font = \"40px Arial\";\n\t\tctx.fillText(\"Probability (%)\", canvas.width\/2, canvas.height - 40);\n\n\t\t\/\/ y-axis label (rotated)\n\t\tctx.translate(20, canvas.height\/2);\n\t\tctx.rotate(-Math.PI\/2);\n\t\tctx.font = \"38px Arial\";\n\t\tctx.fillText(\"discharge (cfs)\", 0, 0);\n\t\tctx.restore();\n\t}\n\n\tfunction labelWithBg(text, x, y, align, baseline){\n\t\t\/\/ Draw text with a small white background rectangle so grid\/border lines don't visually cut through labels\n\t\tconst padX = 4;\n\t\tconst padY = 2;\n\n\t\tctx.save();\n\t\tctx.textAlign = align;\n\t\tctx.textBaseline = baseline;\n\n\t\tconst metrics = ctx.measureText(text);\n\t\t\/\/ Approximate text box height from font size (since actualBoundingBox may vary)\n\t\tconst fontSize = 20;\n\n\t\tlet w = metrics.width;\n\t\tlet h = fontSize;\n\n\t\tlet left;\n\t\tif(align === \"center\") left = x - w\/2;\n\t\telse if(align === \"right\") left = x - w;\n\t\telse left = x;\n\n\t\tlet top;\n\t\tif(baseline === \"top\") top = y;\n\t\telse if(baseline === \"middle\") top = y - h\/2;\n\t\telse if(baseline === \"bottom\") top = y - h;\n\t\telse top = y - h; \/\/ alphabetic etc\n\n\t\tctx.fillStyle = \"white\";\n\t\tctx.fillRect(left - padX, top - padY, w + 2*padX, h + 2*padY);\n\n\t\tctx.fillStyle = \"#222\";\n\t\tctx.fillText(text, x, y);\n\t\tctx.restore();\n\t}\n\n\tfunction drawAxesAndGrid(){\n\t\tconst L = G.left;\n\t\tconst R = canvas.width - G.right;\n\t\tconst T = G.top;\n\t\tconst B = canvas.height - G.bottom;\n\n\t\t\/\/ plot border\n\t\tctx.save();\n\t\tctx.strokeStyle = \"#2a2a2a\";\n\t\tctx.lineWidth = 4;\n\t\tctx.strokeRect(L, T, R-L, B-T);\n\n\t\t\/\/ vertical grid + bottom ticks\n\t\tctx.strokeStyle = \"#7f7f7f\";\n\t\tctx.lineWidth = 2;\n\n\t\tctx.fillStyle = \"#222\";\n\t\tctx.font = \"20px Arial\";\n\t\tctx.textAlign = \"center\";\n\t\tctx.textBaseline = \"top\";\n\n\t\tfor(const pct of PROB_TICKS){\n\t\t\tconst z = probToX(pct);\n\t\t\tconst x = xToCanvas(z);\n\n\t\t\t\/\/ gridline\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(x, T);\n\t\t\tctx.lineTo(x, B);\n\t\t\tctx.stroke();\n\n\t\t\t\/\/ bottom tick\n\t\t\tctx.strokeStyle = \"#2a2a2a\";\n\t\t\tctx.lineWidth = 3;\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(x, B);\n\t\t\tctx.lineTo(x, B + 12);\n\t\t\tctx.stroke();\n\n\t\t\t\/\/ bottom label\n\t\t\tlabelWithBg(formatPct(pct), x, B + 16, \"center\", \"top\");\n\n\t\t\t\/\/ top tick\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(x, T);\n\t\t\tctx.lineTo(x, T - 12);\n\t\t\tctx.stroke();\n\n\t\t\t\/\/ top label (recurrence interval)\n\t\t\tlabelWithBg(formatRI(100 \/ pct), x, T - 26, \"center\", \"bottom\");\n\n\t\t\tctx.strokeStyle = \"#7f7f7f\";\n\t\t\tctx.lineWidth = 2;\n\t\t}\n\n\t\tctx.textBaseline = \"alphabetic\";\n\n\t\t\/\/ horizontal grid + y ticks\n\t\tconst yTicks = computeYTicks(yMax);\n\t\tctx.textAlign = \"right\";\n\t\tctx.textBaseline = \"middle\";\n\t\tctx.strokeStyle = \"#7f7f7f\";\n\t\tctx.lineWidth = 2;\n\t\tfor(const yt of yTicks){\n\t\t\tconst y = stageToCanvas(yt);\n\n\t\t\t\/\/ gridline (skip top-most line at yMax to avoid crossing labels)\n\t\t\tif(yt < yMax - 1e-6){\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(L, y);\n\t\t\t\tctx.lineTo(R, y);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\n\t\t\t\/\/ tick left and right (skip at very top)\n\t\t\tif(yt < yMax - 1e-6){\n\t\t\t\tctx.strokeStyle = \"#2a2a2a\";\n\t\t\t\tctx.lineWidth = 3;\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(L-12, y);\n\t\t\t\tctx.lineTo(L, y);\n\t\t\t\tctx.stroke();\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.moveTo(R, y);\n\t\t\t\tctx.lineTo(R+12, y);\n\t\t\t\tctx.stroke();\n\t\t\t}\n\n\t\t\t\/\/ label\n\t\t\tctx.fillStyle = \"#222\";\n\t\t\tctx.font = \"24px Arial\";\n\t\t\tctx.fillText(formatY(yt), L-18, y);\n\n\t\t\tctx.strokeStyle = \"#7f7f7f\";\n\t\t\tctx.lineWidth = 2;\n\t\t}\n\n\t\tctx.textBaseline = \"alphabetic\";\n\n\t\t\/\/ small y ticks (minor) + faint gridlines\n\t\tconst minor = computeMinorYTicks(yMax, yTicks);\n\n\t\t\/\/ faint gridlines for minor ticks\n\t\tctx.strokeStyle = \"#c8c8c8\";\n\t\tctx.lineWidth = 1;\n\t\tfor(const yt of minor){\n\t\t\tconst y = stageToCanvas(yt);\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(L, y);\n\t\t\tctx.lineTo(R, y);\n\t\t\tctx.stroke();\n\t\t}\n\n\t\t\/\/ small tick marks at left\/right\n\t\tctx.strokeStyle = \"#2a2a2a\";\n\t\tctx.lineWidth = 2;\n\t\tfor(const yt of minor){\n\t\t\tconst y = stageToCanvas(yt);\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(L-7, y);\n\t\t\tctx.lineTo(L, y);\n\t\t\tctx.stroke();\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(R, y);\n\t\t\tctx.lineTo(R+7, y);\n\t\t\tctx.stroke();\n\t\t}\n\n\t\tctx.restore();\n\t}\n\n\tfunction computeYTicks(ymax){\n\t\t\/\/ Major log ticks at powers of 10 between yMin and ymax\n\t\tconst ticks = [];\n\t\tconst k0 = Math.floor(Math.log10(yMin));\n\t\tconst k1 = Math.ceil(Math.log10(ymax));\n\t\tfor(let k=k0; k<=k1; k++){\n\t\t\tconst v = Math.pow(10, k);\n\t\t\tif(v >= yMin - 1e-12 && v <= ymax + 1e-12) ticks.push(v);\n\t\t}\n\t\treturn ticks;\n\t}\n\n\tfunction computeMinorYTicks(ymax, majorTicks){\n\t\t\/\/ Minor ticks at 2..9 * 10^k (no labels)\n\t\tconst ticks = [];\n\t\tconst k0 = Math.floor(Math.log10(yMin));\n\t\tconst k1 = Math.ceil(Math.log10(ymax));\n\t\tfor(let k=k0; k<=k1; k++){\n\t\t\tconst base = Math.pow(10, k);\n\t\t\tfor(let m=2; m<=9; m++){\n\t\t\t\tconst v = m * base;\n\t\t\t\tif(v >= yMin - 1e-12 && v <= ymax + 1e-12) ticks.push(v);\n\t\t\t}\n\t\t}\n\t\treturn ticks;\n\t}\n\n\tfunction niceStep(v){\n\t\tconst pow = Math.pow(10, Math.floor(Math.log10(v)));\n\t\tconst n = v \/ pow;\n\t\tif(n <= 1) return 1*pow;\n\t\tif(n <= 2) return 2*pow;\n\t\tif(n <= 5) return 5*pow;\n\t\treturn 10*pow;\n\t}\n\n\tfunction formatPct(p){\n\t\t\/\/ keep 0.5, 99.5, 0.1 formatting\n\t\tif(p === 0.5 || p === 99.5 || p === 0.1) return String(p);\n\t\tif(Math.abs(p - Math.round(p)) < 1e-9) return String(Math.round(p));\n\t\treturn String(p);\n\t}\n\n\tfunction formatY(v){\n\t\t\/\/ Format discharge labels: 1, 10, 100, 1000, 10000, ...\n\t\tif(v >= 1000){\n\t\t\treturn String(Math.round(v)).replace(\/\\B(?=(\\d{3})+(?!\\d))\/g, \",\");\n\t\t}\n\t\tif(v >= 1){\n\t\t\treturn String(Math.round(v));\n\t\t}\n\t\treturn v.toExponential(0);\n\t}\n\n\tfunction formatRI(ri){\n\t\t\/\/ Match the visual style in the mockup: show 1.01, 1.05, 1.11, 1.33, 2, 4, 10, 20, 100, 1000, etc.\n\t\tif(ri < 2){\n\t\t\treturn ri.toFixed(2).replace(\/0+$\/,\"\").replace(\/\\.$\/,\"\");\n\t\t}\n\t\tif(ri < 10){\n\t\t\treturn ri.toFixed(2).replace(\/0+$\/,\"\").replace(\/\\.$\/,\"\");\n\t\t}\n\t\tif(ri < 100){\n\t\t\treturn String(Math.round(ri));\n\t\t}\n\t\t\/\/ keep 100, 1000\n\t\treturn String(Math.round(ri));\n\t}\n\n\tfunction drawPoints(){\n\t\tif(dataPoints.length === 0) return;\n\n\t\tconst r = pointRadius(dataPoints.length);\n\n\t\tctx.save();\n\t\tctx.lineWidth = 2;\n\t\tfor(const pt of dataPoints){\n\t\t\tconst z = probToX(pt.p);\n\t\t\tconst x = xToCanvas(z);\n\t\t\tconst y = stageToCanvas(pt.y);\n\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(x, y, r, 0, 2*Math.PI);\n\t\t\tctx.fillStyle = \"#12a4d9\";\n\t\t\tctx.fill();\n\t\t\tctx.strokeStyle = \"#1b1b1b\";\n\t\t\tctx.stroke();\n\t\t}\n\t\tctx.restore();\n\t}\n\n\tfunction drawTrendLine(){\n\t\tconst L = G.left;\n\t\tconst R = canvas.width - G.right;\n\t\tconst T = G.top;\n\t\tconst B = canvas.height - G.bottom;\n\n\t\tctx.save();\n\t\t\/\/ line\n\t\tctx.strokeStyle = \"#2e4fa2\";\n\t\tctx.lineWidth = 5;\n\t\tctx.beginPath();\n\t\tctx.moveTo(anchors.a.x, anchors.a.y);\n\t\tctx.lineTo(anchors.b.x, anchors.b.y);\n\t\tctx.stroke();\n\n\t\t\/\/ endpoints\n\t\tfor(const k of [\"a\",\"b\"]){\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(anchors[k].x, anchors[k].y, 8, 0, 2*Math.PI);\n\t\t\tctx.fillStyle = \"#21b14c\";\n\t\t\tctx.fill();\n\t\t\tctx.lineWidth = 3;\n\t\t\tctx.strokeStyle = \"#1b1b1b\";\n\t\t\tctx.stroke();\n\t\t}\n\n\t\t\/\/ keep anchors on border, just in case\n\t\tanchors.a = nearestBorderPoint(anchors.a.x, anchors.a.y);\n\t\tanchors.b = nearestBorderPoint(anchors.b.x, anchors.b.y);\n\n\t\t\/\/ subtle info text (optional)\n\t\tctx.restore();\n\t}\n\n\tfunction tableRow(){\n\t\tconst tr = document.createElement(\"tr\");\n\t\tconst cols = [\"year\",\"stage\",\"discharge\",\"rank\",\"prob\",\"ri\"];\n\t\tfor(const c of cols){\n\t\t\tconst td = document.createElement(\"td\");\n\t\t\tconst inp = document.createElement(\"input\");\n\t\t\tinp.type = \"text\";\n\t\t\tinp.inputMode = \"decimal\";\n\t\t\tinp.autocomplete = \"off\";\n\t\t\tinp.spellcheck = false;\n\t\t\tinp.dataset.col = c;\n\t\t\ttd.appendChild(inp);\n\t\t\ttr.appendChild(td);\n\t\t}\n\t\treturn tr;\n\t}\n\n\tfunction makeRows(n){\n\t\ttbody.innerHTML = \"\";\n\t\tfor(let i=0;i<n;i++){\n\t\t\ttbody.appendChild(tableRow());\n\t\t}\n\t}\n\n\tfunction clearRows(){\n\t\tconst inputs = tbody.querySelectorAll(\"input\");\n\t\tfor(const inp of inputs) inp.value = \"\";\n\t}\n\n\tfunction resetAll(){\n\t\tsetMessage(\"\");\n\t\tdataPoints = [];\n\t\tyMin = 1;\n\t\tyMax = 1000;\n\t\tmakeRows(0);\n\t\tdefaultAnchors();\n\t\tdraw();\n\t}\n\n\tfunction parseDataFromTable(){\n\t\tconst rows = Array.from(tbody.querySelectorAll(\"tr\"));\n\t\tconst pts = [];\n\t\tlet hasDischargeProb = false;\n\n\t\tfor(const r of rows){\n\t\t\tconst yStr = r.querySelector('input[data-col=\"discharge\"]').value.trim();\n\t\t\tconst probStr  = r.querySelector('input[data-col=\"prob\"]').value.trim();\n\n\t\t\tif(yStr === \"\" &#038;&#038; probStr === \"\") continue;\n\n\t\t\tconst yv = Number(yStr);\n\t\t\tconst prob = Number(probStr);\n\n\t\t\tif(!Number.isFinite(yv) || !Number.isFinite(prob)){\n\t\t\t\treturn {ok:false, err:\"Discharge and Probability must be numeric (blank rows are allowed).\"};\n\t\t\t}\n\t\t\tif(prob <= 0 || prob >= 100){\n\t\t\t\treturn {ok:false, err:\"Probability values must be between 0 and 100 (percent).\"};\n\t\t\t}\n\t\t\tif(yv <= 0){\n\t\t\t\treturn {ok:false, err:\"Discharge values must be greater than 0 for a log-scale y-axis.\"};\n\t\t\t}\n\t\t\thasDischargeProb = true;\n\t\t\tpts.push({p: prob, y: yv});\n\t\t}\n\n\t\tif(!hasDischargeProb){\n\t\t\treturn {ok:false, err:\"Enter values in Discharge (cfs) and Probability (percent), then click Plot.\"};\n\t\t}\n\n\t\treturn {ok:true, pts: pts};\n\t}\n\n\tfunction cellInputs(){\n\t\treturn Array.from(tbody.querySelectorAll(\"input\"));\n\t}\n\n\tfunction cellIndexFromInput(inp){\n\t\tconst row = inp.closest(\"tr\");\n\t\tif(!row) return null;\n\t\tconst rows = Array.from(tbody.querySelectorAll(\"tr\"));\n\t\tconst r = rows.indexOf(row);\n\t\tconst colOrder = [\"year\",\"stage\",\"discharge\",\"rank\",\"prob\",\"ri\"];\n\t\tconst c = colOrder.indexOf(inp.dataset.col || \"\");\n\t\tif(r < 0 || c < 0) return null;\n\t\treturn {r,c};\n\t}\n\n\tfunction inputAt(r, c){\n\t\tconst rows = Array.from(tbody.querySelectorAll(\"tr\"));\n\t\tif(r < 0 || r >= rows.length) return null;\n\t\tconst colOrder = [\"year\",\"stage\",\"discharge\",\"rank\",\"prob\",\"ri\"];\n\t\tif(c < 0 || c >= colOrder.length) return null;\n\t\treturn rows[r].querySelector(`input[data-col=\"${colOrder[c]}\"]`);\n\t}\n\n\tfunction handlePaste(evt){\n\t\tconst target = evt.target;\n\t\tif(!(target instanceof HTMLInputElement)) return;\n\n\t\tconst text = (evt.clipboardData || window.clipboardData).getData(\"text\");\n\t\tif(!text) return;\n\n\t\t\/\/ If it's a single value (no newline\/tab), let the browser handle it\n\t\tif(!\/[\\t\\r\\n]\/.test(text)) return;\n\n\t\tconst start = cellIndexFromInput(target);\n\t\tif(!start) return;\n\n\t\tevt.preventDefault();\n\n\t\tconst lines = text.replace(\/\\r\\n\/g,\"\\n\").replace(\/\\r\/g,\"\\n\").split(\"\\n\");\n\t\t\/\/ Drop trailing empty line from clipboard\n\t\twhile(lines.length && lines[lines.length-1].trim()===\"\") lines.pop();\n\n\t\tfor(let i=0;i<lines.length;i++){\n\t\t\tconst cells = lines[i].split(\"\\t\");\n\t\t\tfor(let j=0;j<cells.length;j++){\n\t\t\t\tconst inp = inputAt(start.r + i, start.c + j);\n\t\t\t\tif(inp){\n\t\t\t\t\tinp.value = cells[j].trim();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t\/\/ Enable spreadsheet-style paste (tabs\/rows) into the table\n\ttbody.addEventListener(\"paste\", handlePaste);\n\n\t\/\/ Events\n\tmakeBtn.addEventListener(\"click\", () => {\n\t\tsetMessage(\"\");\n\t\tconst n = Number(nRowsInput.value);\n\t\tif(!Number.isInteger(n) || n < MIN_ROWS || n > MAX_ROWS){\n\t\t\tsetMessage(`Number of rows must be from ${MIN_ROWS} to ${MAX_ROWS}.`);\n\t\t\treturn;\n\t\t}\n\t\tmakeRows(n);\n\t});\n\n\tclearBtn.addEventListener(\"click\", () => {\n\t\tsetMessage(\"\");\n\t\tclearRows();\n\t\tdataPoints = [];\n\t\tyMin = 1;\n\t\tyMax = 1000;\n\t\tdefaultAnchors();\n\t\tdraw();\n\t});\n\n\tprintBtn.addEventListener(\"click\", () => {\n\t\tsetMessage(\"\");\n\t\t\/\/ Browser print dialog allows \"Save as PDF\" in Chrome\/Edge\n\t\tdraw(); \/\/ ensure canvas is up-to-date for printing\n\t\twindow.print();\n\t});\n\n\tresetBtn.addEventListener(\"click\", () => {\n\t\tnRowsInput.value = String(MIN_ROWS);\n\t\tresetAll();\n\t});\n\n\tplotBtn.addEventListener(\"click\", () => {\n\t\tsetMessage(\"\");\n\t\tconst parsed = parseDataFromTable();\n\t\tif(!parsed.ok){\n\t\t\tsetMessage(parsed.err);\n\t\t\treturn;\n\t\t}\n\t\tdataPoints = parsed.pts.slice();\n\n\t\t\/\/ Recompute log-axis bounds from the data\n\t\tyMax = computeYMax();\n\n\t\tconst plotL = G.left;\n\t\tconst plotR = canvas.width - G.right;\n\t\tconst plotB = canvas.height - G.bottom;\n\n\t\tanchors.a = nearestBorderPoint(plotL, plotB);      \/\/ bottom-left\n\t\tanchors.b = nearestBorderPoint(plotR, stageToCanvas(yMax)); \/\/ top-right\n\n\t\tdraw();\n\t});\n\n\t\/\/ Canvas dragging\n\tfunction pointerPos(evt){\n\t\tconst rect = canvas.getBoundingClientRect();\n\t\tconst x = (evt.clientX - rect.left) * (canvas.width \/ rect.width);\n\t\tconst y = (evt.clientY - rect.top) * (canvas.height \/ rect.height);\n\t\treturn {x,y};\n\t}\n\n\tcanvas.addEventListener(\"pointerdown\", (evt) => {\n\t\tconst {x,y} = pointerPos(evt);\n\t\tconst hit = anchorHitTest(x,y);\n\t\tif(hit){\n\t\t\tdragging = hit;\n\t\t\tcanvas.setPointerCapture(evt.pointerId);\n\t\t}\n\t});\n\n\tcanvas.addEventListener(\"pointermove\", (evt) => {\n\t\tif(!dragging) return;\n\t\tconst {x,y} = pointerPos(evt);\n\t\tanchors[dragging] = nearestBorderPoint(x,y);\n\t\tdraw();\n\t});\n\n\tfunction endDrag(evt){\n\t\tif(!dragging) return;\n\t\tdragging = null;\n\t\ttry{ canvas.releasePointerCapture(evt.pointerId); }catch(e){}\n\t\tdraw();\n\t}\n\tcanvas.addEventListener(\"pointerup\", endDrag);\n\tcanvas.addEventListener(\"pointercancel\", endDrag);\n\n\t\/\/ Initialize\n\tresetAll();\n\tmakeRows(MIN_ROWS);\n})();\n<\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Flood Probability Plotter Make rows. Plot Clear Reset Print\/PDF Year Stage (feet) Discharge (cfs) Rank Probability Recurrence Interval Required columns: Discharge (cfs) and Probability (percent). Probability is exceedance probability in percent (e.g., 10 for 10%). Made by Dr. G.S. Springer and ChatGPT 5.2<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-8","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/pages\/8","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/comments?post=8"}],"version-history":[{"count":5,"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/pages\/8\/revisions"}],"predecessor-version":[{"id":13,"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/pages\/8\/revisions\/13"}],"wp:attachment":[{"href":"https:\/\/sites.ohio.edu\/greg-springer\/wp-json\/wp\/v2\/media?parent=8"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}