@verifyhash/uv-index
v0.1.0
Published
Zero-dependency, network-free clear-sky UV-index estimator: solar-elevation-driven UVI, WHO exposure category, and burn-time guidance from lat/lon/date/time (+ optional ozone and elevation).
Downloads
32
Maintainers
Readme
uv-index
A zero-dependency, network-free clear-sky UV-index estimator for Node.js.
Give it a place (latitude / longitude), a date and time, and — optionally — the total-column ozone and site elevation. It computes the sun's elevation angle and derives an approximate clear-sky erythemal UV index (UVI), the matching WHO/WMO exposure category (Low / Moderate / High / Very High / Extreme), and a rough burn-time for fair skin.
Everything runs locally with the Node standard library. No network calls, no npm dependencies, no API keys.
Who it's for
Developers building sun-safety, weather, or outdoor-planning features who want a fast, offline estimate of "how strong will the sun be here, right now, if the sky is clear?" — for example:
- a hiking / beach / gardening app that shades a timeline by UV strength,
- a "reapply sunscreen" nudge that scales with the clear-sky UV ceiling,
- a backfill for historical or future dates where no measured UVI feed exists,
- a sanity bound to cross-check a measured/forecast UVI feed against.
It is a planning-grade ceiling, not a sensor. If you need the actual UVI right now, use a measured feed (they capture the clouds and haze this model deliberately ignores).
Install / use
No install step — it is a single self-contained module. Copy the uv-index/
folder into your project and require it:
const { estimateUVIndex } = require('./uv-index');
const r = estimateUVIndex({
lat: 23.44, // + North
lon: 0, // + East
date: new Date('2023-06-21T12:00:00Z'), // a full UTC instant (date + time)
ozoneDU: 300, // optional, total-column ozone in Dobson Units (default 300)
elevationM: 0, // optional, site elevation in metres (default 0)
skinType: 'II', // optional, Fitzpatrick I–VI for burn time (default 'II')
});
console.log(r);
// {
// uvi: 12.499, // continuous clear-sky UV index
// uviRounded: 12, // rounded, as UVI is officially reported
// category: 'Extreme', // WHO/WMO band
// solarElevationDeg: 89.6, // sun altitude above the horizon
// sunAboveHorizon: true,
// burnTimeMinutes: 13, // ~minutes of clear-sky sun to one MED (skin II)
// inputs: { lat: 23.44, lon: 0, ozoneDU: 300, elevationM: 0, skinType: 'II' }
// }date is interpreted as a UTC instant (both date and time-of-day matter —
this is an instantaneous estimate, not a daily one). To model a specific local
clock time, convert it to UTC first.
Other exports
const {
estimateUVIndex, // the main estimator (above)
categoryFor, // (uvi) -> { category, min, max } — WHO band for a UVI value
burnTimeMinutes, // (uvi, skinType='II') -> minutes to one MED, or null at UVI 0
solarElevation, // (lat, lon, Date) -> sun elevation in degrees (vendored math)
} = require('./uv-index');How the number is built
Solar elevation is computed with the NOAA solar-position algorithm (Meeus-derived), vendored into
lib/solar.js— see "Vendored solar math" below. If the sun is at or below the horizon, UVI is exactly0.Clear-sky UVI from the elevation
h:μ0 = sin(h) # = cos(solar zenith angle) UVI = 12.5 · μ0^2.42 · (Ω/300)^(-1.23) · (1 + 0.06 · z_km)12.5is the base coefficient tuned so an overhead sun at 300 DU ozone and sea level peaks near the low-Extreme band, matching observed clear-sky maxima of ~12 at sea level.2.42is a common empirical exponent for the erythemal (sunburn-weighted) UV response versus solar elevation.Ωis total-column ozone in Dobson Units; the-1.23exponent is the erythemal Radiation Amplification Factor (~1.2 fractional UV change per 1% ozone; literature values span ~1.1–1.4).z_kmis site elevation in km;+6% per kmsits in the field-observed ~5–10%/km range.
Category maps the UVI (rounded to the nearest whole number, as UVI is officially reported) to the WHO/WMO bands:
0–2 Low,3–5 Moderate,6–7 High,8–10 Very High,11+ Extreme.Burn time is the physical time to accumulate one Minimal Erythemal Dose (MED) for the chosen Fitzpatrick skin type:
t = MED / (UVI · 0.025 W/m² · 60 s)minutes, using MED values of 200 / 250 / 350 / 450 / 600 / 1000 J/m² for skin types I–VI.
Honest limits — read this
This is a CLEAR-SKY model. It answers "what is the sun's UV ceiling under a cloudless, clean sky?" and nothing more:
- No clouds. Real clouds can cut UVI by 50%+ (or, with bright broken cloud, briefly raise it slightly). This model never sees them, so on an overcast day its number will read too high.
- No aerosols / haze / pollution. Urban or smoky air absorbs UV; the model ignores it and will overestimate in hazy conditions.
- No surface albedo boost. Fresh snow (up to ~+90%), sand, and water reflect UV upward; the model does not add that, so it can read too low on snow.
- The ozone and elevation terms are approximations. The 300 DU reference, the
-1.23ozone exponent, and the+6%/kmaltitude factor are single-value fits, not a radiative-transfer computation. Real ozone varies ~230–500 DU by latitude and season; if you don't pass a value the model assumes 300 DU. - Geometric sun angle, no refraction. Near sunrise/sunset (elevation within ~0.5° of the horizon) the true, refracted sun may be marginally higher than the geometric elevation used here; the UV contribution there is negligible anyway.
- Mid-latitude clear-sky peaks read on the high side. Because there are no clouds or aerosols, a clear-sky summer noon at ~50° latitude lands near the top of the observed 7–9 window (this library returns ~9). That is the intended clear-sky ceiling, not a typical measured value.
- Not medical advice. Burn time is an order-of-magnitude physical estimate; actual sunburn onset depends on sunscreen, reflection, medication, and individual skin. This is not a substitute for a measured UVI feed.
Use it for planning, shading UIs, and offline estimates — not for anything where being wrong by a few index points on a cloudy or hazy day matters.
Vendored solar math
The solar-elevation geometry in lib/solar.js is a trimmed copy of the
sibling solar-calc project in this incubator (tools/solar-calc/index.js,
v0.1.0 — the solarElevation / NOAA sunPosition path). It is copied in on
purpose so uv-index has zero runtime dependencies and never networks to or
requires another package. Both are MIT-licensed and share the NOAA Global
Monitoring Laboratory Solar Calculator algorithm.
Running the tests
One command, no framework, no dependencies:
node test/index.test.jsThe suite covers: sun below the horizon → UVI 0, a high-UVI low-latitude solar-noon case, a documented mid-latitude clear-sky band plus a winter contrast, WHO category boundaries, ozone/elevation correction direction, burn-time physics, and invalid-input rejection.
License
MIT — see LICENSE.
