Add pill backgrounds to on-graph labels and explain PWAT
Applies the dark rounded-rect pillLabel treatment to labels that overlay graph content across nlos.js scenes (knife-edge, earth curve, LOS, ray trace, duct, duct types, real terrain). Fixes the "deep shadow" label truncating off the right edge of the J(v) plot by right-aligning it to the plot frame. The readout annotation now flips to the left side when the cursor nears the right edge. Adds a plain-language explanation of PWAT (precipitable water) in why-signals-bend.md so readers don't have to already know the term.
This commit is contained in:
parent
0a5ddee7d8
commit
a96d43cfbe
2 changed files with 33 additions and 23 deletions
|
|
@ -140,7 +140,9 @@ A few things I didn't expect going in, now painfully clear from the data.
|
|||
|
||||
<div id="scene-band-time"></div>
|
||||
|
||||
**Humidity is a trade.** At 10 GHz, 20 to 30 mm of precipitable water is the sweet spot. Too dry and the surface refractivity stays low, too wet and the column destabilizes. At 24 GHz, drier is always better, because the 22.235 GHz water vapor line happily eats whatever signal the atmosphere just delivered.
|
||||
**Humidity is a trade.** Precipitable water, or PWAT, is the total amount of water vapor in a column of atmosphere above you, expressed as the depth you would get if you condensed all of it into liquid and let it puddle on the ground. A PWAT of 25 mm means that if every water molecule above your antenna fell out of the sky at once, you would be standing in an inch of water. It is the number forecasters use to describe how wet the air is in bulk, and for microwave propagation it is the variable that matters, because the refractivity gradient that bends signals depends on water vapor more than any other single thing.
|
||||
|
||||
At 10 GHz, 20 to 30 mm of PWAT is the sweet spot. Too dry and the surface refractivity stays low, too wet and the column destabilizes. At 24 GHz, drier is always better, because the 22.235 GHz water vapor line happily eats whatever signal the atmosphere just delivered.
|
||||
|
||||
<div id="scene-pwat"></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -302,8 +302,8 @@
|
|||
|
||||
drawAntenna(ctx, xA, groundY, h1px, COLORS.orange);
|
||||
drawAntenna(ctx, xB, groundY, h2px, COLORS.orange);
|
||||
labelText(ctx, 'A', xA, groundY - h1px - 10, COLORS.orange, 'center');
|
||||
labelText(ctx, 'B', xB, groundY - h2px - 10, COLORS.orange, 'center');
|
||||
pillLabel(ctx, 'A', xA, groundY - h1px - 12, COLORS.orange, 'center', 'middle');
|
||||
pillLabel(ctx, 'B', xB, groundY - h2px - 12, COLORS.orange, 'center', 'middle');
|
||||
|
||||
s.setReadout('status', blocked ? 'BLOCKED' : 'CLEAR');
|
||||
s.setReadout('clearance', `${clearanceM.toFixed(1)} m`);
|
||||
|
|
@ -419,7 +419,7 @@
|
|||
ctx.beginPath();
|
||||
ctx.arc(cx, peakY, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
labelText(ctx, `bulge ${bulgeM.toFixed(1)} m`, cx, peakY - 8, COLORS.yellow, 'center');
|
||||
pillLabel(ctx, `bulge ${bulgeM.toFixed(1)} m`, cx, peakY - 10, COLORS.yellow, 'center', 'middle');
|
||||
|
||||
s.setReadout('horizonA', `${dH1.toFixed(1)} km`);
|
||||
s.setReadout('horizonB', `${dH2.toFixed(1)} km`);
|
||||
|
|
@ -657,7 +657,7 @@
|
|||
ctx.strokeStyle = COLORS.dim;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(xA, yLOS); ctx.lineTo(xB, yLOS); ctx.stroke();
|
||||
labelText(ctx, 'line of sight', (xA + xB) / 2, yLOS - 6, COLORS.dim, 'center');
|
||||
pillLabel(ctx, 'line of sight', (xA + xB) / 2, yLOS - 10, COLORS.dim, 'center', 'middle');
|
||||
|
||||
// knife edge
|
||||
ctx.fillStyle = '#3a4150';
|
||||
|
|
@ -689,7 +689,7 @@
|
|||
ctx.lineTo(obsX + 17, obsTopY + 4 * dir);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
labelText(ctx, `h = ${obsH.toFixed(1)} m`, obsX + 20, (yLOS + obsTopY) / 2, COLORS.yellow, 'left', 'middle');
|
||||
pillLabel(ctx, `h = ${obsH.toFixed(1)} m`, obsX + 20, (yLOS + obsTopY) / 2, COLORS.yellow, 'left', 'middle');
|
||||
}
|
||||
|
||||
// Compute v
|
||||
|
|
@ -718,8 +718,8 @@
|
|||
|
||||
drawAntenna(ctx, xA, yLOS, 0, COLORS.orange);
|
||||
drawAntenna(ctx, xB, yLOS, 0, COLORS.orange);
|
||||
labelText(ctx, 'A', xA, yLOS + 14, COLORS.orange, 'center');
|
||||
labelText(ctx, 'B', xB, yLOS + 14, COLORS.orange, 'center');
|
||||
pillLabel(ctx, 'A', xA, yLOS + 18, COLORS.orange, 'center', 'middle');
|
||||
pillLabel(ctx, 'B', xB, yLOS + 18, COLORS.orange, 'center', 'middle');
|
||||
|
||||
// Distance bracket along the bottom
|
||||
const bracketY = obsBottomY + 18;
|
||||
|
|
@ -775,11 +775,11 @@
|
|||
ctx.beginPath(); ctx.moveTo(xOf(0), pY); ctx.lineTo(xOf(0), pY + pH); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(pX, yOf(6)); ctx.lineTo(pX + pW, yOf(6)); ctx.stroke();
|
||||
ctx.restore();
|
||||
labelText(ctx, 'grazing (ν=0, 6 dB)', xOf(0) + 4, yOf(6) - 4, COLORS.dim);
|
||||
pillLabel(ctx, 'grazing (ν=0, 6 dB)', xOf(0) + 6, yOf(6) - 10, COLORS.dim, 'left', 'middle');
|
||||
|
||||
// Region labels
|
||||
labelText(ctx, 'free space', xOf(-2), pY + 14, COLORS.green);
|
||||
labelText(ctx, 'deep shadow', xOf(3.2), pY + 14, COLORS.red);
|
||||
pillLabel(ctx, 'free space', pX + 6, pY + 14, COLORS.green, 'left', 'middle');
|
||||
pillLabel(ctx, 'deep shadow', pX + pW - 6, pY + 14, COLORS.red, 'right', 'middle');
|
||||
|
||||
// J(v) curve
|
||||
ctx.strokeStyle = COLORS.blue;
|
||||
|
|
@ -793,7 +793,7 @@
|
|||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
labelText(ctx, 'J(ν) — ITU-R P.526', xOf(1.2), yOf(12), COLORS.blue);
|
||||
pillLabel(ctx, 'J(ν) — ITU-R P.526', xOf(1.2), yOf(12), COLORS.blue, 'left', 'middle');
|
||||
|
||||
// Current point with readout annotation
|
||||
const px = xOf(v), py = yOf(lossDb);
|
||||
|
|
@ -806,7 +806,15 @@
|
|||
ctx.moveTo(px, py); ctx.lineTo(pX, py);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
labelText(ctx, `ν = ${v.toFixed(2)}, ${lossDb.toFixed(1)} dB`, px + 8, py - 8, COLORS.yellow);
|
||||
{
|
||||
const rtext = `ν = ${v.toFixed(2)}, ${lossDb.toFixed(1)} dB`;
|
||||
ctx.save();
|
||||
ctx.font = '12px system-ui, sans-serif';
|
||||
const rw = ctx.measureText(rtext).width + 16;
|
||||
ctx.restore();
|
||||
const rightSide = px + 8 + rw < pX + pW;
|
||||
pillLabel(ctx, rtext, rightSide ? px + 8 : px - 8, py - 10, COLORS.yellow, rightSide ? 'left' : 'right', 'middle');
|
||||
}
|
||||
|
||||
s.setReadout('v', v.toFixed(2));
|
||||
s.setReadout('loss', `${lossDb.toFixed(1)} dB`);
|
||||
|
|
@ -1263,12 +1271,12 @@
|
|||
if (hit) {
|
||||
ctx.fillStyle = rayColor;
|
||||
ctx.beginPath(); ctx.arc(hit.x, hit.y, 5, 0, Math.PI * 2); ctx.fill();
|
||||
labelText(ctx, `${(hit.dxm / 1000).toFixed(1)} km`, hit.x, hit.y - 10, rayColor, 'center');
|
||||
pillLabel(ctx, `${(hit.dxm / 1000).toFixed(1)} km`, hit.x, hit.y - 12, rayColor, 'center', 'middle');
|
||||
}
|
||||
|
||||
// Antenna
|
||||
drawAntenna(ctx, xLaunch, launchGroundY, s.values.height * pxPerM_y, COLORS.orange);
|
||||
labelText(ctx, 'TX', xLaunch + 8, launchGroundY - s.values.height * pxPerM_y - 6, COLORS.orange);
|
||||
pillLabel(ctx, 'TX', xLaunch + 8, launchGroundY - s.values.height * pxPerM_y - 10, COLORS.orange, 'left', 'middle');
|
||||
|
||||
// Legend
|
||||
labelText(ctx, 'effective-Earth view (ground curves at d²/2R). Past rays fade as you move sliders.', margin.l + 4, margin.t + 14, COLORS.dim);
|
||||
|
|
@ -1338,7 +1346,7 @@
|
|||
ctx.lineTo(margin.l + pW, groundY - duct * pxPerM_y);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
labelText(ctx, `duct top (${duct} m)`, margin.l + pW - 6, groundY - duct * pxPerM_y - 4, COLORS.cyan, 'right');
|
||||
pillLabel(ctx, `duct top (${duct} m)`, margin.l + pW - 6, groundY - duct * pxPerM_y - 8, COLORS.cyan, 'right', 'middle');
|
||||
|
||||
// Within the duct, effective-earth ray curvature corresponds to dN/dh = -dN/thickness_km.
|
||||
// Outside, standard atmosphere.
|
||||
|
|
@ -1579,7 +1587,7 @@
|
|||
ctx.lineTo(ax + 12, ay + 4);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
labelText(ctx, annotText, ax + 34, ay, COLORS.yellow, 'left', 'middle');
|
||||
pillLabel(ctx, annotText, ax + 34, ay, COLORS.yellow, 'left', 'middle');
|
||||
|
||||
// ---- Right panel: ray paths ----
|
||||
const qX = leftW + 55, qY = 30, qW = w - qX - 15, qH = h - 60;
|
||||
|
|
@ -1618,9 +1626,9 @@
|
|||
ctx.fillRect(qX, yrayOf(0), qW, qY + qH - yrayOf(0));
|
||||
|
||||
// Duct-band labels
|
||||
if (active === 'surface') labelText(ctx, 'duct region (ground to ~100 m)', qX + 8, yrayOf(100) - 4, COLORS.cyan);
|
||||
if (active === 'elevated') labelText(ctx, 'duct region (200–350 m)', qX + 8, yrayOf(350) - 4, COLORS.cyan);
|
||||
if (active === 'evap') labelText(ctx, 'duct region (sea surface to ~30 m)', qX + 8, yrayOf(30) - 4, COLORS.cyan);
|
||||
if (active === 'surface') pillLabel(ctx, 'duct region (ground to ~100 m)', qX + 8, yrayOf(100) - 10, COLORS.cyan, 'left', 'middle');
|
||||
if (active === 'elevated') pillLabel(ctx, 'duct region (200–350 m)', qX + 8, yrayOf(350) - 10, COLORS.cyan, 'left', 'middle');
|
||||
if (active === 'evap') pillLabel(ctx, 'duct region (sea surface to ~30 m)', qX + 8, yrayOf(30) - 10, COLORS.cyan, 'left', 'middle');
|
||||
|
||||
// Illustrative signal paths (schematic)
|
||||
ctx.strokeStyle = COLORS.green;
|
||||
|
|
@ -1774,11 +1782,11 @@
|
|||
ctx.beginPath(); ctx.moveTo(margin.l, yOf(ductTop)); ctx.lineTo(margin.l + pW, yOf(ductTop)); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(margin.l, yOf(ductBottom)); ctx.lineTo(margin.l + pW, yOf(ductBottom)); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
labelText(ctx, 'elevated duct', margin.l + 8, yOf(ductTop) + 14, COLORS.cyan);
|
||||
pillLabel(ctx, 'elevated duct', margin.l + 8, yOf(ductTop) + 14, COLORS.cyan, 'left', 'middle');
|
||||
|
||||
// Earth-curvature annotation near the middle
|
||||
labelText(ctx, `Earth bulge at midpoint: ${maxBulge.toFixed(0)} m (k = 4/3)`,
|
||||
margin.l + pW / 2, margin.t + pH - 6, COLORS.dim, 'center');
|
||||
pillLabel(ctx, `Earth bulge at midpoint: ${maxBulge.toFixed(0)} m (k = 4/3)`,
|
||||
margin.l + pW / 2, margin.t + pH - 10, COLORS.dim, 'center', 'middle');
|
||||
|
||||
// Terrain fill
|
||||
ctx.fillStyle = '#2b3039';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue