@roxyapi/ui 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +34 -7
- package/README.md +145 -26
- package/dist/cdn/components/ashtakavarga-grid.js +74 -19
- package/dist/cdn/components/ashtakavarga-grid.js.map +2 -2
- package/dist/cdn/components/biorhythm-chart.js +18 -4
- package/dist/cdn/components/biorhythm-chart.js.map +2 -2
- package/dist/cdn/components/choghadiya-grid.js +47 -12
- package/dist/cdn/components/choghadiya-grid.js.map +3 -3
- package/dist/cdn/components/compatibility-card.js +21 -7
- package/dist/cdn/components/compatibility-card.js.map +2 -2
- package/dist/cdn/components/dasha-timeline.js +113 -28
- package/dist/cdn/components/dasha-timeline.js.map +3 -3
- package/dist/cdn/components/data.js +27 -13
- package/dist/cdn/components/data.js.map +2 -2
- package/dist/cdn/components/divisional-chart.js +225 -118
- package/dist/cdn/components/divisional-chart.js.map +4 -4
- package/dist/cdn/components/dosha-card.js +18 -4
- package/dist/cdn/components/dosha-card.js.map +2 -2
- package/dist/cdn/components/endpoint-form.js +25 -11
- package/dist/cdn/components/endpoint-form.js.map +2 -2
- package/dist/cdn/components/guna-milan.js +20 -6
- package/dist/cdn/components/guna-milan.js.map +2 -2
- package/dist/cdn/components/hexagram.js +22 -8
- package/dist/cdn/components/hexagram.js.map +2 -2
- package/dist/cdn/components/horoscope-card.js +20 -6
- package/dist/cdn/components/horoscope-card.js.map +2 -2
- package/dist/cdn/components/kp-chart.js +19 -5
- package/dist/cdn/components/kp-chart.js.map +2 -2
- package/dist/cdn/components/kp-planets-table.js +17 -3
- package/dist/cdn/components/kp-planets-table.js.map +2 -2
- package/dist/cdn/components/kp-ruling-planets.js +17 -3
- package/dist/cdn/components/kp-ruling-planets.js.map +2 -2
- package/dist/cdn/components/location-search.js +18 -4
- package/dist/cdn/components/location-search.js.map +2 -2
- package/dist/cdn/components/moon-phase.js +27 -13
- package/dist/cdn/components/moon-phase.js.map +2 -2
- package/dist/cdn/components/nakshatra-card.js +16 -2
- package/dist/cdn/components/nakshatra-card.js.map +2 -2
- package/dist/cdn/components/natal-chart.js +79 -40
- package/dist/cdn/components/natal-chart.js.map +3 -3
- package/dist/cdn/components/numerology-card.js +18 -4
- package/dist/cdn/components/numerology-card.js.map +2 -2
- package/dist/cdn/components/panchang-table.js +53 -25
- package/dist/cdn/components/panchang-table.js.map +3 -3
- package/dist/cdn/components/shadbala-table.js +24 -10
- package/dist/cdn/components/shadbala-table.js.map +2 -2
- package/dist/cdn/components/synastry-chart.js +96 -48
- package/dist/cdn/components/synastry-chart.js.map +3 -3
- package/dist/cdn/components/tarot-card.js +17 -3
- package/dist/cdn/components/tarot-card.js.map +2 -2
- package/dist/cdn/components/tarot-spread.js +39 -25
- package/dist/cdn/components/tarot-spread.js.map +2 -2
- package/dist/cdn/components/transits-table.js +18 -4
- package/dist/cdn/components/transits-table.js.map +2 -2
- package/dist/cdn/components/vedic-kundli.js +215 -105
- package/dist/cdn/components/vedic-kundli.js.map +4 -4
- package/dist/cdn/components/vedic-planets-table.js +22 -8
- package/dist/cdn/components/vedic-planets-table.js.map +2 -2
- package/dist/cdn/components/western-planets-table.js +18 -4
- package/dist/cdn/components/western-planets-table.js.map +2 -2
- package/dist/cdn/components/yoga-list.js +17 -3
- package/dist/cdn/components/yoga-list.js.map +2 -2
- package/dist/cdn/roxy-ui.js +1082 -816
- package/dist/cdn/roxy-ui.js.map +4 -4
- package/dist/components/ashtakavarga-grid.d.ts +13 -1
- package/dist/components/ashtakavarga-grid.d.ts.map +1 -1
- package/dist/components/ashtakavarga-grid.js +86 -11
- package/dist/components/ashtakavarga-grid.js.map +2 -2
- package/dist/components/biorhythm-chart.js +14 -0
- package/dist/components/biorhythm-chart.js.map +2 -2
- package/dist/components/choghadiya-grid.d.ts +6 -0
- package/dist/components/choghadiya-grid.d.ts.map +1 -1
- package/dist/components/choghadiya-grid.js +50 -2
- package/dist/components/choghadiya-grid.js.map +2 -2
- package/dist/components/compatibility-card.js +14 -0
- package/dist/components/compatibility-card.js.map +2 -2
- package/dist/components/dasha-timeline.d.ts +10 -0
- package/dist/components/dasha-timeline.d.ts.map +1 -1
- package/dist/components/dasha-timeline.js +135 -4
- package/dist/components/dasha-timeline.js.map +2 -2
- package/dist/components/data.js +14 -0
- package/dist/components/data.js.map +2 -2
- package/dist/components/divisional-chart.d.ts +9 -6
- package/dist/components/divisional-chart.d.ts.map +1 -1
- package/dist/components/divisional-chart.js +546 -251
- package/dist/components/divisional-chart.js.map +4 -4
- package/dist/components/dosha-card.js +14 -0
- package/dist/components/dosha-card.js.map +2 -2
- package/dist/components/endpoint-form.js +14 -0
- package/dist/components/endpoint-form.js.map +2 -2
- package/dist/components/guna-milan.js +14 -0
- package/dist/components/guna-milan.js.map +2 -2
- package/dist/components/hexagram.js +14 -0
- package/dist/components/hexagram.js.map +2 -2
- package/dist/components/horoscope-card.js +14 -0
- package/dist/components/horoscope-card.js.map +2 -2
- package/dist/components/kp-chart.js +14 -0
- package/dist/components/kp-chart.js.map +2 -2
- package/dist/components/kp-planets-table.js +14 -0
- package/dist/components/kp-planets-table.js.map +2 -2
- package/dist/components/kp-ruling-planets.js +14 -0
- package/dist/components/kp-ruling-planets.js.map +2 -2
- package/dist/components/location-search.js +14 -0
- package/dist/components/location-search.js.map +2 -2
- package/dist/components/moon-phase.js +14 -0
- package/dist/components/moon-phase.js.map +2 -2
- package/dist/components/nakshatra-card.js +14 -0
- package/dist/components/nakshatra-card.js.map +2 -2
- package/dist/components/natal-chart.d.ts.map +1 -1
- package/dist/components/natal-chart.js +76 -6
- package/dist/components/natal-chart.js.map +2 -2
- package/dist/components/numerology-card.js +14 -0
- package/dist/components/numerology-card.js.map +2 -2
- package/dist/components/panchang-table.d.ts +1 -0
- package/dist/components/panchang-table.d.ts.map +1 -1
- package/dist/components/panchang-table.js +37 -1
- package/dist/components/panchang-table.js.map +2 -2
- package/dist/components/shadbala-table.js +14 -0
- package/dist/components/shadbala-table.js.map +2 -2
- package/dist/components/synastry-chart.d.ts +6 -0
- package/dist/components/synastry-chart.d.ts.map +1 -1
- package/dist/components/synastry-chart.js +106 -7
- package/dist/components/synastry-chart.js.map +2 -2
- package/dist/components/tarot-card.js +14 -0
- package/dist/components/tarot-card.js.map +2 -2
- package/dist/components/tarot-spread.js +14 -0
- package/dist/components/tarot-spread.js.map +2 -2
- package/dist/components/transits-table.js +14 -0
- package/dist/components/transits-table.js.map +2 -2
- package/dist/components/vedic-kundli.d.ts +14 -9
- package/dist/components/vedic-kundli.d.ts.map +1 -1
- package/dist/components/vedic-kundli.js +537 -245
- package/dist/components/vedic-kundli.js.map +4 -4
- package/dist/components/vedic-planets-table.js +14 -0
- package/dist/components/vedic-planets-table.js.map +2 -2
- package/dist/components/western-planets-table.js +14 -0
- package/dist/components/western-planets-table.js.map +2 -2
- package/dist/components/yoga-list.js +14 -0
- package/dist/components/yoga-list.js.map +2 -2
- package/dist/index.cjs +1397 -797
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +1278 -678
- package/dist/index.js.map +4 -4
- package/dist/manifest.json +23 -23
- package/dist/styles/tokens.css +8 -23
- package/dist/utils/base-styles.d.ts.map +1 -1
- package/dist/utils/kundli-render.d.ts +43 -104
- package/dist/utils/kundli-render.d.ts.map +1 -1
- package/dist/utils/kundli-styles.d.ts +13 -0
- package/dist/utils/kundli-styles.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ashtakavarga-grid.ts +73 -11
- package/src/components/choghadiya-grid.ts +37 -2
- package/src/components/dasha-timeline.ts +135 -4
- package/src/components/divisional-chart.ts +40 -97
- package/src/components/natal-chart.ts +89 -6
- package/src/components/panchang-table.ts +34 -1
- package/src/components/synastry-chart.ts +84 -8
- package/src/components/vedic-kundli.ts +35 -95
- package/src/styles/tokens.css +8 -23
- package/src/utils/base-styles.ts +14 -0
- package/src/utils/kundli-render.ts +609 -270
- package/src/utils/kundli-styles.ts +124 -0
- package/src/version.ts +1 -1
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
import type { TemplateResult } from 'lit';
|
|
2
|
-
import { nothing, svg } from 'lit';
|
|
2
|
+
import { html, nothing, svg } from 'lit';
|
|
3
3
|
import { PLANET_ABBR, SIGN_ABBR, SIGNS_ORDER } from '../tokens/index.js';
|
|
4
4
|
import { longitudeToSignPosition } from './degree.js';
|
|
5
5
|
import { capitalize } from './string.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical viewBox geometry for every kundli style. The chart is drawn into a
|
|
9
|
+
* 360-unit square centred in a 400-unit viewBox, leaving a 20-unit gutter for
|
|
10
|
+
* outer labels. All coordinates below are derived from these constants; change
|
|
11
|
+
* them and every cell relocates correctly.
|
|
12
|
+
*
|
|
13
|
+
* @remarks SVG is vector-only and scales without raster loss, so the chart
|
|
14
|
+
* remains crisp from a phone screen to a wall projector. Hosts size the chart
|
|
15
|
+
* by setting `width` on the surrounding container; the SVG keeps a 1:1 aspect
|
|
16
|
+
* ratio via the viewBox.
|
|
17
|
+
*/
|
|
18
|
+
const VIEW_BOX = 400;
|
|
19
|
+
const MARGIN = 20;
|
|
20
|
+
const INNER = VIEW_BOX - 2 * MARGIN; // 360
|
|
21
|
+
const CENTRE = VIEW_BOX / 2; // 200
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
24
|
+
* Lowercase rashi key (`"aries"`) to canonical title-cased sign name (`"Aries"`).
|
|
25
|
+
* Bridges API lowercase rashi strings to the SIGNS_ORDER tokens used everywhere
|
|
26
|
+
* else in the render.
|
|
14
27
|
*/
|
|
15
|
-
|
|
28
|
+
const RASHI_TO_SIGN: Record<string, string> = Object.fromEntries(
|
|
16
29
|
SIGNS_ORDER.map((s) => [s.toLowerCase(), s] as const),
|
|
17
30
|
);
|
|
18
31
|
|
|
19
32
|
/**
|
|
20
|
-
* A
|
|
21
|
-
*
|
|
22
|
-
* (abbreviation
|
|
23
|
-
* `<title>` tooltip (
|
|
24
|
-
* birth chart and the Dx divisional charts feed it from their `meta` map.
|
|
33
|
+
* A graha placed inside a kundli cell. Render-only view model fed from a
|
|
34
|
+
* `meta` map on the API response. Carries enough detail to draw a compact
|
|
35
|
+
* in-cell label (abbreviation, whole degree, retrograde mark) and a rich SVG
|
|
36
|
+
* `<title>` tooltip (exact position, nakshatra, pada, avastha).
|
|
25
37
|
*/
|
|
26
38
|
export interface PlacedGraha {
|
|
27
39
|
graha: string;
|
|
@@ -31,28 +43,67 @@ export interface PlacedGraha {
|
|
|
31
43
|
awastha?: string;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Unified view model used by every kundli style. Caller passes a graha-keyed
|
|
48
|
+
* `meta` map (from `/vedic-astrology/birth-chart`, `/divisional-chart`, or
|
|
49
|
+
* `/navamsa`) through {@link toKundliViewModel} to produce this shape.
|
|
50
|
+
*
|
|
51
|
+
* `placements` is keyed by lowercase rashi name (`"aries"`, `"taurus"`, ...)
|
|
52
|
+
* so the sign-fixed styles can index directly. The Lagna entry is not
|
|
53
|
+
* counted as a planet; it only flags the ascendant cell.
|
|
54
|
+
*/
|
|
55
|
+
export interface KundliViewModel {
|
|
56
|
+
lagnaSign: string;
|
|
57
|
+
placements: Record<string, PlacedGraha[]>;
|
|
58
|
+
divisionLabel?: string;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Kundli regional styles. Sign-fixed (south, east) and house-fixed (north).
|
|
63
|
+
* Exposed so consumers can type their own `chart-style` attribute reflection.
|
|
64
|
+
*/
|
|
65
|
+
export type ChartStyle = 'south' | 'north' | 'east';
|
|
66
|
+
|
|
67
|
+
const CHART_STYLES: ReadonlyArray<{ id: ChartStyle; label: string }> = [
|
|
68
|
+
{ id: 'north', label: 'North' },
|
|
69
|
+
{ id: 'south', label: 'South' },
|
|
70
|
+
{ id: 'east', label: 'East' },
|
|
71
|
+
];
|
|
72
|
+
|
|
46
73
|
const RETRO_MARK = 'ʳ';
|
|
47
74
|
|
|
48
75
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
76
|
+
* True when the placed graha's longitude maps to a sign other than the cell
|
|
77
|
+
* it occupies. The API preserves the D1 sidereal longitude on every chart, so
|
|
78
|
+
* inside a D2..D60 cell that longitude refers to the D1 sign, not the cell's
|
|
79
|
+
* divisional sign. In that case the degree-within-sign is not meaningful and
|
|
80
|
+
* must be hidden from the in-cell label.
|
|
81
|
+
*/
|
|
82
|
+
function isDivisionalPlacement(p: PlacedGraha, cellSign: string): boolean {
|
|
83
|
+
if (typeof p.longitude !== 'number' || !Number.isFinite(p.longitude)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
longitudeToSignPosition(p.longitude).sign.toLowerCase() !==
|
|
88
|
+
cellSign.toLowerCase()
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compact in-cell graha label: abbreviation, optional whole-degree, retrograde
|
|
94
|
+
* mark. The degree is shown only when the longitude actually maps to the cell
|
|
95
|
+
* the graha is rendered in (the D1 case); divisional placements show the
|
|
96
|
+
* abbreviation alone since the API longitude refers to D1, not the divisional
|
|
97
|
+
* sign.
|
|
51
98
|
*/
|
|
52
|
-
function grahaLabel(p: PlacedGraha): string {
|
|
99
|
+
function grahaLabel(p: PlacedGraha, cellSign: string): string {
|
|
53
100
|
const abbr = PLANET_ABBR[capitalize(p.graha)] ?? p.graha.slice(0, 2);
|
|
54
101
|
const retro = p.isRetrograde ? RETRO_MARK : '';
|
|
55
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
typeof p.longitude !== 'number' ||
|
|
104
|
+
!Number.isFinite(p.longitude) ||
|
|
105
|
+
isDivisionalPlacement(p, cellSign)
|
|
106
|
+
) {
|
|
56
107
|
return `${abbr}${retro}`;
|
|
57
108
|
}
|
|
58
109
|
const { degree } = longitudeToSignPosition(p.longitude);
|
|
@@ -60,16 +111,25 @@ function grahaLabel(p: PlacedGraha): string {
|
|
|
60
111
|
}
|
|
61
112
|
|
|
62
113
|
/**
|
|
63
|
-
* Full-detail tooltip
|
|
64
|
-
*
|
|
65
|
-
*
|
|
114
|
+
* Full-detail tooltip surfaced via the SVG `<title>` for each planet label.
|
|
115
|
+
* Includes planet name, the divisional placement (when the longitude does not
|
|
116
|
+
* match the cell, the cell's rashi is preferred), the exact D1 longitude as
|
|
117
|
+
* the original reference, nakshatra and pada, avastha, and the retrograde
|
|
118
|
+
* flag. Surfaces on hover or long-press without crowding the cell.
|
|
66
119
|
*/
|
|
67
|
-
function grahaTitle(p: PlacedGraha): string {
|
|
120
|
+
function grahaTitle(p: PlacedGraha, cellSign: string): string {
|
|
68
121
|
const parts: string[] = [capitalize(p.graha)];
|
|
122
|
+
const divisional = isDivisionalPlacement(p, cellSign);
|
|
123
|
+
if (divisional) {
|
|
124
|
+
parts.push(`in ${cellSign}`);
|
|
125
|
+
}
|
|
69
126
|
if (typeof p.longitude === 'number' && Number.isFinite(p.longitude)) {
|
|
70
127
|
const sp = longitudeToSignPosition(p.longitude);
|
|
128
|
+
const minute = String(sp.minute).padStart(2, '0');
|
|
71
129
|
parts.push(
|
|
72
|
-
|
|
130
|
+
divisional
|
|
131
|
+
? `D1: ${sp.degree}°${minute}' ${sp.sign}`
|
|
132
|
+
: `${sp.degree}°${minute}' ${sp.sign}`,
|
|
73
133
|
);
|
|
74
134
|
}
|
|
75
135
|
if (p.nakshatra?.name) {
|
|
@@ -82,321 +142,600 @@ function grahaTitle(p: PlacedGraha): string {
|
|
|
82
142
|
}
|
|
83
143
|
|
|
84
144
|
/**
|
|
85
|
-
* Render
|
|
86
|
-
*
|
|
87
|
-
*
|
|
145
|
+
* Render a vertically centred stack of planet labels at `(cx, baseY)`, one
|
|
146
|
+
* line per planet, with an SVG `<title>` per line carrying the full tooltip.
|
|
147
|
+
* The stack auto-centres on `baseY` regardless of count so a 1-planet cell
|
|
148
|
+
* and a 5-planet cell both look intentional.
|
|
88
149
|
*/
|
|
89
150
|
function renderPlanetStack(
|
|
90
151
|
planets: PlacedGraha[],
|
|
152
|
+
cellSign: string,
|
|
91
153
|
cx: number,
|
|
92
154
|
baseY: number,
|
|
93
155
|
lineHeight: number,
|
|
94
|
-
):
|
|
156
|
+
): TemplateResult[] {
|
|
95
157
|
const startY = baseY - ((planets.length - 1) * lineHeight) / 2;
|
|
96
158
|
return planets.map((p, j) => {
|
|
97
159
|
const yPos = startY + j * lineHeight;
|
|
98
160
|
return svg`<text class="planet-text" x=${cx} y=${yPos} text-anchor="middle" dominant-baseline="central">${grahaLabel(
|
|
99
161
|
p,
|
|
100
|
-
|
|
162
|
+
cellSign,
|
|
163
|
+
)}<title>${grahaTitle(p, cellSign)}</title></text>`;
|
|
101
164
|
});
|
|
102
165
|
}
|
|
103
166
|
|
|
104
167
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
168
|
+
* Bucket a graha-keyed `meta` map (D1 birth chart or D2..D60 divisional
|
|
169
|
+
* chart) into the unified {@link KundliViewModel} the renderer consumes. The
|
|
170
|
+
* Lagna entry is recognised by `graha === 'Lagna'` (or key `"Lagna"`) and
|
|
171
|
+
* sets `lagnaSign`; it is not bucketed as a placed planet.
|
|
172
|
+
*
|
|
173
|
+
* @param meta - Graha-keyed map; missing rashi entries are skipped.
|
|
174
|
+
* @param divisionLabel - Optional title written inside the chart centre.
|
|
107
175
|
*/
|
|
108
|
-
export
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
176
|
+
export function toKundliViewModel(
|
|
177
|
+
meta: Record<
|
|
178
|
+
string,
|
|
179
|
+
{
|
|
180
|
+
graha?: string;
|
|
181
|
+
rashi?: string;
|
|
182
|
+
longitude?: number;
|
|
183
|
+
nakshatra?: { name?: string; pada?: number; lord?: string };
|
|
184
|
+
isRetrograde?: boolean;
|
|
185
|
+
awastha?: string;
|
|
186
|
+
}
|
|
187
|
+
>,
|
|
188
|
+
divisionLabel?: string,
|
|
189
|
+
): KundliViewModel {
|
|
190
|
+
const placements: Record<string, PlacedGraha[]> = {};
|
|
191
|
+
for (const sign of SIGNS_ORDER) placements[sign.toLowerCase()] = [];
|
|
192
|
+
let lagnaSign = '';
|
|
193
|
+
for (const [name, pos] of Object.entries(meta ?? {})) {
|
|
194
|
+
const rashiKey = (pos?.rashi ?? '').toLowerCase();
|
|
195
|
+
if (name === 'Lagna' || pos?.graha === 'Lagna') {
|
|
196
|
+
lagnaSign = RASHI_TO_SIGN[rashiKey] ?? '';
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!rashiKey || !(rashiKey in placements)) continue;
|
|
200
|
+
placements[rashiKey]?.push({
|
|
201
|
+
graha: pos.graha ?? name,
|
|
202
|
+
longitude: pos.longitude,
|
|
203
|
+
nakshatra: pos.nakshatra,
|
|
204
|
+
isRetrograde: pos.isRetrograde,
|
|
205
|
+
awastha: pos.awastha,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { lagnaSign, placements, divisionLabel };
|
|
209
|
+
}
|
|
122
210
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
1: { x: 150, y: 35 },
|
|
128
|
-
2: { x: 222, y: 40 },
|
|
129
|
-
3: { x: 265, y: 100 },
|
|
130
|
-
4: { x: 265, y: 150 },
|
|
131
|
-
5: { x: 265, y: 200 },
|
|
132
|
-
6: { x: 222, y: 260 },
|
|
133
|
-
7: { x: 150, y: 265 },
|
|
134
|
-
8: { x: 78, y: 260 },
|
|
135
|
-
9: { x: 35, y: 200 },
|
|
136
|
-
10: { x: 35, y: 150 },
|
|
137
|
-
11: { x: 35, y: 100 },
|
|
138
|
-
12: { x: 78, y: 40 },
|
|
139
|
-
};
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// South Indian: 4x4 grid with central 2x2 hollow. Signs are FIXED to cells
|
|
213
|
+
// (Pisces top-left corner, clockwise); houses rotate from the Lagna cell.
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
140
215
|
|
|
141
|
-
|
|
142
|
-
* North Indian style: 12 triangular house positions.
|
|
143
|
-
* Lagna (house 1) is the top diamond, numbered clockwise.
|
|
144
|
-
* Centers represent the visual midpoint of each triangular cell.
|
|
145
|
-
*/
|
|
146
|
-
export const NORTH_HOUSE_CENTERS: Record<number, { x: number; y: number }> = {
|
|
147
|
-
1: { x: 150, y: 60 },
|
|
148
|
-
2: { x: 225, y: 100 },
|
|
149
|
-
3: { x: 255, y: 150 },
|
|
150
|
-
4: { x: 225, y: 200 },
|
|
151
|
-
5: { x: 150, y: 240 },
|
|
152
|
-
6: { x: 75, y: 200 },
|
|
153
|
-
7: { x: 45, y: 150 },
|
|
154
|
-
8: { x: 75, y: 100 },
|
|
155
|
-
9: { x: 100, y: 80 },
|
|
156
|
-
10: { x: 150, y: 108 },
|
|
157
|
-
11: { x: 200, y: 80 },
|
|
158
|
-
12: { x: 200, y: 220 },
|
|
159
|
-
};
|
|
216
|
+
const SOUTH_CELL = INNER / 4; // 90
|
|
160
217
|
|
|
161
218
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* (cell 1, 4, 7, 10) and the eight corner half-triangles fill between them,
|
|
166
|
-
* laid out clockwise from the top so cell `n` holds the n-th rashi (Aries = 1).
|
|
167
|
-
* Centers are the visual midpoints of those cells in the 300x300 viewBox,
|
|
168
|
-
* derived from the frame geometry (square 10..290, diagonals, side-midpoint
|
|
169
|
-
* diamond).
|
|
170
|
-
*
|
|
171
|
-
* @remarks The cell geometry is exact; the rashi-to-cell order follows the
|
|
172
|
-
* common clockwise-from-top convention and is slated for a regional
|
|
173
|
-
* reference-image confirmation pass (see docs/todo.md "East Indian polish").
|
|
219
|
+
* Sign-to-cell column/row in the South Indian fixed-sign grid. Pisces sits in
|
|
220
|
+
* the top-left corner and the remaining signs proceed clockwise around the
|
|
221
|
+
* 12 perimeter cells. (col, row) origin is the chart top-left.
|
|
174
222
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
const SOUTH_CELL_GRID: Record<string, { col: number; row: number }> = {
|
|
224
|
+
Pisces: { col: 0, row: 0 },
|
|
225
|
+
Aries: { col: 1, row: 0 },
|
|
226
|
+
Taurus: { col: 2, row: 0 },
|
|
227
|
+
Gemini: { col: 3, row: 0 },
|
|
228
|
+
Cancer: { col: 3, row: 1 },
|
|
229
|
+
Leo: { col: 3, row: 2 },
|
|
230
|
+
Virgo: { col: 3, row: 3 },
|
|
231
|
+
Libra: { col: 2, row: 3 },
|
|
232
|
+
Scorpio: { col: 1, row: 3 },
|
|
233
|
+
Sagittarius: { col: 0, row: 3 },
|
|
234
|
+
Capricorn: { col: 0, row: 2 },
|
|
235
|
+
Aquarius: { col: 0, row: 1 },
|
|
188
236
|
};
|
|
189
237
|
|
|
238
|
+
function southCellRect(sign: string): {
|
|
239
|
+
x: number;
|
|
240
|
+
y: number;
|
|
241
|
+
w: number;
|
|
242
|
+
h: number;
|
|
243
|
+
} {
|
|
244
|
+
const g = SOUTH_CELL_GRID[sign] ?? { col: 0, row: 0 };
|
|
245
|
+
return {
|
|
246
|
+
x: MARGIN + g.col * SOUTH_CELL,
|
|
247
|
+
y: MARGIN + g.row * SOUTH_CELL,
|
|
248
|
+
w: SOUTH_CELL,
|
|
249
|
+
h: SOUTH_CELL,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
190
253
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
254
|
+
* South Indian frame: outer square, the two full-span grid lines, and the
|
|
255
|
+
* partial inner lines that bound the central 2x2 hollow on each edge.
|
|
193
256
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
257
|
+
function renderSouthFrame(divisionLabel?: string): TemplateResult {
|
|
258
|
+
const a = MARGIN;
|
|
259
|
+
const b = MARGIN + SOUTH_CELL; // 110
|
|
260
|
+
const c = MARGIN + 2 * SOUTH_CELL; // 200
|
|
261
|
+
const d = MARGIN + 3 * SOUTH_CELL; // 290
|
|
262
|
+
const e = VIEW_BOX - MARGIN; // 380
|
|
263
|
+
return svg`
|
|
264
|
+
<rect class="line" x=${a} y=${a} width=${INNER} height=${INNER} stroke-width="1.5" fill="none" />
|
|
265
|
+
<line class="line" x1=${a} y1=${b} x2=${e} y2=${b} stroke-width="1" />
|
|
266
|
+
<line class="line" x1=${a} y1=${d} x2=${e} y2=${d} stroke-width="1" />
|
|
267
|
+
<line class="line" x1=${b} y1=${a} x2=${b} y2=${e} stroke-width="1" />
|
|
268
|
+
<line class="line" x1=${d} y1=${a} x2=${d} y2=${e} stroke-width="1" />
|
|
269
|
+
<line class="line" x1=${a} y1=${c} x2=${b} y2=${c} stroke-width="1" />
|
|
270
|
+
<line class="line" x1=${d} y1=${c} x2=${e} y2=${c} stroke-width="1" />
|
|
271
|
+
<line class="line" x1=${c} y1=${a} x2=${c} y2=${b} stroke-width="1" />
|
|
272
|
+
<line class="line" x1=${c} y1=${d} x2=${c} y2=${e} stroke-width="1" />
|
|
273
|
+
${
|
|
274
|
+
divisionLabel
|
|
275
|
+
? svg`<text class="centre-label" x=${CENTRE} y=${CENTRE} text-anchor="middle" dominant-baseline="central">${divisionLabel}</text>`
|
|
276
|
+
: nothing
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
208
280
|
|
|
209
281
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
282
|
+
* House number for a given sign relative to a Lagna sign. House 1 is the
|
|
283
|
+
* Lagna cell; subsequent houses follow the zodiac in order. Returns 0 when
|
|
284
|
+
* the Lagna sign is unknown so the caller can skip rendering the badge.
|
|
212
285
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
286
|
+
function houseNumberInSign(sign: string, lagnaSign: string): number {
|
|
287
|
+
const lagnaIdx = SIGNS_ORDER.findIndex((s) => s === lagnaSign);
|
|
288
|
+
const signIdx = SIGNS_ORDER.findIndex((s) => s === sign);
|
|
289
|
+
if (lagnaIdx === -1 || signIdx === -1) return 0;
|
|
290
|
+
return ((signIdx - lagnaIdx + 12) % 12) + 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function renderSouthCell(
|
|
294
|
+
sign: string,
|
|
295
|
+
planets: PlacedGraha[],
|
|
296
|
+
isLagna: boolean,
|
|
297
|
+
houseNum: number,
|
|
298
|
+
): TemplateResult {
|
|
299
|
+
const r = southCellRect(sign);
|
|
300
|
+
const cx = r.x + r.w / 2;
|
|
301
|
+
const cy = r.y + r.h / 2;
|
|
302
|
+
const signAbbr = SIGN_ABBR[sign] ?? sign.slice(0, 2);
|
|
303
|
+
// Inset the Lagna diagonal so it does not collide with the chart frame on
|
|
304
|
+
// corner cells (Pisces, Gemini, Virgo, Sagittarius) or with the sign label
|
|
305
|
+
// in the top-left of every cell.
|
|
306
|
+
const slashInset = 14;
|
|
221
307
|
return svg`
|
|
222
|
-
<g>
|
|
308
|
+
<g class=${isLagna ? 'cell lagna' : 'cell'}>
|
|
223
309
|
${
|
|
224
|
-
|
|
225
|
-
? svg
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
/>`
|
|
310
|
+
isLagna
|
|
311
|
+
? svg`
|
|
312
|
+
<rect class="lagna-bg" x=${r.x} y=${r.y} width=${r.w} height=${r.h} />
|
|
313
|
+
<line class="lagna-slash" x1=${r.x + r.w - slashInset} y1=${r.y + slashInset} x2=${r.x + slashInset} y2=${r.y + r.h - slashInset} stroke-width="1.2" />
|
|
314
|
+
`
|
|
230
315
|
: nothing
|
|
231
316
|
}
|
|
317
|
+
<text class="sign-text" x=${r.x + 6} y=${r.y + 12} text-anchor="start" dominant-baseline="central">${signAbbr}</text>
|
|
232
318
|
${
|
|
233
|
-
|
|
234
|
-
? svg`<text class="
|
|
319
|
+
houseNum > 0
|
|
320
|
+
? svg`<text class="house-num" x=${r.x + r.w - 6} y=${r.y + 12} text-anchor="end" dominant-baseline="central">${houseNum}</text>`
|
|
235
321
|
: nothing
|
|
236
322
|
}
|
|
237
|
-
${
|
|
238
|
-
h.isLagna
|
|
239
|
-
? svg`<text class="lagna-marker" x=${center.x} y=${center.y - 18} text-anchor="middle" dominant-baseline="central">LAGNA</text>`
|
|
240
|
-
: nothing
|
|
241
|
-
}
|
|
242
|
-
${renderPlanetStack(h.planets, center.x, baseY, 13)}
|
|
323
|
+
${planets.length ? renderPlanetStack(planets, sign, cx, cy + 4, 14) : nothing}
|
|
243
324
|
</g>
|
|
244
325
|
`;
|
|
245
326
|
}
|
|
246
327
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
* Returns the SVG structural lines; call `renderNorthHouseGroup` for content.
|
|
250
|
-
*/
|
|
251
|
-
export function renderNorthFrame(): TemplateResult {
|
|
328
|
+
function renderSouthSvg(vm: KundliViewModel): TemplateResult {
|
|
329
|
+
const lagnaKey = vm.lagnaSign.toLowerCase();
|
|
252
330
|
return svg`
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
331
|
+
${renderSouthFrame(vm.divisionLabel)}
|
|
332
|
+
${SIGNS_ORDER.map((sign) =>
|
|
333
|
+
renderSouthCell(
|
|
334
|
+
sign,
|
|
335
|
+
vm.placements[sign.toLowerCase()] ?? [],
|
|
336
|
+
sign.toLowerCase() === lagnaKey,
|
|
337
|
+
houseNumberInSign(sign, vm.lagnaSign),
|
|
338
|
+
),
|
|
339
|
+
)}
|
|
260
340
|
`;
|
|
261
341
|
}
|
|
262
342
|
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// North Indian: outer square + inscribed midpoint diamond + both outer
|
|
345
|
+
// diagonals. 12 cells: 4 cardinal diamonds + 8 corner triangles. Houses are
|
|
346
|
+
// FIXED (H1 always top-centre); signs rotate from the Lagna sign.
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
const NORTH_VERTICES = {
|
|
350
|
+
tl: { x: MARGIN, y: MARGIN },
|
|
351
|
+
tr: { x: VIEW_BOX - MARGIN, y: MARGIN },
|
|
352
|
+
br: { x: VIEW_BOX - MARGIN, y: VIEW_BOX - MARGIN },
|
|
353
|
+
bl: { x: MARGIN, y: VIEW_BOX - MARGIN },
|
|
354
|
+
top: { x: CENTRE, y: MARGIN },
|
|
355
|
+
right: { x: VIEW_BOX - MARGIN, y: CENTRE },
|
|
356
|
+
bottom: { x: CENTRE, y: VIEW_BOX - MARGIN },
|
|
357
|
+
left: { x: MARGIN, y: CENTRE },
|
|
358
|
+
tlMid: { x: CENTRE - INNER / 4, y: CENTRE - INNER / 4 },
|
|
359
|
+
trMid: { x: CENTRE + INNER / 4, y: CENTRE - INNER / 4 },
|
|
360
|
+
brMid: { x: CENTRE + INNER / 4, y: CENTRE + INNER / 4 },
|
|
361
|
+
blMid: { x: CENTRE - INNER / 4, y: CENTRE + INNER / 4 },
|
|
362
|
+
} as const;
|
|
363
|
+
|
|
263
364
|
/**
|
|
264
|
-
*
|
|
365
|
+
* Centroid (geometric mean) of an arbitrary set of polygon vertices. Used by
|
|
366
|
+
* every cell that needs a label-anchor point; defining it once keeps the
|
|
367
|
+
* North diamond and East triangle math identical.
|
|
265
368
|
*/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
369
|
+
function centroidOf(pts: Array<{ x: number; y: number }>): {
|
|
370
|
+
x: number;
|
|
371
|
+
y: number;
|
|
372
|
+
} {
|
|
373
|
+
const x = pts.reduce((s, p) => s + p.x, 0) / pts.length;
|
|
374
|
+
const y = pts.reduce((s, p) => s + p.y, 0) / pts.length;
|
|
375
|
+
return { x, y };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* House centres for the North Indian diamond. Numbered 1..12 counter-clockwise
|
|
380
|
+
* from the top diamond (H1 is always the ascendant cell). Centroids derived
|
|
381
|
+
* from the canonical geometry above; do not edit by eye, recompute if you
|
|
382
|
+
* change `VIEW_BOX` or `MARGIN`.
|
|
383
|
+
*/
|
|
384
|
+
const NORTH_HOUSE_CENTERS: Record<number, { x: number; y: number }> = {
|
|
385
|
+
1: { x: CENTRE, y: NORTH_VERTICES.tlMid.y },
|
|
386
|
+
2: centroidOf([NORTH_VERTICES.tl, NORTH_VERTICES.top, NORTH_VERTICES.tlMid]),
|
|
387
|
+
3: centroidOf([NORTH_VERTICES.tl, NORTH_VERTICES.left, NORTH_VERTICES.tlMid]),
|
|
388
|
+
4: { x: NORTH_VERTICES.tlMid.x, y: CENTRE },
|
|
389
|
+
5: centroidOf([NORTH_VERTICES.bl, NORTH_VERTICES.left, NORTH_VERTICES.blMid]),
|
|
390
|
+
6: centroidOf([
|
|
391
|
+
NORTH_VERTICES.bl,
|
|
392
|
+
NORTH_VERTICES.bottom,
|
|
393
|
+
NORTH_VERTICES.blMid,
|
|
394
|
+
]),
|
|
395
|
+
7: { x: CENTRE, y: NORTH_VERTICES.blMid.y },
|
|
396
|
+
8: centroidOf([
|
|
397
|
+
NORTH_VERTICES.br,
|
|
398
|
+
NORTH_VERTICES.bottom,
|
|
399
|
+
NORTH_VERTICES.brMid,
|
|
400
|
+
]),
|
|
401
|
+
9: centroidOf([
|
|
402
|
+
NORTH_VERTICES.br,
|
|
403
|
+
NORTH_VERTICES.right,
|
|
404
|
+
NORTH_VERTICES.brMid,
|
|
405
|
+
]),
|
|
406
|
+
10: { x: NORTH_VERTICES.brMid.x, y: CENTRE },
|
|
407
|
+
11: centroidOf([
|
|
408
|
+
NORTH_VERTICES.tr,
|
|
409
|
+
NORTH_VERTICES.right,
|
|
410
|
+
NORTH_VERTICES.trMid,
|
|
411
|
+
]),
|
|
412
|
+
12: centroidOf([NORTH_VERTICES.tr, NORTH_VERTICES.top, NORTH_VERTICES.trMid]),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Rashi number (1..12, Aries=1) occupying the given house when the Lagna sits
|
|
417
|
+
* in `lagnaSign`. House 1 is the Lagna sign; subsequent houses follow the
|
|
418
|
+
* zodiac in order.
|
|
419
|
+
*/
|
|
420
|
+
function rashiInHouse(houseNum: number, lagnaSign: string): number {
|
|
421
|
+
const lagnaIdx = SIGNS_ORDER.findIndex((s) => s === lagnaSign);
|
|
422
|
+
if (lagnaIdx === -1) return houseNum;
|
|
423
|
+
return ((lagnaIdx + houseNum - 1) % 12) + 1;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function renderNorthFrame(divisionLabel?: string): TemplateResult {
|
|
427
|
+
const { tl, tr, br, bl, top, right, bottom, left } = NORTH_VERTICES;
|
|
272
428
|
return svg`
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
429
|
+
<rect class="line" x=${tl.x} y=${tl.y} width=${INNER} height=${INNER} stroke-width="1.5" fill="none" />
|
|
430
|
+
<polygon class="line" points="${top.x},${top.y} ${right.x},${right.y} ${bottom.x},${bottom.y} ${left.x},${left.y}" stroke-width="1" fill="none" />
|
|
431
|
+
<line class="line" x1=${tl.x} y1=${tl.y} x2=${br.x} y2=${br.y} stroke-width="1" />
|
|
432
|
+
<line class="line" x1=${tr.x} y1=${tr.y} x2=${bl.x} y2=${bl.y} stroke-width="1" />
|
|
433
|
+
${
|
|
434
|
+
divisionLabel
|
|
435
|
+
? svg`<text class="centre-label" x=${CENTRE} y=${CENTRE} text-anchor="middle" dominant-baseline="central">${divisionLabel}</text>`
|
|
436
|
+
: nothing
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderNorthCell(
|
|
442
|
+
houseNum: number,
|
|
443
|
+
rashiNum: number,
|
|
444
|
+
sign: string,
|
|
445
|
+
planets: PlacedGraha[],
|
|
446
|
+
isLagna: boolean,
|
|
447
|
+
): TemplateResult {
|
|
448
|
+
const c = NORTH_HOUSE_CENTERS[houseNum];
|
|
449
|
+
if (!c) return svg``;
|
|
450
|
+
// Tight cells (H2/3/5/6/8/9/11/12 corner triangles) clip the rasi number
|
|
451
|
+
// when it sits too high above the centroid. Clamp the upward offset based
|
|
452
|
+
// on the cell's distance from the chart vertical centre so the label
|
|
453
|
+
// always stays comfortably inside its triangle or diamond.
|
|
454
|
+
const rashiOffsetY = Math.min(14, Math.abs(c.y - CENTRE) * 0.45 + 6);
|
|
455
|
+
const ascOffsetY = rashiOffsetY + 12;
|
|
456
|
+
return svg`
|
|
457
|
+
<g class=${isLagna ? 'cell lagna' : 'cell'}>
|
|
458
|
+
<text class="rashi-num" x=${c.x} y=${c.y - rashiOffsetY} text-anchor="middle" dominant-baseline="central">${rashiNum}</text>
|
|
279
459
|
${
|
|
280
|
-
|
|
281
|
-
? svg`<text class="
|
|
460
|
+
isLagna
|
|
461
|
+
? svg`<text class="lagna-marker" x=${c.x} y=${c.y - ascOffsetY} text-anchor="middle" dominant-baseline="central">Asc</text>`
|
|
282
462
|
: nothing
|
|
283
463
|
}
|
|
284
|
-
|
|
285
|
-
${renderPlanetStack(h.planets, center.x, center.y + 14, 11)}
|
|
464
|
+
${planets.length ? renderPlanetStack(planets, sign, c.x, c.y + 8, 12) : nothing}
|
|
286
465
|
</g>
|
|
287
466
|
`;
|
|
288
467
|
}
|
|
289
468
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*/
|
|
293
|
-
export function renderSouthFrame(): TemplateResult {
|
|
469
|
+
function renderNorthSvg(vm: KundliViewModel): TemplateResult {
|
|
470
|
+
const lagnaSign = vm.lagnaSign || 'Aries';
|
|
294
471
|
return svg`
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
472
|
+
${renderNorthFrame(vm.divisionLabel)}
|
|
473
|
+
${Array.from({ length: 12 }, (_, i) => {
|
|
474
|
+
const houseNum = i + 1;
|
|
475
|
+
const rashiNum = rashiInHouse(houseNum, lagnaSign);
|
|
476
|
+
const sign = SIGNS_ORDER[rashiNum - 1] ?? 'Aries';
|
|
477
|
+
return renderNorthCell(
|
|
478
|
+
houseNum,
|
|
479
|
+
rashiNum,
|
|
480
|
+
sign,
|
|
481
|
+
vm.placements[sign.toLowerCase()] ?? [],
|
|
482
|
+
houseNum === 1,
|
|
483
|
+
);
|
|
484
|
+
})}
|
|
305
485
|
`;
|
|
306
486
|
}
|
|
307
487
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// East Indian (Bengali / Maithili): 3x3 underlying grid, 4 edge rectangles +
|
|
490
|
+
// 4 corner cells each split by a diagonal from the outer chart corner to the
|
|
491
|
+
// inner corner of the centre cell. Aries fixed top-centre; signs proceed
|
|
492
|
+
// counter-clockwise. Houses rotate from the Lagna.
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
const EAST_CELL = INNER / 3; // 120
|
|
496
|
+
|
|
497
|
+
interface EastCell {
|
|
498
|
+
/** Vertices of the cell polygon, in viewBox units. */
|
|
499
|
+
points: Array<{ x: number; y: number }>;
|
|
500
|
+
/** Visual centroid for label placement. */
|
|
501
|
+
centroid: { x: number; y: number };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function eastCells(): Record<string, EastCell> {
|
|
505
|
+
const a = MARGIN; // 20
|
|
506
|
+
const b = MARGIN + EAST_CELL; // 140
|
|
507
|
+
const c = MARGIN + 2 * EAST_CELL; // 260
|
|
508
|
+
const d = VIEW_BOX - MARGIN; // 380
|
|
509
|
+
const aries = [
|
|
510
|
+
{ x: b, y: a },
|
|
511
|
+
{ x: c, y: a },
|
|
512
|
+
{ x: c, y: b },
|
|
513
|
+
{ x: b, y: b },
|
|
514
|
+
];
|
|
515
|
+
const cancer = [
|
|
516
|
+
{ x: a, y: b },
|
|
517
|
+
{ x: b, y: b },
|
|
518
|
+
{ x: b, y: c },
|
|
519
|
+
{ x: a, y: c },
|
|
520
|
+
];
|
|
521
|
+
const libra = [
|
|
522
|
+
{ x: b, y: c },
|
|
523
|
+
{ x: c, y: c },
|
|
524
|
+
{ x: c, y: d },
|
|
525
|
+
{ x: b, y: d },
|
|
526
|
+
];
|
|
527
|
+
const capricorn = [
|
|
528
|
+
{ x: c, y: b },
|
|
529
|
+
{ x: d, y: b },
|
|
530
|
+
{ x: d, y: c },
|
|
531
|
+
{ x: c, y: c },
|
|
532
|
+
];
|
|
533
|
+
const taurus = [
|
|
534
|
+
{ x: a, y: a },
|
|
535
|
+
{ x: b, y: a },
|
|
536
|
+
{ x: b, y: b },
|
|
537
|
+
];
|
|
538
|
+
const gemini = [
|
|
539
|
+
{ x: a, y: a },
|
|
540
|
+
{ x: b, y: b },
|
|
541
|
+
{ x: a, y: b },
|
|
542
|
+
];
|
|
543
|
+
const leo = [
|
|
544
|
+
{ x: a, y: c },
|
|
545
|
+
{ x: b, y: c },
|
|
546
|
+
{ x: a, y: d },
|
|
547
|
+
];
|
|
548
|
+
const virgo = [
|
|
549
|
+
{ x: b, y: c },
|
|
550
|
+
{ x: b, y: d },
|
|
551
|
+
{ x: a, y: d },
|
|
552
|
+
];
|
|
553
|
+
const scorpio = [
|
|
554
|
+
{ x: c, y: c },
|
|
555
|
+
{ x: c, y: d },
|
|
556
|
+
{ x: d, y: d },
|
|
557
|
+
];
|
|
558
|
+
const sagittarius = [
|
|
559
|
+
{ x: c, y: c },
|
|
560
|
+
{ x: d, y: d },
|
|
561
|
+
{ x: d, y: c },
|
|
562
|
+
];
|
|
563
|
+
const aquarius = [
|
|
564
|
+
{ x: d, y: a },
|
|
565
|
+
{ x: d, y: b },
|
|
566
|
+
{ x: c, y: b },
|
|
567
|
+
];
|
|
568
|
+
const pisces = [
|
|
569
|
+
{ x: c, y: a },
|
|
570
|
+
{ x: d, y: a },
|
|
571
|
+
{ x: c, y: b },
|
|
572
|
+
];
|
|
573
|
+
const polys = {
|
|
574
|
+
Aries: aries,
|
|
575
|
+
Taurus: taurus,
|
|
576
|
+
Gemini: gemini,
|
|
577
|
+
Cancer: cancer,
|
|
578
|
+
Leo: leo,
|
|
579
|
+
Virgo: virgo,
|
|
580
|
+
Libra: libra,
|
|
581
|
+
Scorpio: scorpio,
|
|
582
|
+
Sagittarius: sagittarius,
|
|
583
|
+
Capricorn: capricorn,
|
|
584
|
+
Aquarius: aquarius,
|
|
585
|
+
Pisces: pisces,
|
|
586
|
+
} as const;
|
|
587
|
+
const out: Record<string, EastCell> = {};
|
|
588
|
+
for (const [sign, points] of Object.entries(polys)) {
|
|
589
|
+
out[sign] = { points: [...points], centroid: centroidOf(points) };
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const EAST_CELLS = eastCells();
|
|
595
|
+
|
|
596
|
+
function renderEastFrame(divisionLabel?: string): TemplateResult {
|
|
597
|
+
const a = MARGIN;
|
|
598
|
+
const b = MARGIN + EAST_CELL;
|
|
599
|
+
const c = MARGIN + 2 * EAST_CELL;
|
|
600
|
+
const d = VIEW_BOX - MARGIN;
|
|
313
601
|
return svg`
|
|
314
|
-
<rect class="line" x
|
|
315
|
-
<line class="line" x1
|
|
316
|
-
<line class="line" x1
|
|
317
|
-
<
|
|
602
|
+
<rect class="line" x=${a} y=${a} width=${INNER} height=${INNER} stroke-width="1.5" fill="none" />
|
|
603
|
+
<line class="line" x1=${a} y1=${b} x2=${b} y2=${b} stroke-width="1" />
|
|
604
|
+
<line class="line" x1=${c} y1=${b} x2=${d} y2=${b} stroke-width="1" />
|
|
605
|
+
<line class="line" x1=${a} y1=${c} x2=${b} y2=${c} stroke-width="1" />
|
|
606
|
+
<line class="line" x1=${c} y1=${c} x2=${d} y2=${c} stroke-width="1" />
|
|
607
|
+
<line class="line" x1=${b} y1=${a} x2=${b} y2=${b} stroke-width="1" />
|
|
608
|
+
<line class="line" x1=${b} y1=${c} x2=${b} y2=${d} stroke-width="1" />
|
|
609
|
+
<line class="line" x1=${c} y1=${a} x2=${c} y2=${b} stroke-width="1" />
|
|
610
|
+
<line class="line" x1=${c} y1=${c} x2=${c} y2=${d} stroke-width="1" />
|
|
611
|
+
<line class="line" x1=${a} y1=${a} x2=${b} y2=${b} stroke-width="1" />
|
|
612
|
+
<line class="line" x1=${d} y1=${a} x2=${c} y2=${b} stroke-width="1" />
|
|
613
|
+
<line class="line" x1=${d} y1=${d} x2=${c} y2=${c} stroke-width="1" />
|
|
614
|
+
<line class="line" x1=${a} y1=${d} x2=${b} y2=${c} stroke-width="1" />
|
|
615
|
+
${
|
|
616
|
+
divisionLabel
|
|
617
|
+
? svg`<text class="centre-label" x=${CENTRE} y=${CENTRE} text-anchor="middle" dominant-baseline="central">${divisionLabel}</text>`
|
|
618
|
+
: nothing
|
|
619
|
+
}
|
|
318
620
|
`;
|
|
319
621
|
}
|
|
320
622
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const signAbbr = SIGN_ABBR[h.sign] ?? '';
|
|
623
|
+
function renderEastCell(
|
|
624
|
+
sign: string,
|
|
625
|
+
planets: PlacedGraha[],
|
|
626
|
+
isLagna: boolean,
|
|
627
|
+
houseNum: number,
|
|
628
|
+
): TemplateResult {
|
|
629
|
+
const cell = EAST_CELLS[sign];
|
|
630
|
+
if (!cell) return svg``;
|
|
631
|
+
const { centroid: cen, points } = cell;
|
|
632
|
+
const signAbbr = SIGN_ABBR[sign] ?? sign.slice(0, 2);
|
|
633
|
+
const polyPoints = points.map((p) => `${p.x},${p.y}`).join(' ');
|
|
333
634
|
return svg`
|
|
334
|
-
<g>
|
|
635
|
+
<g class=${isLagna ? 'cell lagna' : 'cell'}>
|
|
335
636
|
${
|
|
336
|
-
|
|
337
|
-
? svg`<
|
|
637
|
+
isLagna
|
|
638
|
+
? svg`<polygon class="lagna-bg" points=${polyPoints} />`
|
|
338
639
|
: nothing
|
|
339
640
|
}
|
|
641
|
+
<text class="sign-text" x=${cen.x} y=${cen.y - 16} text-anchor="middle" dominant-baseline="central">${signAbbr}</text>
|
|
340
642
|
${
|
|
341
|
-
|
|
342
|
-
? svg`<text class="
|
|
643
|
+
houseNum > 0
|
|
644
|
+
? svg`<text class="house-num" x=${cen.x + 18} y=${cen.y - 16} text-anchor="start" dominant-baseline="central">${houseNum}</text>`
|
|
343
645
|
: nothing
|
|
344
646
|
}
|
|
345
647
|
${
|
|
346
|
-
|
|
347
|
-
? svg`<text class="lagna-marker" x=${
|
|
648
|
+
isLagna
|
|
649
|
+
? svg`<text class="lagna-marker" x=${cen.x} y=${cen.y - 30} text-anchor="middle" dominant-baseline="central">Asc</text>`
|
|
348
650
|
: nothing
|
|
349
651
|
}
|
|
350
|
-
${renderPlanetStack(
|
|
652
|
+
${planets.length ? renderPlanetStack(planets, sign, cen.x, cen.y + 4, 12) : nothing}
|
|
351
653
|
</g>
|
|
352
654
|
`;
|
|
353
655
|
}
|
|
354
656
|
|
|
657
|
+
function renderEastSvg(vm: KundliViewModel): TemplateResult {
|
|
658
|
+
const lagnaKey = vm.lagnaSign.toLowerCase();
|
|
659
|
+
return svg`
|
|
660
|
+
${renderEastFrame(vm.divisionLabel)}
|
|
661
|
+
${SIGNS_ORDER.map((sign) =>
|
|
662
|
+
renderEastCell(
|
|
663
|
+
sign,
|
|
664
|
+
vm.placements[sign.toLowerCase()] ?? [],
|
|
665
|
+
sign.toLowerCase() === lagnaKey,
|
|
666
|
+
houseNumberInSign(sign, vm.lagnaSign),
|
|
667
|
+
),
|
|
668
|
+
)}
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Public entry point
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
|
|
355
676
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* to flag the ascendant cell, not rendered as a planet.
|
|
677
|
+
* Render the kundli body for the requested style. Returns the SVG inner
|
|
678
|
+
* content; the caller wraps it in an `<svg>` element with the canonical
|
|
679
|
+
* viewBox `0 0 400 400` and applies its own theming CSS.
|
|
360
680
|
*/
|
|
361
|
-
export function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
>,
|
|
373
|
-
): HouseDef[] {
|
|
374
|
-
const byRashi = new Map<string, PlacedGraha[]>();
|
|
375
|
-
let lagnaKey = '';
|
|
376
|
-
for (const [name, pos] of Object.entries(meta)) {
|
|
377
|
-
const rashiKey = (pos?.rashi ?? '').toLowerCase();
|
|
378
|
-
if (name === 'Lagna' || pos?.graha === 'Lagna') {
|
|
379
|
-
lagnaKey = rashiKey;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (!rashiKey) continue;
|
|
383
|
-
const list = byRashi.get(rashiKey) ?? [];
|
|
384
|
-
list.push({
|
|
385
|
-
graha: pos.graha ?? name,
|
|
386
|
-
longitude: pos.longitude,
|
|
387
|
-
nakshatra: pos.nakshatra,
|
|
388
|
-
isRetrograde: pos.isRetrograde,
|
|
389
|
-
awastha: pos.awastha,
|
|
390
|
-
});
|
|
391
|
-
byRashi.set(rashiKey, list);
|
|
681
|
+
export function renderKundliSvg(
|
|
682
|
+
vm: KundliViewModel,
|
|
683
|
+
style: ChartStyle,
|
|
684
|
+
): TemplateResult {
|
|
685
|
+
switch (style) {
|
|
686
|
+
case 'north':
|
|
687
|
+
return renderNorthSvg(vm);
|
|
688
|
+
case 'east':
|
|
689
|
+
return renderEastSvg(vm);
|
|
690
|
+
default:
|
|
691
|
+
return renderSouthSvg(vm);
|
|
392
692
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Render a WAI-ARIA-compliant tablist that lets the end user switch between
|
|
697
|
+
* South / North / East kundli styles at runtime. The hosting component owns
|
|
698
|
+
* the `chartStyle` state; this helper renders the buttons and wires the
|
|
699
|
+
* arrow-key navigation plus click handler.
|
|
700
|
+
*
|
|
701
|
+
* @param active - The currently selected style.
|
|
702
|
+
* @param setStyle - Callback the host component uses to update its state.
|
|
703
|
+
*/
|
|
704
|
+
export function renderKundliStyleTablist(
|
|
705
|
+
active: ChartStyle,
|
|
706
|
+
setStyle: (next: ChartStyle) => void,
|
|
707
|
+
): TemplateResult {
|
|
708
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
709
|
+
const idx = CHART_STYLES.findIndex((s) => s.id === active);
|
|
710
|
+
if (e.key === 'ArrowRight') {
|
|
711
|
+
e.preventDefault();
|
|
712
|
+
const next = CHART_STYLES[(idx + 1) % CHART_STYLES.length];
|
|
713
|
+
if (next) setStyle(next.id);
|
|
714
|
+
} else if (e.key === 'ArrowLeft') {
|
|
715
|
+
e.preventDefault();
|
|
716
|
+
const next =
|
|
717
|
+
CHART_STYLES[(idx - 1 + CHART_STYLES.length) % CHART_STYLES.length];
|
|
718
|
+
if (next) setStyle(next.id);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
return html`<div
|
|
722
|
+
class="kundli-tablist"
|
|
723
|
+
role="tablist"
|
|
724
|
+
aria-label="Kundli style"
|
|
725
|
+
@keydown=${onKeyDown}
|
|
726
|
+
>
|
|
727
|
+
${CHART_STYLES.map(
|
|
728
|
+
(s) => html`<button
|
|
729
|
+
type="button"
|
|
730
|
+
class="kundli-tab"
|
|
731
|
+
role="tab"
|
|
732
|
+
id="kundli-tab-${s.id}"
|
|
733
|
+
aria-selected=${active === s.id ? 'true' : 'false'}
|
|
734
|
+
tabindex=${active === s.id ? '0' : '-1'}
|
|
735
|
+
@click=${() => setStyle(s.id)}
|
|
736
|
+
>
|
|
737
|
+
${s.label}
|
|
738
|
+
</button>`,
|
|
739
|
+
)}
|
|
740
|
+
</div>`;
|
|
402
741
|
}
|