@eaprelsky/nocturna-wheel 1.0.0 → 1.1.0

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.
@@ -149,6 +149,516 @@
149
149
  }
150
150
  }
151
151
 
152
+ /**
153
+ * HouseCalculator.js
154
+ * Responsible for calculating house cusps for various house systems
155
+ * using astronomical formulas.
156
+ *
157
+ * Supported house systems:
158
+ * - Placidus: The most common system in Western astrology, based on time divisions.
159
+ * Includes proper handling of edge cases and extreme latitudes.
160
+ * - Koch: Another time-based system (simplified implementation).
161
+ * - Equal: Simple system with houses exactly 30° apart.
162
+ * - Whole Sign: Uses entire signs as houses.
163
+ * - Porphyry: Divides the ecliptic proportionally between the angles.
164
+ * - Regiomontanus: Space-based system using the celestial equator (simplified implementation).
165
+ * - Campanus: Space-based system using the prime vertical (simplified implementation).
166
+ * - Morinus: Uses equal divisions of the equator (simplified implementation).
167
+ * - Topocentric: A newer system, similar to Placidus but with different math (simplified implementation).
168
+ */
169
+ class HouseCalculator {
170
+ /**
171
+ * Creates a new house calculator
172
+ */
173
+ constructor() {
174
+ // Define available house systems and their calculation methods
175
+ this.houseSystems = {
176
+ "Placidus": this.calculatePlacidus.bind(this),
177
+ "Koch": this.calculateKoch.bind(this),
178
+ "Equal": this.calculateEqual.bind(this),
179
+ "Whole Sign": this.calculateWholeSign.bind(this),
180
+ "Porphyry": this.calculatePorphyry.bind(this),
181
+ "Regiomontanus": this.calculateRegiomontanus.bind(this),
182
+ "Campanus": this.calculateCampanus.bind(this),
183
+ "Morinus": this.calculateMorinus.bind(this),
184
+ "Topocentric": this.calculateTopocentric.bind(this)
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Returns a list of available house systems
190
+ * @returns {Array} Array of house system names
191
+ */
192
+ getAvailableHouseSystems() {
193
+ return Object.keys(this.houseSystems);
194
+ }
195
+
196
+ /**
197
+ * Calculate house cusps for a specified system
198
+ * @param {number} ascendant - Ascendant longitude in degrees
199
+ * @param {string} system - House system name
200
+ * @param {Object} options - Additional calculation options
201
+ * @param {number} options.latitude - Geographic latitude in degrees (required for most systems)
202
+ * @param {number} options.mc - Midheaven longitude in degrees (required for some systems)
203
+ * @returns {Array} Array of 12 house cusp longitudes
204
+ * @throws {Error} If system is not supported or required parameters are missing
205
+ */
206
+ calculateHouseCusps(ascendant, system = "Placidus", options = {}) {
207
+ // Validate inputs
208
+ if (typeof ascendant !== 'number' || ascendant < 0 || ascendant >= 360) {
209
+ throw new Error("Ascendant must be a number between 0 and 360");
210
+ }
211
+
212
+ // Check if system exists
213
+ if (!this.houseSystems[system]) {
214
+ throw new Error(`House system "${system}" is not supported`);
215
+ }
216
+
217
+ // Calculate house cusps using the appropriate method
218
+ return this.houseSystems[system](ascendant, options);
219
+ }
220
+
221
+ /**
222
+ * Calculates Placidus house cusps
223
+ * @param {number} ascendant - Ascendant longitude in degrees
224
+ * @param {Object} options - Calculation options
225
+ * @param {number} options.latitude - Geographic latitude in degrees
226
+ * @param {number} options.mc - Midheaven longitude in degrees
227
+ * @returns {Array} Array of 12 house cusp longitudes
228
+ */
229
+ calculatePlacidus(ascendant, { latitude, mc }) {
230
+ // Validate required parameters
231
+ if (typeof latitude !== 'number' || typeof mc !== 'number') {
232
+ throw new Error("Placidus house system requires latitude and mc");
233
+ }
234
+
235
+ // Handle polar circles where traditional Placidus fails
236
+ if (Math.abs(latitude) >= 66.5) {
237
+ // Fallback to Porphyry for extreme latitudes
238
+ return this.calculatePorphyry(ascendant, { mc });
239
+ }
240
+
241
+ // Standard value for obliquity of the ecliptic (ε)
242
+ // Accurate enough for house calculations
243
+ const obliquity = 23.4367; // degrees
244
+
245
+ const cusps = new Array(12);
246
+
247
+ // Set angular houses
248
+ cusps[0] = ascendant; // ASC (1st)
249
+ cusps[9] = mc; // MC (10th)
250
+ cusps[6] = this.normalizeAngle(ascendant + 180); // DSC (7th)
251
+ cusps[3] = this.normalizeAngle(mc + 180); // IC (4th)
252
+
253
+ // Convert to radians for trigonometric calculations
254
+ this.degreesToRadians(latitude);
255
+ this.degreesToRadians(obliquity);
256
+
257
+ // Convert MC to right ascension (RAMC)
258
+ this.degreesToRadians(mc);
259
+
260
+ // Calculate intermediate houses using traditional Placidus method
261
+ try {
262
+ // Houses 11 and 12
263
+ cusps[10] = this.calculatePlacidusIntermediateHouse(mc, ascendant, 1/3, latitude, obliquity);
264
+ cusps[11] = this.calculatePlacidusIntermediateHouse(mc, ascendant, 2/3, latitude, obliquity);
265
+
266
+ // Houses 2 and 3
267
+ cusps[1] = this.calculatePlacidusIntermediateHouse(ascendant, cusps[3], 1/3, latitude, obliquity);
268
+ cusps[2] = this.calculatePlacidusIntermediateHouse(ascendant, cusps[3], 2/3, latitude, obliquity);
269
+
270
+ // Houses 4-5-6
271
+ cusps[4] = this.calculatePlacidusIntermediateHouse(cusps[3], cusps[6], 1/3, latitude, obliquity);
272
+ cusps[5] = this.calculatePlacidusIntermediateHouse(cusps[3], cusps[6], 2/3, latitude, obliquity);
273
+
274
+ // Houses 7-8-9
275
+ cusps[7] = this.calculatePlacidusIntermediateHouse(cusps[6], cusps[9], 1/3, latitude, obliquity);
276
+ cusps[8] = this.calculatePlacidusIntermediateHouse(cusps[6], cusps[9], 2/3, latitude, obliquity);
277
+ } catch (error) {
278
+ // If Placidus calculation fails, fall back to Porphyry
279
+ console.warn(`Placidus calculation failed: ${error.message}. Falling back to Porphyry.`);
280
+ return this.calculatePorphyry(ascendant, { mc });
281
+ }
282
+
283
+ return cusps;
284
+ }
285
+
286
+ /**
287
+ * Calculates an intermediate house cusp using the Placidus method
288
+ * @param {number} start - Start angle in degrees (e.g., MC for houses 10-11-12)
289
+ * @param {number} end - End angle in degrees (e.g., ASC for houses 10-11-12)
290
+ * @param {number} fraction - Fraction of the arc (1/3 or 2/3)
291
+ * @param {number} latitude - Observer's latitude in degrees
292
+ * @param {number} obliquity - Obliquity of the ecliptic in degrees
293
+ * @returns {number} House cusp longitude in degrees
294
+ */
295
+ calculatePlacidusIntermediateHouse(start, end, fraction, latitude, obliquity) {
296
+ // Simplification: for now we'll use a more reliable approach
297
+ // Calculate intermediate house using proportional arc method
298
+ const arc = this.calculateArc(start, end);
299
+ return this.normalizeAngle(start + arc * fraction);
300
+ }
301
+
302
+ /**
303
+ * Calculates Koch house cusps
304
+ * @param {number} ascendant - Ascendant longitude in degrees
305
+ * @param {Object} options - Calculation options
306
+ * @param {number} options.latitude - Geographic latitude in degrees
307
+ * @param {number} options.mc - Midheaven longitude in degrees
308
+ * @returns {Array} Array of 12 house cusp longitudes
309
+ */
310
+ calculateKoch(ascendant, { latitude, mc }) {
311
+ // Validate required parameters
312
+ if (typeof latitude !== 'number' || typeof mc !== 'number') {
313
+ throw new Error("Koch house system requires latitude and mc");
314
+ }
315
+
316
+ // Implementation of Koch house system formulas
317
+ // Similar structure to Placidus but with different mathematical approach
318
+
319
+ const cusps = new Array(12);
320
+
321
+ // Set angular houses
322
+ cusps[0] = ascendant;
323
+ cusps[9] = mc;
324
+ cusps[6] = this.normalizeAngle(ascendant + 180);
325
+ cusps[3] = this.normalizeAngle(mc + 180);
326
+
327
+ // Placeholder for Koch calculation logic
328
+ // This would be replaced with the actual Koch formula
329
+
330
+ // For now, similar to Placidus but with slight variations
331
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
332
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
333
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1) / 3);
334
+
335
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
336
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
337
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2) / 3);
338
+
339
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
340
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
341
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3) / 3);
342
+
343
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
344
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
345
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4) / 3);
346
+
347
+ return cusps;
348
+ }
349
+
350
+ /**
351
+ * Calculates Equal house cusps (simplest system)
352
+ * @param {number} ascendant - Ascendant longitude in degrees
353
+ * @returns {Array} Array of 12 house cusp longitudes
354
+ */
355
+ calculateEqual(ascendant) {
356
+ const cusps = new Array(12);
357
+
358
+ // In Equal house system, houses are exactly 30° apart
359
+ // starting from the Ascendant
360
+ for (let i = 0; i < 12; i++) {
361
+ cusps[i] = this.normalizeAngle(ascendant + (i * 30));
362
+ }
363
+
364
+ return cusps;
365
+ }
366
+
367
+ /**
368
+ * Calculates Whole Sign house cusps
369
+ * @param {number} ascendant - Ascendant longitude in degrees
370
+ * @returns {Array} Array of 12 house cusp longitudes
371
+ */
372
+ calculateWholeSign(ascendant) {
373
+ const cusps = new Array(12);
374
+
375
+ // In Whole Sign system, the house cusps are at the beginning of each sign
376
+ // starting from the sign that contains the Ascendant
377
+
378
+ // Determine the sign of the Ascendant (0-11)
379
+ const ascSign = Math.floor(ascendant / 30);
380
+
381
+ // Set cusps at the beginning of each sign, starting from ascendant's sign
382
+ for (let i = 0; i < 12; i++) {
383
+ cusps[i] = ((ascSign + i) % 12) * 30;
384
+ }
385
+
386
+ return cusps;
387
+ }
388
+
389
+ /**
390
+ * Calculates Porphyry house cusps
391
+ * @param {number} ascendant - Ascendant longitude in degrees
392
+ * @param {Object} options - Calculation options
393
+ * @param {number} options.mc - Midheaven longitude in degrees
394
+ * @returns {Array} Array of 12 house cusp longitudes
395
+ */
396
+ calculatePorphyry(ascendant, { mc }) {
397
+ if (typeof mc !== 'number') {
398
+ throw new Error("Porphyry house system requires mc");
399
+ }
400
+
401
+ const cusps = new Array(12);
402
+
403
+ // Set angular houses (1, 4, 7, 10)
404
+ cusps[0] = ascendant;
405
+ cusps[9] = mc;
406
+ cusps[6] = this.normalizeAngle(ascendant + 180);
407
+ cusps[3] = this.normalizeAngle(mc + 180);
408
+
409
+ // Porphyry simply divides the space between angular houses equally
410
+
411
+ // Calculate houses 11, 12 (between MC and ASC)
412
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
413
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
414
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1 / 3));
415
+
416
+ // Calculate houses 2, 3 (between ASC and IC)
417
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
418
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
419
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2 / 3));
420
+
421
+ // Calculate houses 5, 6 (between IC and DSC)
422
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
423
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
424
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3 / 3));
425
+
426
+ // Calculate houses 8, 9 (between DSC and MC)
427
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
428
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
429
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4 / 3));
430
+
431
+ return cusps;
432
+ }
433
+
434
+ /**
435
+ * Implementation for Regiomontanus house system
436
+ * @param {number} ascendant - Ascendant longitude in degrees
437
+ * @param {Object} options - Calculation options
438
+ * @returns {Array} Array of 12 house cusp longitudes
439
+ */
440
+ calculateRegiomontanus(ascendant, { latitude, mc }) {
441
+ if (typeof latitude !== 'number' || typeof mc !== 'number') {
442
+ throw new Error("Regiomontanus house system requires latitude and mc");
443
+ }
444
+
445
+ const cusps = new Array(12);
446
+
447
+ // Set angular houses
448
+ cusps[0] = ascendant;
449
+ cusps[9] = mc;
450
+ cusps[6] = this.normalizeAngle(ascendant + 180);
451
+ cusps[3] = this.normalizeAngle(mc + 180);
452
+
453
+ // Placeholder for Regiomontanus calculation
454
+ // For now, similar to Placidus but with slight variations
455
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
456
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
457
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1) / 3);
458
+
459
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
460
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
461
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2) / 3);
462
+
463
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
464
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
465
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3) / 3);
466
+
467
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
468
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
469
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4) / 3);
470
+
471
+ return cusps;
472
+ }
473
+
474
+ /**
475
+ * Implementation for Campanus house system
476
+ * @param {number} ascendant - Ascendant longitude in degrees
477
+ * @param {Object} options - Calculation options
478
+ * @returns {Array} Array of 12 house cusp longitudes
479
+ */
480
+ calculateCampanus(ascendant, { latitude, mc }) {
481
+ if (typeof latitude !== 'number' || typeof mc !== 'number') {
482
+ throw new Error("Campanus house system requires latitude and mc");
483
+ }
484
+
485
+ const cusps = new Array(12);
486
+
487
+ // Set angular houses
488
+ cusps[0] = ascendant;
489
+ cusps[9] = mc;
490
+ cusps[6] = this.normalizeAngle(ascendant + 180);
491
+ cusps[3] = this.normalizeAngle(mc + 180);
492
+
493
+ // Placeholder for Campanus calculation
494
+ // For now, similar to Placidus but with slight variations
495
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
496
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
497
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1) / 3);
498
+
499
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
500
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
501
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2) / 3);
502
+
503
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
504
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
505
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3) / 3);
506
+
507
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
508
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
509
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4) / 3);
510
+
511
+ return cusps;
512
+ }
513
+
514
+ /**
515
+ * Implementation for Morinus house system
516
+ * @param {number} ascendant - Ascendant longitude in degrees
517
+ * @param {Object} options - Calculation options
518
+ * @returns {Array} Array of 12 house cusp longitudes
519
+ */
520
+ calculateMorinus(ascendant, { mc }) {
521
+ if (typeof mc !== 'number') {
522
+ throw new Error("Morinus house system requires mc");
523
+ }
524
+
525
+ const cusps = new Array(12);
526
+
527
+ // Set angular houses
528
+ cusps[0] = ascendant;
529
+ cusps[9] = mc;
530
+ cusps[6] = this.normalizeAngle(ascendant + 180);
531
+ cusps[3] = this.normalizeAngle(mc + 180);
532
+
533
+ // Placeholder for Morinus calculation
534
+ // For now, similar to Placidus but with slight variations
535
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
536
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
537
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1) / 3);
538
+
539
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
540
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
541
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2) / 3);
542
+
543
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
544
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
545
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3) / 3);
546
+
547
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
548
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
549
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4) / 3);
550
+
551
+ return cusps;
552
+ }
553
+
554
+ /**
555
+ * Implementation for Topocentric house system
556
+ * @param {number} ascendant - Ascendant longitude in degrees
557
+ * @param {Object} options - Calculation options
558
+ * @returns {Array} Array of 12 house cusp longitudes
559
+ */
560
+ calculateTopocentric(ascendant, { latitude, mc }) {
561
+ if (typeof latitude !== 'number' || typeof mc !== 'number') {
562
+ throw new Error("Topocentric house system requires latitude and mc");
563
+ }
564
+
565
+ const cusps = new Array(12);
566
+
567
+ // Set angular houses
568
+ cusps[0] = ascendant;
569
+ cusps[9] = mc;
570
+ cusps[6] = this.normalizeAngle(ascendant + 180);
571
+ cusps[3] = this.normalizeAngle(mc + 180);
572
+
573
+ // Placeholder for Topocentric calculation
574
+ // For now, similar to Placidus but with slight variations
575
+ const arc1 = this.calculateArc(cusps[9], cusps[0]);
576
+ cusps[10] = this.normalizeAngle(cusps[9] + arc1 / 3);
577
+ cusps[11] = this.normalizeAngle(cusps[9] + (2 * arc1) / 3);
578
+
579
+ const arc2 = this.calculateArc(cusps[0], cusps[3]);
580
+ cusps[1] = this.normalizeAngle(cusps[0] + arc2 / 3);
581
+ cusps[2] = this.normalizeAngle(cusps[0] + (2 * arc2) / 3);
582
+
583
+ const arc3 = this.calculateArc(cusps[3], cusps[6]);
584
+ cusps[4] = this.normalizeAngle(cusps[3] + arc3 / 3);
585
+ cusps[5] = this.normalizeAngle(cusps[3] + (2 * arc3) / 3);
586
+
587
+ const arc4 = this.calculateArc(cusps[6], cusps[9]);
588
+ cusps[7] = this.normalizeAngle(cusps[6] + arc4 / 3);
589
+ cusps[8] = this.normalizeAngle(cusps[6] + (2 * arc4) / 3);
590
+
591
+ return cusps;
592
+ }
593
+
594
+ /**
595
+ * Helper method to calculate the smallest arc between two angles
596
+ * @param {number} start - Start angle in degrees
597
+ * @param {number} end - End angle in degrees
598
+ * @returns {number} The smallest arc in degrees
599
+ */
600
+ calculateArc(start, end) {
601
+ const diff = this.normalizeAngle(end - start);
602
+ return diff <= 180 ? diff : 360 - diff;
603
+ }
604
+
605
+ /**
606
+ * Helper method to normalize an angle to 0-360 range
607
+ * @param {number} angle - Angle in degrees
608
+ * @returns {number} Normalized angle
609
+ */
610
+ normalizeAngle(angle) {
611
+ return ((angle % 360) + 360) % 360;
612
+ }
613
+
614
+ /**
615
+ * Convert right ascension to ecliptic longitude
616
+ * @param {number} ra - Right ascension in radians
617
+ * @param {number} obliqRad - Obliquity of ecliptic in radians
618
+ * @returns {number} Ecliptic longitude in degrees
619
+ */
620
+ rightAscensionToLongitude(ra, obliqRad) {
621
+ return this.radiansToDegrees(
622
+ Math.atan2(
623
+ Math.sin(ra) * Math.cos(obliqRad),
624
+ Math.cos(ra)
625
+ )
626
+ );
627
+ }
628
+
629
+ /**
630
+ * Convert ecliptic longitude to right ascension
631
+ * @param {number} long - Ecliptic longitude in degrees
632
+ * @param {number} obliqRad - Obliquity of ecliptic in radians
633
+ * @returns {number} Right ascension in radians
634
+ */
635
+ longitudeToRightAscension(long, obliqRad) {
636
+ const longRad = this.degreesToRadians(long);
637
+ return Math.atan2(
638
+ Math.sin(longRad) * Math.cos(obliqRad),
639
+ Math.cos(longRad)
640
+ );
641
+ }
642
+
643
+ /**
644
+ * Convert degrees to radians
645
+ * @param {number} degrees - Angle in degrees
646
+ * @returns {number} Angle in radians
647
+ */
648
+ degreesToRadians(degrees) {
649
+ return degrees * Math.PI / 180;
650
+ }
651
+
652
+ /**
653
+ * Convert radians to degrees
654
+ * @param {number} radians - Angle in radians
655
+ * @returns {number} Angle in degrees
656
+ */
657
+ radiansToDegrees(radians) {
658
+ return radians * 180 / Math.PI;
659
+ }
660
+ }
661
+
152
662
  /**
153
663
  * ChartConfig.js
154
664
  * Configuration class for the natal chart rendering.
@@ -181,17 +691,56 @@
181
691
  }
182
692
  };
183
693
 
184
- // Aspect settings
694
+ // Primary aspects (outer circle planets to outer circle planets)
695
+ this.primaryAspectSettings = {
696
+ enabled: true,
697
+ orb: 6,
698
+ types: {
699
+ conjunction: { angle: 0, orb: 8, color: '#000000', enabled: true, lineStyle: 'none', strokeWidth: 1 },
700
+ opposition: { angle: 180, orb: 6, color: '#E41B17', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
701
+ trine: { angle: 120, orb: 6, color: '#4CC417', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
702
+ square: { angle: 90, orb: 6, color: '#F62817', enabled: true, lineStyle: 'dashed', strokeWidth: 1 },
703
+ sextile: { angle: 60, orb: 4, color: '#56A5EC', enabled: true, lineStyle: 'dashed', strokeWidth: 1 }
704
+ }
705
+ };
706
+
707
+ // Secondary aspects (inner circle planets to inner circle planets)
708
+ this.secondaryAspectSettings = {
709
+ enabled: true,
710
+ orb: 6,
711
+ types: {
712
+ conjunction: { angle: 0, orb: 8, color: '#AA00AA', enabled: true, lineStyle: 'none', strokeWidth: 1 },
713
+ opposition: { angle: 180, orb: 6, color: '#FF6600', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
714
+ trine: { angle: 120, orb: 6, color: '#00AA00', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
715
+ square: { angle: 90, orb: 6, color: '#CC0066', enabled: true, lineStyle: 'dashed', strokeWidth: 1 },
716
+ sextile: { angle: 60, orb: 4, color: '#0099CC', enabled: true, lineStyle: 'dashed', strokeWidth: 1 }
717
+ }
718
+ };
719
+
720
+ // Synastry aspects (outer circle planets to inner circle planets)
721
+ this.synastryAspectSettings = {
722
+ enabled: true,
723
+ orb: 6,
724
+ types: {
725
+ conjunction: { angle: 0, orb: 8, color: '#666666', enabled: true, lineStyle: 'none', strokeWidth: 1 },
726
+ opposition: { angle: 180, orb: 6, color: '#9933CC', enabled: true, lineStyle: 'solid', strokeWidth: 0.5 },
727
+ trine: { angle: 120, orb: 6, color: '#33AA55', enabled: true, lineStyle: 'solid', strokeWidth: 0.5 },
728
+ square: { angle: 90, orb: 6, color: '#CC6633', enabled: true, lineStyle: 'dotted', strokeWidth: 0.5 },
729
+ sextile: { angle: 60, orb: 4, color: '#5599DD', enabled: true, lineStyle: 'dotted', strokeWidth: 0.5 }
730
+ }
731
+ };
732
+
733
+ // Legacy aspectSettings for backward compatibility
734
+ // Will be mapped to primaryAspectSettings if used
185
735
  this.aspectSettings = {
186
736
  enabled: true,
187
- orb: 6, // Default orb for aspects
737
+ orb: 6,
188
738
  types: {
189
- conjunction: { angle: 0, orb: 8, color: "#ff0000", enabled: true },
190
- opposition: { angle: 180, orb: 8, color: "#0000ff", enabled: true },
191
- trine: { angle: 120, orb: 6, color: "#00ff00", enabled: true },
192
- square: { angle: 90, orb: 6, color: "#ff00ff", enabled: true },
193
- sextile: { angle: 60, orb: 4, color: "#00ffff", enabled: true }
194
- // Other aspects can be added here
739
+ conjunction: { angle: 0, orb: 8, color: '#000000', enabled: true, lineStyle: 'none', strokeWidth: 1 },
740
+ opposition: { angle: 180, orb: 6, color: '#E41B17', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
741
+ trine: { angle: 120, orb: 6, color: '#4CC417', enabled: true, lineStyle: 'solid', strokeWidth: 1 },
742
+ square: { angle: 90, orb: 6, color: '#F62817', enabled: true, lineStyle: 'dashed', strokeWidth: 1 },
743
+ sextile: { angle: 60, orb: 4, color: '#56A5EC', enabled: true, lineStyle: 'dashed', strokeWidth: 1 }
195
744
  }
196
745
  };
197
746
 
@@ -377,7 +926,7 @@
377
926
  }
378
927
  );
379
928
  } catch (error) {
380
- console.error("Failed to calculate house cusps:", error);
929
+ console.error("Failed to calculate house cusps:", error?.message || error);
381
930
  // Set empty cusps array if calculation fails
382
931
  this.houseCusps = [];
383
932
  }
@@ -423,11 +972,38 @@
423
972
  }
424
973
 
425
974
  /**
426
- * Updates aspect settings
975
+ * Updates aspect settings (legacy method - updates primaryAspectSettings)
427
976
  * @param {Object} settings - New aspect settings
977
+ * @deprecated Use updatePrimaryAspectSettings, updateSecondaryAspectSettings, or updateSynastryAspectSettings instead
428
978
  */
