A working Vedic astrology tool that computes planetary positions, finds Nadi bonds and projects slow-planet progression across a lifetime — with no build step, no npm, no server and no network calls.
Here is what that constraint forced me to do, and the one bug that cost me the most time.
The constraint
The tool had to run from a single file, opened straight from disk, on a phone, with no internet. That rules out:
- a CDN for the ephemeris
- a bundler
- a server to do the astronomy
- a timezone API
Everything has to be inside one index.html.
Step 1 — inlining an ephemeris
Accurate planetary longitudes are the hard part. I did not want to write my own VSOP87 implementation, so I inlined the excellent Astronomy Engine.
Astronomy Engine ships as a UMD bundle. Dropping it into a <script> tag works — it detects the absence of module and define and assigns itself to window.Astronomy.
Then my own engine reads it:
const Astro = (typeof Astronomy !== 'undefined')
? Astronomy
: (typeof require === 'function' ? require('astronomy-engine') : null);
Step 2 — the bug that cost me an hour
The tool worked in my browser test. It did not work in Node. That is normal and expected — the UMD wrapper takes the CommonJS branch under Node.
But then the browser version broke too, with a single unhelpful line:
ReferenceError: $jscomp is not defined
Astronomy Engine is compiled by Google Closure, and the compiled output depends on a $jscomp runtime shim — iterator helpers, makeIterator, arrayIteratorImpl, and a dozen others.
That shim is emitted at the very top of the bundle, before the UMD wrapper.
When I extracted the library to inline it, I started my slice at the UMD wrapper. I had left the shim behind.
var $jscomp=$jscomp||{};$jscomp.scope={};
$jscomp.makeIterator=function(r){...};
// ... about 110 KB of Closure helpers and the library itself ...
(function(r){ ... }).Astronomy = r();
The fix is one character: start the slice at byte 0, not at the wrapper.
The lesson is general. If you inline a Closure-compiled bundle, you have to take the runtime shim with it. It does not announce itself; it fails only when the first compiled function that needs a helper is actually called. That is why the page rendered fine and only broke when I pressed the button.
Step 3 — what Bhrigu Nandi Nadi actually computes
BNN is a Jaimini technique that ignores houses entirely and reads relationships between planets.
Bonds (yuti). Two planets in the same sign are in a bond. The planet with the lower degree leads it. That leading planet is the one that expresses first.
const groups = {};
for (const p of PLANETS) {
const s = signOf(chart.positions[p]);
(groups[s] = groups[s] || []).push(p);
}
Each pair inside a group then gets a relation from a classical friend/enemy table:
const f = NADI_FRIEND[a] && NADI_FRIEND[a][b];
kind = f === 1 ? 'Friendly' : f === -1 ? 'Inimical' : 'Neutral';
Trine circuits. Every planet is placed in one of four circuits — Dharma (1, 5, 9), Artha (2, 6, 10), Kama (3, 7, 11), Moksha (4, 8, 12).
That is the whole technique. No houses, no aspects. Just sign co-tenancy and degree order.
Step 4 — slow-planet progression
Progression here means a planet advancing one sign at a fixed rate across a life:
| Planet | Years per sign |
|---|---|
| Jupiter | 12 |
| Saturn | 30 |
| Rahu / Ketu | 18 |
A retrograde planet progresses backwards. The first transition is partial — the planet finishes the degrees left in its birth sign first:
const frac = degInSign / 30;
const majorLeft = (1 - frac) * R.major;
From there each subsequent sign takes the full period. The output is a table of age ranges, which is the useful form: "Jupiter sits in Libra from age 0 to 4.5, then Scorpio to 16.5."
Step 5 — keeping it offline
Two things had to go.
The timezone database. A real tz database is large, and historical offsets for India alone are messy. I replaced it with a fixed UTC offset the user selects. Less clever, much smaller, and honest about its limits — the UI says so.
The network. Nothing is fetched. No fonts, no icons, no analytics. The whole thing is 206 KB and works on a plane.
What I would do differently
Test the inlined bundle in isolation first. A five-line page that calls one function from the library would have caught the missing shim in thirty seconds instead of an hour of staring at my own UI code.
Keep the engine and the UI in separate files during development. I concatenate them into one file at the end. Editing a 200 KB file directly is miserable.
Write the engine as a factory that exports a namespace. Mine does — root.KV = factory() — which is why the same file can be driven from a page, an iframe, or a test harness.
The result
The tool is at sahilyadavy251-tech.github.io/nadi-bnn-progression and the source is on GitHub. MIT licensed — fork it, embed it, ship it inside your own app.
It is extracted from Advanced Occult Astrology, a free Vedic astrology app I maintain with 20 divisional charts, Vimshottari dasha to four levels, Shadbala, Ashtakavarga, Jaimini Chara Karakas and the KP system.
If you have inlined a compiled bundle before and hit the same shim problem, I would like to hear how you caught it.
Top comments (0)