ग्रहण (grahan) — "eclipse"
Astronomy-grade sky calculations with cultural intelligence. Sun and Moon positions, sunrise/sunset, moon phases — and cultural layers computed from them, starting with the Vedic panchang. Zero dependencies, pure functions, runs in Node ≥ 18, browsers, and edge runtimes.
📖 How it was built — the verification method and the bugs the Arctic fixtures caught.
📚 API reference — every public function of all three packages, with runnable examples.
npm install @grahan/vedic # panchang (pulls in @grahan/core)
npm install @grahan/calendars # Bikram Sambat ↔ AD (pulls in @grahan/core)
npm install @grahan/core # just the astronomy
The packages are ESM-only: use import on Node ≥ 18 (that is also
what bundlers consume); CommonJS require() works from Node ≥ 22.
TypeScript resolves the bundled declarations with moduleResolution
node16, nodenext, or bundler. Bad inputs (latitude 95, an invalid
Date, an unknown IANA zone) throw a RangeError naming the offending
value — nothing silently returns NaN.
import { panchang } from '@grahan/vedic';
const p = panchang({
date: new Date('2026-07-02T06:00:00Z'), // a fixed instant: 2 July 2026, 11:45 in Kathmandu
latitude: 27.7172,
longitude: 85.324,
timezone: 'Asia/Kathmandu',
});
console.log(p.tithi); // { index: 17, paksha: 'krishna', name: 'Tritiya' }
console.log(p.nakshatra); // { index: 21, name: 'Shravana', pada: 1 }
console.log(p.vaar.vaar); // 'Guruvaar' (Thursday)
console.log(p.moonPhase.phaseName); // 'waning-gibbous'
console.log(p.rahuKaal); // { start: 08:06:42Z, end: 09:50:43Z } — 13:51–15:35 NPT
Sunrise, sunset, yoga, and karana are in the same result. Polar latitudes
return explicit 'always-up' / 'always-down' daylight states instead of
fabricated times.
import { kundali, kundaliSvg, vimshottari } from '@grahan/vedic';
const birth = new Date('1993-08-18T05:15:00Z'); // 11:00 NPT
const k = kundali({ date: birth, latitude: 27.0104, longitude: 84.8821 });
console.log(k.lagna.rashiName); // 'Tula' (Libra 11°47′ rising)
console.log(k.grahas[1].nakshatra); // { index: 9, name: 'Magha', pada: 3 } — Moon
console.log(k.grahas[6].retrograde); // true — Saturn ℞ in Kumbha, 5th house
const svg = kundaliSvg(k, { style: 'north' }); // or 'south'; ready to embed
const v = vimshottari(k.grahas[1].longitude, birth);
console.log(v.birthLord, v.balanceYears.toFixed(2)); // 'ketu' '3.21'
console.log(v.mahadashas[3].lord); // 'moon' — starts 2022-11-04
Transits (transits()), auspicious windows (abhijitMuhurta(),
findMuhurta()), and 36-point match-making (gunMilan()) ship in the same
package — see the CHANGELOG for the full v0.2 surface.
The secular core works standalone:
import { sunriseSunset, moonPhase, julianDayFromDate } from '@grahan/core';
sunriseSunset({
year: 2026,
month: 7,
day: 2,
latitude: 27.7172,
longitude: 85.324,
timezone: 'Asia/Kathmandu',
});
// { sunrise: { kind: 'rises', date: 2026-07-01T23:26:36Z }, sunset: … }
The project is named after the Sanskrit word for eclipse; as of v0.4 it computes them — detection, type, magnitudes, and contact times for every eclipse from 1900 to 2100, in the same zero-dependency core:
import {
nextSolarEclipseAt,
nextLunarEclipse,
julianDayFromDate,
dateFromJulianDay,
} from '@grahan/core';
// When does Kathmandu next see the Sun bitten? Any lat/lon works.
const jd = julianDayFromDate(new Date('2026-07-06T00:00:00Z'));
const solar = nextSolarEclipseAt(jd, { latitude: 27.7172, longitude: 85.324 });
solar.type; // 'partial' — 2027-08-02: the Egypt totality, grazing Nepal
dateFromJulianDay(solar.maximum); // 2027-08-02T11:05:02Z (16:50 NPT)
solar.magnitude; // 0.0751 — a small bite off the solar diameter
solar.obscuration; // 0.0248 — 2.5 % of the disc covered
// The New Year's Eve total lunar eclipse of 2028, high over Kathmandu:
const lunar = nextLunarEclipse(
julianDayFromDate(new Date('2028-12-01T00:00:00Z')),
);
lunar.type; // 'total', umbral magnitude 1.25
dateFromJulianDay(lunar.totalBegin); // 2028-12-31T16:16:13Z — totality 22:01–23:12 NPT
nextSolarEclipse() gives the global picture (partial / annular / total /
hybrid, centrality, and the greatest-eclipse instant, ground point, and
magnitude), and nextNewMoon() / nextFullMoon() ship alongside. Local
circumstances report the visible maximum: if the geometric peak is
below the horizon, you get the sunrise/sunset moment instead, matching
NASA-style local tables.
panchang() answers "what is the sky now?"; panchangAtSunrise() answers
what a printed panchang answers: label a civil date by its sunrise
elements, with the instant each one ends:
import { panchangAtSunrise } from '@grahan/vedic';
const day = panchangAtSunrise({
year: 2026,
month: 7,
day: 2, // the civil date — no instant needed
latitude: 27.7172,
longitude: 85.324,
timezone: 'Asia/Kathmandu',
});
day.vaar.name; // 'Thursday'
day.tithi[0].name; // 'Dwitiya' (krishna) — the day's label
day.tithi[0].endsAt; // 2026-07-02T04:08:19Z — "upto 09:53" NPT
day.tithi[1].name; // 'Tritiya' — what the next day will be labelled
day.nakshatra[0]; // Uttara Ashadha pada 4, upto 09:42 NPT
day.karana.map((k) => k.name); // ['Gara', 'Vanija', 'Vishti']
Each element is an ordered list of spans touching the vedic day
(sunrise to next sunrise) — never a single squashed value. On a kshaya
day the skipped tithi appears as a middle span that begins and ends
between the two sunrises; on a vriddhi day one span runs past the next
sunrise. Polar dates without a sunrise fall back to a local
midnight-to-midnight window with the daylight flag set. The raw end-time
solvers (tithiEndTime, nakshatraEndTime, yogaEndTime,
karanaEndTime) are exported too.
Rahu/Ketu can now follow the true (osculating) node instead of the
mean node — kundali({ ..., node: 'true' }) — and its retrograde flag is
computed, not assumed (the true node briefly runs direct).
| Package | What it does |
|---|---|
@grahan/core |
Secular astronomy: julian day, ΔT, apparent Sun/Moon/planets, mean & true lunar node, sidereal time, ascendant, sunrise/sunset, moon phase, lunar & solar eclipses (global + local) |
@grahan/vedic |
Vedic layer on core: panchang(), panchangAtSunrise() with element end times, kundali() with navamsa + SVG charts, Vimshottari dashas, transits, muhurta, gun-milan |
@grahan/calendars |
World calendars, Bikram Sambat first: BS ↔ AD conversion (1975–2200 BS, verified tables + Surya Siddhanta projection), todayBs() |
More layers (Hijri, Hebrew, prayer times, tropical charts) are planned on the same core.
Implemented from public-domain algorithms (Meeus Astronomical Algorithms, truncated VSOP87 and ELP-2000/82 series) and verified against Swiss Ephemeris 2.10 reference fixtures committed in this repo (Swiss Ephemeris itself is never used at runtime — it is AGPL; grahan is MIT).
| Quantity | Promise | Measured vs Swiss Ephemeris (1900–2100) |
|---|---|---|
| Sun longitude | ±36″ | max 4.6″, mean 0.9″ (120 instants) |
| Moon longitude | ±180″ | max 65″, mean 10.5″ (120 instants) |
| Planets (Me–Sa) | ±72″ | max 7.8″ (Mercury) down to 1.1″ (Saturn) |
| Ascendant | ±36″ | max 3.1″ (168 cases, 1°N–60°N) |
| Sunrise/sunset | ±60 s | max 4.6 s, mean 0.9 s (272 events, 5 sites incl. 71°N) |
| Lahiri ayanamsa | — | max 0.002″ (41 epochs) |
| Eclipse times | ±2½ min | detection & type exact on all 909 events; instants mean ~20 s, max 151 s (a 0.005-magnitude graze) |
| Eclipse magnitudes | — | ≤ 0.002 (lunar ≤ 0.0015); fixtures cross-checked against NASA/Espenak's canon |
| Panchang end times | ±1 min | max 37.5 s, mean 9.7 s (122 boundaries, 1990–2060); spot-checked against drikpanchang.com |
| True lunar node | ±0.03° | max 66″, mean 15″ (160 instants — the Moon series' own error carried into the osculating plane) |
Limits, stated plainly: ΔT model is fit for 1800–2150 (degrades outside);
ayanamsa is verified for 1900–2100; timezone conversion supports years
100–9999 CE at whole-second precision; panchang() reports elements at the
queried instant while panchangAtSunrise() carries the transition times;
the ascendant is valid for |latitude| ≲ 66°; gun-milan follows the most
widely published classical tables — regional scoring variants exist and are
documented in the module; sub-0.02-magnitude grazing eclipses have
inherently soft timings, and a sunrise-clamped local eclipse maximum at
polar latitudes can differ from other sources by minutes (refraction
models differ there).
v1.0 froze the public API: the exact export surface of every package is
pinned by tests, breaking changes only come with a major version, and
additions come with minors. Bad inputs throw RangeError — behavior
that is part of the contract. Releases are published from CI via npm
trusted publishing with provenance attestations; there are no long-lived
publish tokens. Node ≥ 18 (import) is supported; see Install for the
full matrix.
MIT © Svarbhanu Neel