@verifyhash/solar-calc
v0.1.1
Published
Zero-dependency solar position & daylight math (NOAA algorithm): sunrise, sunset, solar noon, day length, elevation, azimuth, civil twilight, and golden hour for any latitude/longitude/date.
Maintainers
Readme
solar-calc
A tiny, zero-dependency Node library for solar geometry: sunrise, sunset, solar noon, day length, the Sun's elevation and azimuth at any instant, plus civil-twilight and golden-hour boundaries — for any latitude, longitude, and date.
Every export is a pure function: no I/O, no network, no daemon, no dependencies, and no mutation of its arguments. The math follows the algorithm published by NOAA's Solar Calculator, a simplified form of Jean Meeus, Astronomical Algorithms (2nd ed.). Against published almanac times it lands within about a minute at mid latitudes (see the test fixtures).
Who it's for
Developers building weather, photography, astronomy, or agriculture tools who need reliable solar numbers without pulling in a big ephemeris package or hitting an API:
- a weather dashboard showing today's sunrise/sunset and length of day;
- a photography planner that wants the golden-hour window for a shoot;
- an astronomy or stargazing app that needs when civil twilight ends;
- a solar/agriculture model that needs the Sun's elevation through the day.
Honest limits
- Accuracy is ~1 minute at mid latitudes and degrades toward the poles,
where a tiny error in the Sun's altitude maps to a large time swing. Near the
Arctic/Antarctic circles, treat sunrise/sunset times as approximate; the
polar-night / midnight-sun detection (returning
null) is reliable. - Elevation and azimuth are geometric — the Sun's true position. They do not add atmospheric refraction, so within ~0.5° of the horizon the apparent (refracted) Sun sits a little higher than the returned value.
- Sunrise/sunset use the standard −0.833° horizon (refraction + solar radius). One visible consequence: at the equator on an equinox the day comes out as ~12 h 07 m, not exactly 12 h — that offset is real, not a bug.
- This is a position library, not an irradiance/energy model: it tells you where the Sun is, not how much power hits a panel.
Install / use
No install step, no dependencies. Copy the folder in and require it:
const solar = require('./solar-calc');Conventions
| input | meaning |
|-------------|---------------------------------------------------------------------|
| lat | latitude in degrees, + North (New York ≈ 40.71) |
| lon | longitude in degrees, + East (New York ≈ -74.01) |
| dateInput | a JS Date (only its Y/M/D matter, read in UTC) or a plain { year, month, day } object with a 1-based month |
| tz | offset of the local clock from UTC in hours (US Eastern Standard Time = -5); defaults to 0 (UTC) |
Daily event functions (sunrise, sunset, solarNoon, twilight, golden hour)
return a JS Date for the true UTC instant of the event, or null when it
doesn't happen that day (polar night / midnight sun). To print one in local
time, use localClockMinutes(instant, tz) or format the Date with your own
tz.
The instantaneous functions solarElevation(lat, lon, date) and
solarAzimuth(lat, lon, date) take a full Date instant (date and
time, UTC) and need no tz — the instant already fixes the moment.
Worked example
const solar = require('./solar-calc');
// New York City, June solstice 2021. EDT is UTC−4.
const lat = 40.7128;
const lon = -74.006;
const tz = -4;
const date = { year: 2021, month: 6, day: 21 };
// Pretty-print a UTC event instant as local "HH:MM".
function localHM(instant) {
if (!instant) return 'none';
const m = solar.localClockMinutes(instant, tz);
const h = Math.floor(m / 60);
const min = Math.round(m % 60);
return `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
}
console.log('sunrise ', localHM(solar.sunrise(lat, lon, date, tz))); // 05:25
console.log('solar noon ', localHM(solar.solarNoon(lat, lon, date, tz))); // 12:58
console.log('sunset ', localHM(solar.sunset(lat, lon, date, tz))); // 20:31
const mins = solar.dayLengthMinutes(lat, lon, date, tz);
console.log('day length ', (mins / 60).toFixed(2), 'hours'); // 15.09 hours
// Golden hour for an evening shoot:
console.log('golden hour', localHM(solar.goldenHourEveningStart(lat, lon, date, tz)),
'→', localHM(solar.sunset(lat, lon, date, tz)));
// Where is the Sun at 15:00 local (= 19:00 UTC)?
const t = new Date(Date.UTC(2021, 5, 21, 19, 0, 0));
console.log('elevation ', solar.solarElevation(lat, lon, t).toFixed(1), '°');
console.log('azimuth ', solar.solarAzimuth(lat, lon, t).toFixed(1), '° from north');API
All angles are in degrees; all times/durations in minutes unless noted.
Daily events → Date (UTC instant) or null
| function | returns |
|----------|---------|
| solarNoon(lat, lon, dateInput, tz?) | instant the Sun is highest (always defined) |
| sunrise(lat, lon, dateInput, tz?) | upper limb clearing the horizon, or null |
| sunset(lat, lon, dateInput, tz?) | upper limb touching the horizon, or null |
| civilDawn(lat, lon, dateInput, tz?) | Sun 6° below horizon, rising, or null |
| civilDusk(lat, lon, dateInput, tz?) | Sun 6° below horizon, setting, or null |
| goldenHourMorningEnd(lat, lon, dateInput, tz?) | Sun reaches 6° elevation after sunrise, or null |
| goldenHourEveningStart(lat, lon, dateInput, tz?) | Sun descends to 6° elevation before sunset, or null |
The morning golden hour runs sunrise → goldenHourMorningEnd; the evening
golden hour runs goldenHourEveningStart → sunset.
Durations & raw quantities → number
| function | returns |
|----------|---------|
| dayLengthMinutes(lat, lon, dateInput, tz?) | minutes between sunrise and sunset; 0 on polar night, 1440 under the midnight sun |
| declination(dateInput, tz?) | Sun's declination in degrees (+ North) for that day |
| equationOfTime(dateInput, tz?) | apparent − mean solar time, in minutes (≈ −14 … +16 over a year) |
Instantaneous position → number (degrees)
| function | returns |
|----------|---------|
| solarElevation(lat, lon, date) | geometric elevation above the horizon; negative below. date is a full UTC Date instant |
| solarAzimuth(lat, lon, date) | compass bearing clockwise from true North (0 = N, 90 = E, 180 = S, 270 = W) |
Helper
| function | returns |
|----------|---------|
| localClockMinutes(instant, tz?) | minutes-after-midnight [0, 1440) on the local clock for a returned event instant, or null if passed null |
Internals exposed for testing
_sunPosition(t)— the raw NOAA kernel behind everything above. Takes the Julian centuryt(centuries since J2000.0) and returns{ declination, eqTime }: the Sun's declination in degrees and the equation of time in minutes (how far a sundial runs ahead of a mean clock). Exported with the underscore prefix so the test suite can pin it against NOAA reference values — treat it as internal; its shape is not covered by semver.
Running the tests
One command, no dependencies:
node test/solar.test.jsThe suite checks four known-city fixtures (New York, London, Sydney, Tokyo) against published sunrise/sunset times within ±2 minutes, verifies the equator-at-equinox ≈12-hour day, the Tromsø midnight-sun (no sunset) and polar-night (no sunrise) cases, cross-checks the daily and instantaneous code paths against each other, and asserts the twilight/golden-hour ordering. It exits non-zero if anything fails.
License
MIT.
Install
npm install @verifyhash/solar-calcconst solar = require('@verifyhash/solar-calc');
const lat = 40.7128, lon = -74.006, tz = -4; // New York City, EDT
const date = { year: 2021, month: 6, day: 21 };
solar.sunrise(lat, lon, date, tz); // Date — UTC instant of sunrise
solar.dayLengthMinutes(lat, lon, date, tz); // minutes of daylight