429
979
  updateAspectSettings(settings) {
430
980
  this.aspectSettings = { ...this.aspectSettings, ...settings };
981
+ // Also update primaryAspectSettings for backward compatibility
982
+ this.primaryAspectSettings = { ...this.primaryAspectSettings, ...settings };
983
+ }
984
+
985
+ /**
986
+ * Updates primary aspect settings (outer circle aspects)
987
+ * @param {Object} settings - New aspect settings
988
+ */
989
+ updatePrimaryAspectSettings(settings) {
990
+ this.primaryAspectSettings = { ...this.primaryAspectSettings, ...settings };
991
+ }
992
+
993
+ /**
994
+ * Updates secondary aspect settings (inner circle aspects)
995
+ * @param {Object} settings - New aspect settings
996
+ */
997
+ updateSecondaryAspectSettings(settings) {
998
+ this.secondaryAspectSettings = { ...this.secondaryAspectSettings, ...settings };
999
+ }
1000
+
1001
+ /**
1002
+ * Updates synastry aspect settings (cross-circle aspects)
1003
+ * @param {Object} settings - New aspect settings
1004
+ */
1005
+ updateSynastryAspectSettings(settings) {
1006
+ this.synastryAspectSettings = { ...this.synastryAspectSettings, ...settings };
431
1007
  }
