@verifyhash/sun-site-planner
v0.1.1
Published
Offline zero-dependency daytime sun-exposure planner: sun path, sunrise/sunset/solar-noon, noon altitude, and mask-aware hours-of-direct-sun for solar siting, gardening, and real-estate sun checks.
Maintainers
Readme
sun-site-planner
An offline, zero-dependency daytime sun-exposure planner for a single spot on a single day. Give it a latitude/longitude and a date and it tells you where the Sun travels, when it rises and sets, how high it climbs at midday — and, if you describe the trees, walls, and hills around you, how many hours of direct sun that exact spot actually gets.
This is a solar-siting engine, not a stargazing tool. It answers the questions a gardener, a solar-panel installer, a balcony grower, or a house-hunter asks:
- "Does this raised bed really get 6 hours of sun, or does the neighbour's oak eat the morning?"
- "Which corner of the yard stays lit longest in December?"
- "Is this south-facing roof worth panels once the chimney and the ridgeline to the east are accounted for?"
What it computes
For the requested day and location, planDay(...) returns:
path— the Sun's track as{ minuteOfDay, time, azimuthDeg, elevationDeg, aboveHorizon, directSun, obstructionDeg }samples at a fixed step (default every 10 minutes), from just before sunrise to just after sunset. Azimuth is degrees clockwise from true North (0 = N, 90 = E, 180 = S, 270 = W); elevation is degrees above the true horizon.sunrise,sunset,solarNoon— each as a local clock minute-of-day, an"HH:MM"label, and the underlying UTC instant.maxElevationDeg— the noon-altitude (highest the Sun gets that day).dayLengthMinutes— astronomical sunrise-to-sunset length.hoursOfDirectSun(anddirectSunMinutes) — the headline number. With no obstructions this equals the day length. With ahorizonMaskit is the total time the Sun is above both the true horizon and whatever blocks the sky in its current direction.
The horizon mask (the differentiator)
Real sites are shaded. You describe the surroundings with a horizonMask: an
array of { azimuthDeg, elevationDeg } samples giving the apparent height of the
skyline in each compass direction (e.g. a 40°-high wall due east is
{ azimuthDeg: 90, elevationDeg: 40 }). The engine treats the samples as a
closed profile around you and interpolates every direction in between — including
across the 0°/360° seam — then counts a minute as "direct sun" only when the Sun
clears that profile. A tall obstruction to the east steals the morning; one to
the west steals the evening. Omit the mask for an open horizon.
Usage
const { planDay } = require('./index.js');
const result = planDay({
lat: 40.7128, // + North
lon: -74.006, // + East
date: '2026-06-21', // "YYYY-MM-DD" (or a Date / {year,month,day})
tzOffsetMinutes: -240, // local clock offset from UTC, for the HH:MM labels
stepMinutes: 10, // sun-path sampling step (optional)
horizonMask: [ // optional; omit for open sky
{ azimuthDeg: 90, elevationDeg: 40 }, // 40°-high wall due east
{ azimuthDeg: 180, elevationDeg: 0 },
{ azimuthDeg: 270, elevationDeg: 0 },
],
});
console.log(result.solarNoon.time); // "12:58"
console.log(result.maxElevationDeg); // ~72.7 (noon altitude)
console.log(result.hoursOfDirectSun); // hours the spot is actually litUI (single-page, offline)
index.html is a self-contained page that drives this exact engine — just
open the file in a browser (double-click it, or File → Open). No build
step, no server, and no internet connection: the engine and its vendored
solar-calc are inlined into the page, and there are no scripts, styles, or
fonts loaded from any other host. Nothing you enter leaves your device.
What it does:
- Inputs for latitude, longitude, date, and UTC offset. A "Use my location" button reads the browser's geolocation and fills lat/lon; if permission is denied or geolocation is unavailable it says so and you just type the coordinates in. The UTC offset is estimated from longitude until you edit it.
- A sun-path chart plots the Sun's elevation across the day with dashed
sunrise / solar-noon / sunset markers, driven by
planDay()'spath/sunrise/solarNoon/sunset. Peak altitude and hours of direct sun are shown as readouts (with a noon-shadow length for intuition). - A horizon sketcher — eight compass sliders (N through NW) for the skyline
height in each direction — builds a
horizonMaskand live-updates the hours-of-direct-sun total and the chart's shaded skyline on every drag (no Generate button). "Clear skyline" returns to open sky. - Light and dark themes follow the OS via
color-scheme/ aprefers-color-schememedia query.
The page is generated by build.js, which inlines the engine into the
ENGINE_START/ENGINE_END markers in index.html. After any engine change,
re-run node build.js and commit the regenerated index.html.
Who it's for
Home solar / panel siting, food gardeners judging bed placement, balcony and window growers, and buyers or renters comparing how much sun a yard or room gets across the year. Anyone who needs a defensible "hours of sun" number for a specific, partly-shaded spot — with no app, account, or internet connection.
Honest limits
- Geometry only. The Sun's position uses the NOAA / Meeus algorithm and is good
to a fraction of a degree, but
hoursOfDirectSuncounts only geometric line-of-sight: it does not model thin cloud, haze, diffuse skylight, or a panel/leaf still doing useful work in shade. It answers "is the Sun's disc visible from here?", not "how many watt-hours land here." - Elevations near 0° ignore atmospheric refraction beyond the standard −0.833° sunrise/sunset allowance, so a minute or two at the very edges is approximate.
- The horizon mask is only as good as your samples. A sparse mask is linearly interpolated, so sharp gaps between buildings are smoothed over — sample more densely where the skyline is jagged.
- Single day, single point. It does not sum a season or map shade across an area.
- Times are wall-clock via a fixed
tzOffsetMinutes; it does not know daylight-saving transitions — pass the offset in effect on that date.
Vendored solar math
The solar geometry (sunrise, sunset, solarNoon, dayLengthMinutes,
solarElevation, solarAzimuth) is a committed copy of the sibling
solar-calc module (NOAA algorithm), living at
vendor/solar-calc/ with its MIT LICENSE. It is copied
in on purpose so this project is fully self-contained — there is no
cross-project require and no network access anywhere in the engine.
Tests
One command, zero dependencies:
node test/engine.test.jsIt checks the noon altitude against almanac references (New York summer solstice
≈ 72.7°, Sydney winter solstice ≈ 32.7°), that sunrise < solarNoon < sunset,
that hours-of-direct-sun with no mask equals the day length exactly, and that a
tall east (or west) obstruction makes it strictly less. Exit code 0 = all pass.
