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