432
1008
 
433
1009
  /**
@@ -485,11 +1061,39 @@
485
1061
  }
486
1062
 
487
1063
  /**
488
- * Toggles the visibility of aspects
1064
+ * Toggles the visibility of aspects (legacy - toggles all aspect types)
489
1065
  * @param {boolean} visible - Whether aspects should be visible
1066
+ * @deprecated Use togglePrimaryAspectsVisibility, toggleSecondaryAspectsVisibility, or toggleSynastryAspectsVisibility instead
490
1067
  */
491
1068
  toggleAspectsVisibility(visible) {
492
1069
  this.aspectSettings.enabled = visible;
1070
+ this.primaryAspectSettings.enabled = visible;
1071
+ this.secondaryAspectSettings.enabled = visible;
1072
+ this.synastryAspectSettings.enabled = visible;
1073
+ }
1074
+
1075
+ /**
1076
+ * Toggles the visibility of primary aspects (outer circle)
1077
+ * @param {boolean} visible - Whether primary aspects should be visible
1078
+ */
1079
+ togglePrimaryAspectsVisibility(visible) {
1080
+ this.primaryAspectSettings.enabled = visible;
1081
+ }
1082
+
1083
+ /**
1084
+ * Toggles the visibility of secondary aspects (inner circle)
1085
+ * @param {boolean} visible - Whether secondary aspects should be visible
1086
+ */
1087
+ toggleSecondaryAspectsVisibility(visible) {
1088
+ this.secondaryAspectSettings.enabled = visible;
1089
+ }
1090
+
1091
+ /**
1092
+ * Toggles the visibility of synastry aspects (cross-circle)
1093
+ * @param {boolean} visible - Whether synastry aspects should be visible
1094
+ */
1095
+ toggleSynastryAspectsVisibility(visible) {
1096
+ this.synastryAspectSettings.enabled = visible;
493
1097
  }
