astrology-insights
Version:
Comprehensive Vedic astrology engine for Node.js — Panchang, birth charts (Kundli), Vimshottari Dasha, divisional charts, dosha analysis, and planetary remedies. Swiss Ephemeris precision, validated against Drik Panchang.
345 lines (309 loc) • 12.6 kB
text/typescript
/**
* Divisional Chart (Varga) Calculator
* Computes planet positions in any of the 16 Shodashvarga divisional charts.
*/
import { SIGN_NAMES } from '../core/constants';
import { getChartInfo } from './charts';
// ── Types ───────────────────────────────────────────────────────────────────
export interface DivisionalPosition {
planet: string;
d1SignNumber: number; // Original sign in D1 (1-12)
d1Degree: number; // Degree in D1 sign (0-30)
vargaSignNumber: number; // Sign in this divisional chart (1-12)
vargaSignName: string;
vargaDegree: number; // Degree within varga division
}
export interface DivisionalChart {
name: string; // e.g. "Navamsa (D9)"
division: number; // e.g. 9
planets: DivisionalPosition[];
lagnaSign: { number: number; name: string };
}
export interface PlanetInput {
name: string;
signNumber: number; // 1-12
degreeInSign: number; // 0-30
}
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Normalise a sign number to 1-12. */
function normSign(n: number): number {
return ((n - 1) % 12 + 12) % 12 + 1;
}
/** Get element of a sign: 'fire' | 'earth' | 'air' | 'water'. */
function signElement(signNumber: number): 'fire' | 'earth' | 'air' | 'water' {
const mod = ((signNumber - 1) % 4);
return (['fire', 'earth', 'air', 'water'] as const)[mod];
}
/** Is sign odd (1,3,5,...) or even (2,4,6,...)? */
function isOddSign(signNumber: number): boolean {
return signNumber % 2 === 1;
}
// ── Division-specific calculators ───────────────────────────────────────────
/**
* D2 (Hora): 2 parts of 15 deg.
* Odd signs: 0-15 = Leo(5), 15-30 = Cancer(4)
* Even signs: 0-15 = Cancer(4), 15-30 = Leo(5)
*/
function calcD2(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const part = deg < 15 ? 0 : 1;
const odd = isOddSign(sign);
let vargaSign: number;
if (odd) {
vargaSign = part === 0 ? 5 : 4; // Leo, Cancer
} else {
vargaSign = part === 0 ? 4 : 5; // Cancer, Leo
}
return { vargaSign, vargaDeg: deg - part * 15 };
}
/**
* D3 (Drekkana): 3 parts of 10 deg.
* 1st (0-10): same sign
* 2nd (10-20): 5th sign from it
* 3rd (20-30): 9th sign from it
*/
function calcD3(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partIndex = Math.min(Math.floor(deg / 10), 2);
const offsets = [0, 4, 8]; // 1st, 5th, 9th from sign
const vargaSign = normSign(sign + offsets[partIndex]);
return { vargaSign, vargaDeg: deg - partIndex * 10 };
}
/**
* D4 (Chaturthamsa): 4 parts of 7.5 deg.
* Part N (0-indexed) -> sign = original + (N * 3)
*/
function calcD4(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partIndex = Math.min(Math.floor(deg / 7.5), 3);
const vargaSign = normSign(sign + partIndex * 3);
return { vargaSign, vargaDeg: deg - partIndex * 7.5 };
}
/**
* D7 (Saptamsa): 7 parts of 4.2857 deg.
* Odd signs: start from same sign, advance +1 per division
* Even signs: start from 7th sign, advance +1 per division
*/
function calcD7(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 7;
const partIndex = Math.min(Math.floor(deg / partSize), 6);
const startSign = isOddSign(sign) ? sign : normSign(sign + 6);
const vargaSign = normSign(startSign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D9 (Navamsa): 9 parts of 3.333 deg.
* Fire signs (1,5,9): start from Aries (1)
* Earth signs (2,6,10): start from Capricorn (10)
* Air signs (3,7,11): start from Libra (7)
* Water signs (4,8,12): start from Cancer (4)
*/
function calcD9(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 9;
const partIndex = Math.min(Math.floor(deg / partSize), 8);
const elem = signElement(sign);
const startMap: Record<string, number> = { fire: 1, earth: 10, air: 7, water: 4 };
const vargaSign = normSign(startMap[elem] + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D10 (Dasamsa): 10 parts of 3 deg.
* Odd signs: start from same sign
* Even signs: start from 9th sign
*/
function calcD10(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 3;
const partIndex = Math.min(Math.floor(deg / partSize), 9);
const startSign = isOddSign(sign) ? sign : normSign(sign + 8);
const vargaSign = normSign(startSign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D12 (Dwadasamsa): 12 parts of 2.5 deg.
* Start from same sign, advance +1 per division.
*/
function calcD12(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 2.5;
const partIndex = Math.min(Math.floor(deg / partSize), 11);
const vargaSign = normSign(sign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D16 (Shodasamsa): 16 parts of 1.875 deg.
* Fire signs: start Aries(1), Earth: Leo(5), Air: Sagittarius(9), Water: Aries(1)
*/
function calcD16(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 16;
const partIndex = Math.min(Math.floor(deg / partSize), 15);
const elem = signElement(sign);
const startMap: Record<string, number> = { fire: 1, earth: 5, air: 9, water: 1 };
const vargaSign = normSign(startMap[elem] + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D20 (Vimsamsa): 20 parts of 1.5 deg.
* Fire signs: start Aries(1), Earth: Sagittarius(9), Air: Leo(5)
* Water signs: follow the fire cycle → Aries(1)
*/
function calcD20(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 1.5;
const partIndex = Math.min(Math.floor(deg / partSize), 19);
const elem = signElement(sign);
const startMap: Record<string, number> = { fire: 1, earth: 9, air: 5, water: 1 };
const vargaSign = normSign(startMap[elem] + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D24 (Chaturvimsamsa): 24 parts of 1.25 deg.
* Odd signs: start Leo(5), Even signs: start Cancer(4)
*/
function calcD24(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 24;
const partIndex = Math.min(Math.floor(deg / partSize), 23);
const startSign = isOddSign(sign) ? 5 : 4;
const vargaSign = normSign(startSign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D27 (Saptavimsamsa/Bhamsa): 27 parts of 1.111 deg.
* Fire: Aries(1), Earth: Cancer(4), Air: Libra(7), Water: Capricorn(10)
*/
function calcD27(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 27;
const partIndex = Math.min(Math.floor(deg / partSize), 26);
const elem = signElement(sign);
const startMap: Record<string, number> = { fire: 1, earth: 4, air: 7, water: 10 };
const vargaSign = normSign(startMap[elem] + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D30 (Trimsamsa): Irregular division (not equal parts).
* Odd signs: 0-5 Mars(Aries/1), 5-10 Saturn(Aquarius/11), 10-18 Jupiter(Sagittarius/9),
* 18-25 Mercury(Gemini/3), 25-30 Venus(Taurus/2)
* Even signs: 0-5 Venus(Taurus/2), 5-12 Mercury(Virgo/6), 12-20 Jupiter(Pisces/12),
* 20-25 Saturn(Capricorn/10), 25-30 Mars(Scorpio/8)
*/
function calcD30(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
if (isOddSign(sign)) {
if (deg < 5) return { vargaSign: 1, vargaDeg: deg };
if (deg < 10) return { vargaSign: 11, vargaDeg: deg - 5 };
if (deg < 18) return { vargaSign: 9, vargaDeg: deg - 10 };
if (deg < 25) return { vargaSign: 3, vargaDeg: deg - 18 };
return { vargaSign: 2, vargaDeg: deg - 25 };
} else {
if (deg < 5) return { vargaSign: 2, vargaDeg: deg };
if (deg < 12) return { vargaSign: 6, vargaDeg: deg - 5 };
if (deg < 20) return { vargaSign: 12, vargaDeg: deg - 12 };
if (deg < 25) return { vargaSign: 10, vargaDeg: deg - 20 };
return { vargaSign: 8, vargaDeg: deg - 25 };
}
}
/**
* D40 (Khavedamsa): 40 parts of 0.75 deg.
* Odd signs: start Aries(1), Even signs: start Libra(7)
*/
function calcD40(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 0.75;
const partIndex = Math.min(Math.floor(deg / partSize), 39);
const startSign = isOddSign(sign) ? 1 : 7;
const vargaSign = normSign(startSign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D45 (Akshavedamsa): 45 parts of 0.667 deg.
* Fire: Aries(1), Earth: Leo(5), Air: Sagittarius(9), Water: Aries(1)
*/
function calcD45(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 30 / 45;
const partIndex = Math.min(Math.floor(deg / partSize), 44);
const elem = signElement(sign);
const startMap: Record<string, number> = { fire: 1, earth: 5, air: 9, water: 1 };
const vargaSign = normSign(startMap[elem] + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
/**
* D60 (Shashtiamsa): 60 parts of 0.5 deg.
* Start from same sign, advance +1 per division.
*/
function calcD60(sign: number, deg: number): { vargaSign: number; vargaDeg: number } {
const partSize = 0.5;
const partIndex = Math.min(Math.floor(deg / partSize), 59);
const vargaSign = normSign(sign + partIndex);
return { vargaSign, vargaDeg: deg - partIndex * partSize };
}
// ── Calculator dispatch ─────────────────────────────────────────────────────
type DivisionCalc = (sign: number, deg: number) => { vargaSign: number; vargaDeg: number };
const DIVISION_CALCULATORS: Record<number, DivisionCalc> = {
2: calcD2,
3: calcD3,
4: calcD4,
7: calcD7,
9: calcD9,
10: calcD10,
12: calcD12,
16: calcD16,
20: calcD20,
24: calcD24,
27: calcD27,
30: calcD30,
40: calcD40,
45: calcD45,
60: calcD60,
};
/**
* Compute a single planet's varga position for a given division.
*/
function computeVargaPosition(
division: number,
sign: number,
deg: number,
): { vargaSign: number; vargaDeg: number } {
// D1 is the birth chart itself
if (division === 1) {
return { vargaSign: sign, vargaDeg: deg };
}
const calc = DIVISION_CALCULATORS[division];
if (!calc) {
throw new Error(`Unsupported divisional chart: D${division}`);
}
return calc(sign, deg);
}
// ── Public API ──────────────────────────────────────────────────────────────
/**
* Calculate a divisional chart for a given division number.
* @param division - The division number (1, 2, 3, 4, 7, 9, 10, 12, 16, 20, 24, 27, 30, 40, 45, 60)
* @param planets - Array of {name, signNumber (1-12), degreeInSign (0-30)}
* @param lagnaSignNumber - Lagna sign number (1-12)
* @param lagnaDegree - Lagna degree in sign (0-30)
*/
export function calculateDivisionalChart(
division: number,
planets: PlanetInput[],
lagnaSignNumber: number,
lagnaDegree: number,
): DivisionalChart {
const chartInfo = getChartInfo(division);
const chartName = chartInfo
? `${chartInfo.name} (${chartInfo.shortName})`
: `D${division}`;
// Calculate lagna position in this varga
const lagnaVarga = computeVargaPosition(division, lagnaSignNumber, lagnaDegree);
// Calculate each planet
const positions: DivisionalPosition[] = planets.map(p => {
const result = computeVargaPosition(division, p.signNumber, p.degreeInSign);
return {
planet: p.name,
d1SignNumber: p.signNumber,
d1Degree: p.degreeInSign,
vargaSignNumber: result.vargaSign,
vargaSignName: SIGN_NAMES[result.vargaSign] || `Sign-${result.vargaSign}`,
vargaDegree: result.vargaDeg,
};
});
return {
name: chartName,
division,
planets: positions,
lagnaSign: {
number: lagnaVarga.vargaSign,
name: SIGN_NAMES[lagnaVarga.vargaSign] || `Sign-${lagnaVarga.vargaSign}`,
},
};
}