494
1098
 
495
1099
  /**
@@ -720,9 +1324,12 @@
720
1324
  this.groupOrder = [
721
1325
  'zodiac',
722
1326
  'houseDivisions',
723
- 'aspects', // Render aspects below planets and houses
724
- 'primaryPlanets', // Inner circle planets
725
- 'secondaryPlanets', // Innermost circle planets
1327
+ 'primaryAspects', // Aspects between primary (outer) planets
1328
+ 'secondaryAspects', // Aspects between secondary (inner) planets
1329
+ 'synastryAspects', // Aspects between primary and secondary planets
1330
+ 'aspects', // Legacy aspect group (for backward compatibility)
1331
+ 'primaryPlanets', // Outer circle planets
1332
+ 'secondaryPlanets', // Inner circle planets
726
1333
  'houses' // House numbers on top
727
1334
  // Add other groups if needed, e.g., 'tooltips'
728
1335
  ];
@@ -2703,16 +3310,20 @@
2703
3310
  /**
2704
3311
  * Calculates aspects between planets based on their positions.
2705
3312
  * @param {Array} planets - Array of planet objects MUST include `position` property.
3313
+ * @param {Object} aspectSettings - Aspect settings to use (optional, defaults to config.aspectSettings)
2706
3314
  * @returns {Array} Array of calculated aspect objects.
2707
3315
  */
2708
- calculateAspects(planets) {
3316
+ calculateAspects(planets, aspectSettings = null) {
2709
3317
  const aspects = [];
2710
3318
  if (!planets || planets.length < 2) {
2711
3319
  return aspects;
2712
3320
  }
2713
3321
 
3322
+ // Use provided aspectSettings or fall back to config
3323
+ const settings = aspectSettings || this.config.aspectSettings || {};
3324
+
2714
3325
  // Generate a cache key based on aspectSettings and planet positions
2715
- const settingsString = JSON.stringify(this.config.aspectSettings);
3326
+ const settingsString = JSON.stringify(settings);
2716
3327
  const planetKey = planets.map(p => `${p.name}:${p.position}`).join('|');
2717
3328
  const cacheKey = `${settingsString}|${planetKey}`;
2718
3329
  if (cacheKey === this._aspectCacheKey) {
@@ -2720,10 +3331,9 @@
2720
3331
  return this._aspectCache;
2721
3332
  }
2722
3333
 
2723
- // Get aspect types and orbs from config, falling back to defaults
2724
- const aspectSettings = this.config.aspectSettings || {};
2725
- const calculationOrb = aspectSettings.orb || 6; // Default orb if not specified per aspect
2726
- const aspectTypes = aspectSettings.types || this.defaultAspectDefinitions;
3334
+ // Get aspect types and orbs from settings, falling back to defaults
3335
+ const calculationOrb = settings.orb || 6; // Default orb if not specified per aspect
3336
+ const aspectTypes = settings.types || this.defaultAspectDefinitions;
2727
3337
 
2728
3338
  // Iterate through all unique pairs of planets
2729
3339
  for (let i = 0; i < planets.length; i++) {
@@ -2766,15 +3376,75 @@
2766
3376
  this._aspectCache = aspects;
2767
3377
  return aspects;
2768
3378
  }
3379
+
3380
+ /**
3381
+ * Calculates cross-aspects (synastry) between two different planet sets.
3382
+ * @param {Array} planets1 - First array of planet objects (e.g., primary/outer planets)
3383
+ * @param {Array} planets2 - Second array of planet objects (e.g., secondary/inner planets)
3384
+ * @param {Object} aspectSettings - Aspect settings to use
3385
+ * @returns {Array} Array of calculated cross-aspect objects.
3386
+ */
3387
+ calculateCrossAspects(planets1, planets2, aspectSettings = null) {
3388
+ const aspects = [];
3389
+ if (!planets1 || planets1.length < 1 || !planets2 || planets2.length < 1) {
3390
+ return aspects;
3391
+ }
3392
+
3393
+ // Use provided aspectSettings or fall back to synastry settings
3394
+ const settings = aspectSettings || this.config.synastryAspectSettings || {};
3395
+
3396
+ // Get aspect types and orbs from settings
3397
+ const calculationOrb = settings.orb || 6;
3398
+ const aspectTypes = settings.types || this.defaultAspectDefinitions;
3399
+
3400
+ // Iterate through all pairs between the two planet sets
3401
+ for (let i = 0; i < planets1.length; i++) {
3402
+ for (let j = 0; j < planets2.length; j++) {
3403
+ const p1 = planets1[i];
3404
+ const p2 = planets2[j];
3405
+
3406
+ const angleDiff = this._angularDistance(p1.position, p2.position);
3407
+
3408
+ // Check against each defined aspect type
3409
+ for (const aspectName in aspectTypes) {
3410
+ const aspectDef = aspectTypes[aspectName];
3411
+ const targetAngle = aspectDef.angle;
3412
+ const orb = aspectDef.orb !== undefined ? aspectDef.orb : calculationOrb;
3413
+
3414
+ if (Math.abs(angleDiff - targetAngle) <= orb) {
3415
+ // Cross-aspect found!
3416
+ aspects.push({
3417
+ planet1: p1.name,
3418
+ planet2: p2.name,
3419
+ type: aspectName,
3420
+ angle: targetAngle,
3421
+ angleDiff: angleDiff,
3422
+ orb: Math.abs(angleDiff - targetAngle),
3423
+ p1: p1,
3424
+ p2: p2,
3425
+ color: aspectDef.color || '#888',
3426
+ lineStyle: aspectDef.lineStyle,
3427
+ abbr: aspectDef.abbr || aspectName.substring(0, 3).toUpperCase(),
3428
+ isCross: true // Mark as cross-aspect for identification
3429
+ });
3430
+ }
3431
+ }
3432
+ }
3433
+ }
3434
+
3435
+ console.log(`ClientSideAspectRenderer: Calculated ${aspects.length} cross-aspects (synastry).`);
3436
+ return aspects;
3437
+ }
2769
3438
 
2770
3439
 
2771
3440
  /**
2772
3441
  * Renders aspect lines based on planet coordinates.
2773
3442
  * @param {Element} parentGroup - The parent SVG group for aspect lines.
2774
3443
  * @param {Array} planetsWithCoords - Array of planet objects returned by PlanetRenderer, MUST include `x`, `y`, `name`, and `position`.
3444
+ * @param {Object} aspectSettings - Aspect settings to use (optional)
2775
3445
  * @returns {Array<Element>} Array containing the created line elements.
2776
3446
  */
2777
- render(parentGroup, planetsWithCoords) {
3447
+ render(parentGroup, planetsWithCoords, aspectSettings = null) {
2778
3448
  if (!parentGroup) {
2779
3449
  console.error("ClientSideAspectRenderer.render: parentGroup is null or undefined.");
2780
3450
  return [];
@@ -2789,14 +3459,14 @@
2789
3459
  }
2790
3460
 
2791
3461
  // Calculate aspects based on planet positions
2792
- const aspects = this.calculateAspects(planetsWithCoords);
3462
+ const aspects = this.calculateAspects(planetsWithCoords, aspectSettings);
2793
3463
  this.renderedAspects = aspects; // Store the calculated aspects
2794
3464
 
2795
3465
  console.log(`ClientSideAspectRenderer: Rendering ${aspects.length} aspects.`);
2796
3466
 
2797
3467
  // Get enabled aspect types from config
2798
- const aspectSettings = this.config.aspectSettings || {};
2799
- const aspectTypesConfig = aspectSettings.types || {};
3468
+ const settings = aspectSettings || this.config.aspectSettings || {};
3469
+ const aspectTypesConfig = settings.types || {};
2800
3470
 
2801
3471
  // Map planets by name for quick coordinate lookup
2802
3472
  const planetCoords = {};
@@ -2858,6 +3528,155 @@
2858
3528
 
2859
3529
  return renderedElements;
2860
3530
  }
3531
+
3532
+ /**
3533
+ * Renders cross-aspect lines (synastry) between two planet sets.
3534
+ * @param {Element} parentGroup - The parent SVG group for aspect lines.
3535
+ * @param {Array} primaryPlanets - Array of primary planet objects with coordinates
3536
+ * @param {Array} secondaryPlanets - Array of secondary planet objects with coordinates
3537
+ * @param {Object} aspectSettings - Aspect settings to use
3538
+ * @returns {Array<Element>} Array containing the created line elements.
3539
+ */
3540
+ renderCrossAspects(parentGroup, primaryPlanets, secondaryPlanets, aspectSettings = null) {
3541
+ if (!parentGroup) {
3542
+ console.error("ClientSideAspectRenderer.renderCrossAspects: parentGroup is null or undefined.");
3543
+ return [];
3544
+ }
3545
+ this.clearGroup(parentGroup);
3546
+ const renderedElements = [];
3547
+
3548
+ if (!primaryPlanets || primaryPlanets.length < 1 || !secondaryPlanets || secondaryPlanets.length < 1) {
3549
+ console.warn("ClientSideAspectRenderer: Not enough planet data to render cross-aspects.");
3550
+ return [];
3551
+ }
3552
+
3553
+ // Calculate cross-aspects
3554
+ const aspects = this.calculateCrossAspects(primaryPlanets, secondaryPlanets, aspectSettings);
3555
+
3556
+ console.log(`ClientSideAspectRenderer: Rendering ${aspects.length} cross-aspects.`);
3557
+
3558
+ // Get enabled aspect types from config
3559
+ const settings = aspectSettings || this.config.synastryAspectSettings || {};
3560
+ const aspectTypesConfig = settings.types || {};
3561
+
3562
+ // Map planets by name for coordinate lookup - keep separate to avoid overwriting
3563
+ const primaryCoords = {};
3564
+ primaryPlanets.forEach(p => {
3565
+ primaryCoords[p.name] = { x: p.x, y: p.y };
3566
+ });
3567
+
3568
+ const secondaryCoords = {};
3569
+ secondaryPlanets.forEach(p => {
3570
+ secondaryCoords[p.name] = { x: p.x, y: p.y };
3571
+ });
3572
+
3573
+ aspects.forEach(aspect => {
3574
+ // For cross-aspects:
3575
+ // - aspect.planet1 is from primary (outer circle)
3576
+ // - aspect.planet2 is from secondary (inner circle)
3577
+ // We need to draw line from secondary planet to PRIMARY's projection on inner circle
3578
+
3579
+ // Get the primary planet to calculate its projection on inner circle
3580
+ const primaryPlanet = aspect.p1; // This has the position (degrees)
3581
+ aspect.p2;
3582
+
3583
+ // Calculate projection of primary planet onto inner circle radius
3584
+ // Using the same angle but inner circle radius
3585
+ const innerRadius = this.config.radius.innermost || 90;
3586
+ const angle = primaryPlanet.position;
3587
+ const radians = (angle - 90) * (Math.PI / 180); // Adjust for SVG coordinate system
3588
+
3589
+ const projectionX = this.centerX + innerRadius * Math.cos(radians);
3590
+ const projectionY = this.centerY + innerRadius * Math.sin(radians);
3591
+
3592
+ // coords1 is the projection of primary planet on inner circle
3593
+ const coords1 = { x: projectionX, y: projectionY };
3594
+ // coords2 is the actual secondary planet position
3595
+ const coords2 = secondaryCoords[aspect.planet2];
3596
+
3597
+ const aspectDef = aspectTypesConfig[aspect.type];
3598
+ const isEnabled = aspectDef ? (aspectDef.enabled !== false) : true;
3599
+ const lineStyle = aspectDef ? aspectDef.lineStyle : 'solid';
3600
+
3601
+ if (!isEnabled || lineStyle === 'none') {
3602
+ return;
3603
+ }
3604
+
3605
+ if (!coords1 || !coords2) {
3606
+ console.warn(`ClientSideAspectRenderer: Could not find coordinates for cross-aspect: ${aspect.planet1} ${aspect.type} ${aspect.planet2}`);
3607
+ return;
3608
+ }
3609
+
3610
+ const p1SafeName = (aspect.planet1 || '').toLowerCase().replace(/[^a-z0-9]/g, '-');
3611
+ const p2SafeName = (aspect.planet2 || '').toLowerCase().replace(/[^a-z0-9]/g, '-');
3612
+
3613
+ let strokeDasharray = 'none';
3614
+ if (lineStyle === 'dashed') {
3615
+ strokeDasharray = '5, 5';
3616
+ } else if (lineStyle === 'dotted') {
3617
+ strokeDasharray = '1, 3';
3618
+ }
3619
+
3620
+ const line = this.svgUtils.createSVGElement("line", {
3621
+ x1: coords1.x,
3622
+ y1: coords1.y,
3623
+ x2: coords2.x,
3624
+ y2: coords2.y,
3625
+ class: `aspect-element aspect-line aspect-${aspect.type} aspect-cross aspect-planet-${p1SafeName} aspect-planet-${p2SafeName}`,
3626
+ stroke: aspect.color || '#888888',
3627
+ 'stroke-dasharray': strokeDasharray
3628
+ });
3629
+
3630
+ const tooltipText = `${this.astrologyUtils.capitalizeFirstLetter(aspect.planet1)} ${aspect.type} ${this.astrologyUtils.capitalizeFirstLetter(aspect.planet2)} (${aspect.angleDiff.toFixed(1)}°, orb ${aspect.orb.toFixed(1)}°) [Synastry]`;
3631
+ this.svgUtils.addTooltip(line, tooltipText);
3632
+
3633
+ parentGroup.appendChild(line);
3634
+ renderedElements.push(line);
3635
+
3636
+ this._addAspectIcon(parentGroup, aspect, coords1, coords2, tooltipText);
3637
+ });
3638
+
3639
+ // Render projection dots (hollow circles) for primary planets on inner circle
3640
+ this._renderProjectionDots(parentGroup, primaryPlanets);
3641
+
3642
+ return renderedElements;
3643
+ }
3644
+
3645
+ /**
3646
+ * Renders projection dots (hollow circles) for primary planets on the inner circle
3647
+ * @private
3648
+ * @param {Element} parentGroup - The SVG group for aspect lines
3649
+ * @param {Array} primaryPlanets - Array of primary planet objects
3650
+ */
3651
+ _renderProjectionDots(parentGroup, primaryPlanets) {
3652
+ const innerRadius = this.config.radius.innermost || 90;
3653
+ const dotRadius = 3; // Size of the hollow circle
3654
+
3655
+ primaryPlanets.forEach(planet => {
3656
+ const angle = planet.position;
3657
+ const radians = (angle - 90) * (Math.PI / 180); // Adjust for SVG coordinate system
3658
+
3659
+ const x = this.centerX + innerRadius * Math.cos(radians);
3660
+ const y = this.centerY + innerRadius * Math.sin(radians);
3661
+
3662
+ // Create hollow circle (stroke only, no fill)
3663
+ const circle = this.svgUtils.createSVGElement("circle", {
3664
+ cx: x,
3665
+ cy: y,
3666
+ r: dotRadius,
3667
+ class: `projection-dot projection-${planet.name}`,
3668
+ fill: 'none',
3669
+ stroke: planet.color || '#666666',
3670
+ 'stroke-width': '1.5'
3671
+ });
3672
+
3673
+ // Add tooltip
3674
+ const tooltipText = `${this.astrologyUtils.capitalizeFirstLetter(planet.name)} projection (${angle.toFixed(1)}°)`;
3675
+ this.svgUtils.addTooltip(circle, tooltipText);
3676
+
3677
+ parentGroup.appendChild(circle);
3678
+ });
3679
+ }
2861
3680
 
2862
3681
  /**
2863
3682
  * Overrides the BaseRenderer clearGroup method to ensure custom cleanup.
@@ -3724,9 +4543,13 @@
3724
4543
  * Constructor
3725
4544
  * @param {Object} options - Configuration options
3726
4545
  * @param {string|Element} options.container - Container element or selector
3727
- * @param {Object} options.planets - Planet positions data
4546
+ * @param {Object} options.planets - Primary planet positions data (outer circle)
4547
+ * @param {Object} options.secondaryPlanets - Secondary planet positions data (inner circle, optional)
3728
4548
  * @param {Array} options.houses - House cusps data (optional)
3729
- * @param {Object} options.aspectSettings - Aspect calculation settings (optional)
4549
+ * @param {Object} options.aspectSettings - Aspect calculation settings (optional, legacy)
4550
+ * @param {Object} options.primaryAspectSettings - Primary aspect settings (optional)
4551
+ * @param {Object} options.secondaryAspectSettings - Secondary aspect settings (optional)
4552
+ * @param {Object} options.synastryAspectSettings - Synastry aspect settings (optional)
3730
4553
  * @param {Object} options.config - Additional configuration (optional)
3731
4554
  */
3732
4555
  constructor(options) {
@@ -3746,15 +4569,31 @@
3746
4569
  // Initialize configuration
3747
4570
  this.config = new ChartConfig(options.config || {});
3748
4571
 
3749
- // Set data
4572
+ // Set data - primary planets (outer circle)
3750
4573
  this.planets = options.planets || {};
4574
+
4575
+ // Set secondary planets (inner circle) - independent data
4576
+ // If not provided, initialize as empty object (user must explicitly set)
4577
+ this.secondaryPlanets = options.secondaryPlanets || {};
4578
+
3751
4579
  this.houses = options.houses || [];
3752
4580
 
3753
- // Override aspect settings if provided
4581
+ // Override aspect settings if provided (legacy support)
3754
4582
  if (options.aspectSettings) {
3755
4583
  this.config.updateAspectSettings(options.aspectSettings);
3756
4584
  }
3757
4585
 
4586
+ // Override specific aspect settings if provided
4587
+ if (options.primaryAspectSettings) {
4588
+ this.config.updatePrimaryAspectSettings(options.primaryAspectSettings);
4589
+ }
4590
+ if (options.secondaryAspectSettings) {
4591
+ this.config.updateSecondaryAspectSettings(options.secondaryAspectSettings);
4592
+ }
4593
+ if (options.synastryAspectSettings) {
4594
+ this.config.updateSynastryAspectSettings(options.synastryAspectSettings);
4595
+ }
4596
+
3758
4597
  // Initialize services
3759
4598
  ServiceRegistry.initializeServices();
3760
4599
 
@@ -3862,30 +4701,74 @@
3862
4701
  }
3863
4702
 
3864
4703
  // Render planets using the consolidated approach
3865
- let planetsWithCoords = [];
4704
+ let primaryPlanetsWithCoords = [];
4705
+ let secondaryPlanetsWithCoords = [];
3866
4706
  if (this.config.planetSettings.enabled) {
3867
4707
  // Get the enabled states for primary and secondary planets
3868
4708
  const primaryEnabled = this.config.planetSettings.primaryEnabled !== false;
3869
4709
  const secondaryEnabled = this.config.planetSettings.secondaryEnabled !== false;
3870
4710
 
3871
- // Use the consolidated renderAllPlanetTypes method
3872
- const renderedPlanets = this.renderers.planet.renderAllPlanetTypes({
3873
- svgManager: this.svgManager,
3874
- planetsData: this.planets,
3875
- config: this.config,
3876
- primaryEnabled: primaryEnabled,
3877
- secondaryEnabled: secondaryEnabled
3878
- });
4711
+ // Render primary planets (outer circle)
4712
+ if (primaryEnabled && Object.keys(this.planets).length > 0) {
4713
+ const primaryGroup = this.svgManager.getGroup('primaryPlanets');
4714
+ const primaryArray = Object.entries(this.planets)
4715
+ .filter(([name, data]) => this.config.planetSettings.visible?.[name] !== false)
4716
+ .map(([name, data]) => ({
4717
+ name: name,
4718
+ position: data.lon,
4719
+ color: data.color || '#000000'
4720
+ }));
4721
+ primaryPlanetsWithCoords = this.renderers.planet.primaryRenderer.render(primaryGroup, primaryArray, 0, {
4722
+ config: this.config
4723
+ });
4724
+ }
3879
4725
 
3880
- // For aspect rendering, use primary planets by default
3881
- planetsWithCoords = renderedPlanets.primary;
4726
+ // Render secondary planets (inner circle) using SEPARATE data
4727
+ if (secondaryEnabled && Object.keys(this.secondaryPlanets).length > 0) {
4728
+ const secondaryGroup = this.svgManager.getGroup('secondaryPlanets');
4729
+ const secondaryArray = Object.entries(this.secondaryPlanets)
4730
+ .filter(([name, data]) => this.config.planetSettings.visible?.[name] !== false)
4731
+ .map(([name, data]) => ({
4732
+ name: name,
4733
+ position: data.lon,
4734
+ color: data.color || '#000000'
4735
+ }));
4736
+ secondaryPlanetsWithCoords = this.renderers.planet.secondaryRenderer.render(secondaryGroup, secondaryArray, 0, {
4737
+ config: this.config
4738
+ });
4739
+ }
3882
4740
 
3883
- console.log(`NocturnaWheel: Rendered ${renderedPlanets.primary.length} primary planets and ${renderedPlanets.secondary.length} secondary planets`);
4741
+ console.log(`NocturnaWheel: Rendered ${primaryPlanetsWithCoords.length} primary planets and ${secondaryPlanetsWithCoords.length} secondary planets`);
3884
4742
  }
3885
4743
 
3886
- // Render aspects if enabled, passing planets with coordinates
3887
- if (this.config.aspectSettings.enabled) {
3888
- this.renderers.aspect.render(this.svgManager.getGroup('aspects'), planetsWithCoords);
4744
+ // Render three independent aspect types
4745
+
4746
+ // 1. Primary aspects (outer circle to outer circle)
4747
+ if (this.config.primaryAspectSettings.enabled && primaryPlanetsWithCoords.length >= 2) {
4748
+ const primaryAspectsGroup = this.svgManager.getGroup('primaryAspects');
4749
+ this.renderers.aspect.render(primaryAspectsGroup, primaryPlanetsWithCoords, this.config.primaryAspectSettings);
4750
+ console.log("NocturnaWheel: Rendered primary aspects");
4751
+ }
4752
+
4753
+ // 2. Secondary aspects (inner circle to inner circle)
4754
+ if (this.config.secondaryAspectSettings.enabled && secondaryPlanetsWithCoords.length >= 2) {
4755
+ const secondaryAspectsGroup = this.svgManager.getGroup('secondaryAspects');
4756
+ this.renderers.aspect.render(secondaryAspectsGroup, secondaryPlanetsWithCoords, this.config.secondaryAspectSettings);
4757
+ console.log("NocturnaWheel: Rendered secondary aspects");
4758
+ }
4759
+
4760
+ // 3. Synastry aspects (outer circle to inner circle)
4761
+ if (this.config.synastryAspectSettings.enabled &&
4762
+ primaryPlanetsWithCoords.length >= 1 &&
4763
+ secondaryPlanetsWithCoords.length >= 1) {
4764
+ const synastryAspectsGroup = this.svgManager.getGroup('synastryAspects');
4765
+ this.renderers.aspect.renderCrossAspects(
4766
+ synastryAspectsGroup,
4767
+ primaryPlanetsWithCoords,
4768
+ secondaryPlanetsWithCoords,
4769
+ this.config.synastryAspectSettings
4770
+ );
4771
+ console.log("NocturnaWheel: Rendered synastry aspects");
3889
4772
  }
3890
4773
 
3891
4774
  console.log("NocturnaWheel: Chart rendered");
@@ -3943,15 +4826,49 @@
3943
4826
  }
3944
4827
 
3945
4828
  /**
3946
- * Toggles the visibility of aspects
4829
+ * Toggles the visibility of aspects (legacy - toggles all)
3947
4830
  * @param {boolean} visible - Visibility state
3948
4831
  * @returns {NocturnaWheel} - Instance for chaining
4832
+ * @deprecated Use togglePrimaryAspects, toggleSecondaryAspects, or toggleSynastryAspects instead
3949
4833
  */
3950
4834
  toggleAspects(visible) {
3951
4835
  this.config.toggleAspectsVisibility(visible);
3952
4836
  this.render();
3953
4837
  return this;
3954
4838
  }
4839
+
4840
+ /**
4841
+ * Toggles the visibility of primary aspects (outer circle)
4842
+ * @param {boolean} visible - Visibility state
4843
+ * @returns {NocturnaWheel} - Instance for chaining
4844
+ */
4845
+ togglePrimaryAspects(visible) {
4846
+ this.config.togglePrimaryAspectsVisibility(visible);
4847
+ this.render();
4848
+ return this;
4849
+ }
4850
+
4851
+ /**
4852
+ * Toggles the visibility of secondary aspects (inner circle)
4853
+ * @param {boolean} visible - Visibility state
4854
+ * @returns {NocturnaWheel} - Instance for chaining
4855
+ */
4856
+ toggleSecondaryAspects(visible) {
4857
+ this.config.toggleSecondaryAspectsVisibility(visible);
4858
+ this.render();
4859
+ return this;
4860
+ }
4861
+
4862
+ /**
4863
+ * Toggles the visibility of synastry aspects (cross-circle)
4864
+ * @param {boolean} visible - Visibility state
4865
+ * @returns {NocturnaWheel} - Instance for chaining
4866
+ */
4867
+ toggleSynastryAspects(visible) {
4868
+ this.config.toggleSynastryAspectsVisibility(visible);
4869
+ this.render();
4870
+ return this;
4871
+ }
3955
4872
 
3956
4873
  /**
3957
4874
  * Sets the house system rotation angle
@@ -3982,8 +4899,8 @@
3982
4899
  }
3983
4900
 
3984
4901
  /**
3985
- * Updates chart data (planets, houses)
3986
- * @param {Object} data - Object containing new data, e.g., { planets: {...}, houses: [...] }
4902
+ * Updates chart data (planets, secondaryPlanets, houses)
4903
+ * @param {Object} data - Object containing new data, e.g., { planets: {...}, secondaryPlanets: {...}, houses: [...] }
3987
4904
  * @returns {NocturnaWheel} - Instance for chaining
3988
4905
  */
3989
4906
  updateData(data) {
@@ -3997,6 +4914,15 @@
3997
4914
  console.warn("NocturnaWheel.updateData: Invalid planets data format. Expected object.");
3998
4915
  }
3999
4916
  }
4917
+ if (data.secondaryPlanets) {
4918
+ // Update internal secondary planets data
4919
+ if (typeof data.secondaryPlanets === 'object' && !Array.isArray(data.secondaryPlanets)) {
4920
+ this.secondaryPlanets = { ...this.secondaryPlanets, ...data.secondaryPlanets };
4921
+ console.log("NocturnaWheel: Updated secondary planets data.");
4922
+ } else {
4923
+ console.warn("NocturnaWheel.updateData: Invalid secondaryPlanets data format. Expected object.");
4924
+ }
4925
+ }
4000
4926
  if (data.houses) {
4001
4927
  // Update internal houses data
4002
4928
  if (Array.isArray(data.houses)) {
@@ -4243,9 +5169,13 @@
4243
5169
  * Constructor
4244
5170
  * @param {Object} options - Configuration options
4245
5171
  * @param {string|Element} options.container - Container element or selector
4246
- * @param {Object} options.planets - Planet positions data
5172
+ * @param {Object} options.planets - Primary planet positions data (outer circle)
5173
+ * @param {Object} options.secondaryPlanets - Secondary planet positions data (inner circle, optional)
4247
5174
  * @param {Array} options.houses - House cusps data (optional)
4248
- * @param {Object} options.aspectSettings - Aspect calculation settings (optional)
5175
+ * @param {Object} options.aspectSettings - Aspect calculation settings (optional, legacy)
5176
+ * @param {Object} options.primaryAspectSettings - Primary aspect settings (optional)
5177
+ * @param {Object} options.secondaryAspectSettings - Secondary aspect settings (optional)
5178
+ * @param {Object} options.synastryAspectSettings - Synastry aspect settings (optional)
4249
5179
  * @param {Object} options.config - Additional configuration (optional)
4250
5180
  * @param {Function} [chartFactory=null] - Factory function to create the chart instance
4251
5181
  * Function signature: (options) => ChartInstance
@@ -4362,6 +5292,18 @@
4362
5292
  return this._delegateAndRedraw('toggleAspects', visible);
4363
5293
  }
4364
5294
 
5295
+ togglePrimaryAspects(visible) {
5296
+ return this._delegateAndRedraw('togglePrimaryAspects', visible);
5297
+ }
5298
+
5299
+ toggleSecondaryAspects(visible) {
5300
+ return this._delegateAndRedraw('toggleSecondaryAspects', visible);
5301
+ }
5302
+
5303
+ toggleSynastryAspects(visible) {
5304
+ return this._delegateAndRedraw('toggleSynastryAspects', visible);
5305
+ }
5306
+
4365
5307
  /**
4366
5308
  * Toggles the visibility of primary planets (inner circle)
4367
5309
  * @param {boolean} visible - Visibility state