@eaprelsky/nocturna-wheel 0.2.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/assets/css/demo.css +466 -0
  4. package/dist/assets/css/nocturna-wheel.css +218 -0
  5. package/dist/assets/png/nocturna-logo.png +0 -0
  6. package/dist/assets/svg/zodiac/zodiac-aspect-conjunction.svg +3 -0
  7. package/dist/assets/svg/zodiac/zodiac-aspect-opposition.svg +3 -0
  8. package/dist/assets/svg/zodiac/zodiac-aspect-quincunx.svg +3 -0
  9. package/dist/assets/svg/zodiac/zodiac-aspect-semisextile.svg +3 -0
  10. package/dist/assets/svg/zodiac/zodiac-aspect-semisquare.svg +3 -0
  11. package/dist/assets/svg/zodiac/zodiac-aspect-sesquisquare.svg +3 -0
  12. package/dist/assets/svg/zodiac/zodiac-aspect-sextile.svg +3 -0
  13. package/dist/assets/svg/zodiac/zodiac-aspect-square.svg +3 -0
  14. package/dist/assets/svg/zodiac/zodiac-aspect-trine.svg +3 -0
  15. package/dist/assets/svg/zodiac/zodiac-planet-jupiter.svg +3 -0
  16. package/dist/assets/svg/zodiac/zodiac-planet-ketu.svg +3 -0
  17. package/dist/assets/svg/zodiac/zodiac-planet-lilith.svg +3 -0
  18. package/dist/assets/svg/zodiac/zodiac-planet-mars.svg +3 -0
  19. package/dist/assets/svg/zodiac/zodiac-planet-mercury.svg +3 -0
  20. package/dist/assets/svg/zodiac/zodiac-planet-moon.svg +17 -0
  21. package/dist/assets/svg/zodiac/zodiac-planet-neptune.svg +3 -0
  22. package/dist/assets/svg/zodiac/zodiac-planet-pluto.svg +3 -0
  23. package/dist/assets/svg/zodiac/zodiac-planet-rahu.svg +3 -0
  24. package/dist/assets/svg/zodiac/zodiac-planet-saturn.svg +3 -0
  25. package/dist/assets/svg/zodiac/zodiac-planet-selena.svg +3 -0
  26. package/dist/assets/svg/zodiac/zodiac-planet-sun.svg +3 -0
  27. package/dist/assets/svg/zodiac/zodiac-planet-uranus.svg +3 -0
  28. package/dist/assets/svg/zodiac/zodiac-planet-venus.svg +3 -0
  29. package/dist/assets/svg/zodiac/zodiac-sign-aquarius.svg +3 -0
  30. package/dist/assets/svg/zodiac/zodiac-sign-aries.svg +3 -0
  31. package/dist/assets/svg/zodiac/zodiac-sign-cancer.svg +3 -0
  32. package/dist/assets/svg/zodiac/zodiac-sign-capricorn.svg +3 -0
  33. package/dist/assets/svg/zodiac/zodiac-sign-gemini.svg +3 -0
  34. package/dist/assets/svg/zodiac/zodiac-sign-leo.svg +3 -0
  35. package/dist/assets/svg/zodiac/zodiac-sign-libra.svg +3 -0
  36. package/dist/assets/svg/zodiac/zodiac-sign-pisces.svg +3 -0
  37. package/dist/assets/svg/zodiac/zodiac-sign-sagittarius.svg +3 -0
  38. package/dist/assets/svg/zodiac/zodiac-sign-scorpio.svg +3 -0
  39. package/dist/assets/svg/zodiac/zodiac-sign-taurus.svg +3 -0
  40. package/dist/assets/svg/zodiac/zodiac-sign-virgo.svg +3 -0
  41. package/dist/nocturna-wheel.bundle.js +4598 -0
  42. package/dist/nocturna-wheel.bundle.js.map +1 -0
  43. package/dist/nocturna-wheel.es.js +4580 -0
  44. package/dist/nocturna-wheel.es.js.map +1 -0
  45. package/dist/nocturna-wheel.min.js +2 -0
  46. package/dist/nocturna-wheel.min.js.map +1 -0
  47. package/dist/nocturna-wheel.umd.js +4598 -0
  48. package/dist/nocturna-wheel.umd.js.map +1 -0
  49. package/package.json +85 -0
@@ -0,0 +1,4580 @@
1
+ /**
2
+ * ServiceRegistry.js
3
+ * A simple service locator/registry with all dependencies inlined.
4
+ */
5
+
6
+ // Self-contained implementation
7
+ class ServiceRegistry {
8
+ // Private map to store service instances
9
+ static #instances = new Map();
10
+
11
+ /**
12
+ * Registers a service instance with the registry
13
+ * @param {string} key - Service identifier
14
+ * @param {Object} instance - Service instance
15
+ */
16
+ static register(key, instance) {
17
+ this.#instances.set(key, instance);
18
+ }
19
+
20
+ /**
21
+ * Retrieves a service instance from the registry
22
+ * @param {string} key - Service identifier
23
+ * @returns {Object|undefined} The service instance, or undefined if not found
24
+ */
25
+ static get(key) {
26
+ return this.#instances.get(key);
27
+ }
28
+
29
+ /**
30
+ * Checks if a service is registered
31
+ * @param {string} key - Service identifier
32
+ * @returns {boolean} True if the service is registered
33
+ */
34
+ static has(key) {
35
+ return this.#instances.has(key);
36
+ }
37
+
38
+ /**
39
+ * Clears all registered services
40
+ * Useful for testing or reinitialization
41
+ */
42
+ static clear() {
43
+ this.#instances.clear();
44
+ }
45
+
46
+ /**
47
+ * Gets or creates a basic SvgUtils-compatible instance
48
+ * @returns {Object} An object with SVG utility methods
49
+ */
50
+ static getSvgUtils() {
51
+ if (!this.has('svgUtils')) {
52
+ // Create a simple SVG utilities object
53
+ const svgUtils = {
54
+ svgNS: "http://www.w3.org/2000/svg",
55
+
56
+ createSVGElement(tagName, attributes = {}) {
57
+ const element = document.createElementNS(this.svgNS, tagName);
58
+ for (const [key, value] of Object.entries(attributes)) {
59
+ element.setAttribute(key, value);
60
+ }
61
+ return element;
62
+ },
63
+
64
+ addTooltip(element, text) {
65
+ const title = document.createElementNS(this.svgNS, "title");
66
+ title.textContent = text;
67
+ element.appendChild(title);
68
+ return element;
69
+ },
70
+
71
+ pointOnCircle(centerX, centerY, radius, angle) {
72
+ const radians = (angle - 90) * (Math.PI / 180);
73
+ return {
74
+ x: centerX + radius * Math.cos(radians),
75
+ y: centerY + radius * Math.sin(radians)
76
+ };
77
+ }
78
+ };
79
+
80
+ this.register('svgUtils', svgUtils);
81
+ }
82
+ return this.get('svgUtils');
83
+ }
84
+
85
+ /**
86
+ * Gets or creates an IconProvider instance
87
+ * @param {string} basePath - Optional base path for SVG assets
88
+ * @returns {Object} The IconProvider instance
89
+ */
90
+ static getIconProvider(basePath = './assets/svg/zodiac/') {
91
+ if (!this.has('iconProvider')) {
92
+ // Create a simple icon provider
93
+ const iconProvider = {
94
+ basePath: basePath,
95
+
96
+ getPlanetIconPath(planetName) {
97
+ return `${this.basePath}zodiac-planet-${planetName.toLowerCase()}.svg`;
98
+ },
99
+
100
+ getZodiacIconPath(signName) {
101
+ return `${this.basePath}zodiac-sign-${signName.toLowerCase()}.svg`;
102
+ },
103
+
104
+ getAspectIconPath(aspectType) {
105
+ return `${this.basePath}zodiac-aspect-${aspectType.toLowerCase()}.svg`;
106
+ },
107
+
108
+ createTextFallback(svgUtils, options, text) {
109
+ const { x, y, size = '16px', color = '#000000', className = 'icon-fallback' } = options;
110
+
111
+ const textElement = svgUtils.createSVGElement("text", {
112
+ x: x,
113
+ y: y,
114
+ 'text-anchor': 'middle',
115
+ 'dominant-baseline': 'middle',
116
+ 'font-size': size,
117
+ 'class': className,
118
+ 'fill': color
119
+ });
120
+
121
+ textElement.textContent = text;
122
+ return textElement;
123
+ }
124
+ };
125
+
126
+ this.register('iconProvider', iconProvider);
127
+ }
128
+ return this.get('iconProvider');
129
+ }
130
+
131
+ /**
132
+ * Initializes all core services at once
133
+ * @param {Object} options - Initialization options
134
+ */
135
+ static initializeServices(options = {}) {
136
+ // Initialize SvgUtils
137
+ this.getSvgUtils();
138
+
139
+ // Initialize IconProvider with the assets base path
140
+ this.getIconProvider(options.assetBasePath || './assets/svg/zodiac/');
141
+
142
+ console.log("ServiceRegistry: Core services initialized");
143
+ }
144
+ }
145
+
146
+ /**
147
+ * ChartConfig.js
148
+ * Configuration class for the natal chart rendering.
149
+ */
150
+
151
+ class ChartConfig {
152
+ /**
153
+ * Creates a new configuration with default settings
154
+ * @param {Object} customConfig - Custom configuration to merge with defaults
155
+ */
156
+ constructor(customConfig = {}) {
157
+ // Astronomical data - pure positional data without styling
158
+ this.astronomicalData = {
159
+ ascendant: 0, // Ascendant longitude in degrees
160
+ mc: 90, // Midheaven longitude in degrees
161
+ latitude: 51.5, // Default latitude (London)
162
+ houseSystem: "Placidus", // Default house system
163
+ planets: {
164
+ // Default planet positions
165
+ sun: 0,
166
+ moon: 0,
167
+ mercury: 0,
168
+ venus: 0,
169
+ mars: 0,
170
+ jupiter: 0,
171
+ saturn: 0,
172
+ uranus: 0,
173
+ neptune: 0,
174
+ pluto: 0
175
+ }
176
+ };
177
+
178
+ // Aspect settings
179
+ this.aspectSettings = {
180
+ enabled: true,
181
+ orb: 6, // Default orb for aspects
182
+ 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
189
+ }
190
+ };
191
+
192
+ // Planet settings
193
+ this.planetSettings = {
194
+ enabled: true,
195
+ primaryEnabled: true, // Toggle for primary (inner circle) planets
196
+ secondaryEnabled: true, // Toggle for secondary (innermost circle) planets
197
+ dotSize: 3, // Size of the position dot
198
+ iconSize: 24, // Size of the planet icon
199
+ orbs: {
200
+ // Default orbs for each planet
201
+ sun: 8,
202
+ moon: 8,
203
+ mercury: 6,
204
+ venus: 6,
205
+ mars: 6,
206
+ jupiter: 6,
207
+ saturn: 6,
208
+ uranus: 4,
209
+ neptune: 4,
210
+ pluto: 4
211
+ },
212
+ colors: {
213
+ // Default colors for each planet
214
+ sun: "#ff9900",
215
+ moon: "#aaaaaa",
216
+ mercury: "#3399cc",
217
+ venus: "#cc66cc",
218
+ mars: "#cc3333",
219
+ jupiter: "#9966cc",
220
+ saturn: "#336633",
221
+ uranus: "#33cccc",
222
+ neptune: "#3366ff",
223
+ pluto: "#663366"
224
+ },
225
+ visible: {
226
+ // Default visibility for each planet
227
+ sun: true,
228
+ moon: true,
229
+ mercury: true,
230
+ venus: true,
231
+ mars: true,
232
+ jupiter: true,
233
+ saturn: true,
234
+ uranus: true,
235
+ neptune: true,
236
+ pluto: true
237
+ }
238
+ };
239
+
240
+ // House settings - only UI related settings, no calculations
241
+ this.houseSettings = {
242
+ enabled: true,
243
+ lineColor: "#666666",
244
+ textColor: "#333333",
245
+ fontSize: 10,
246
+ rotationAngle: 0 // Custom rotation angle for house system
247
+ };
248
+
249
+ // Zodiac settings
250
+ this.zodiacSettings = {
251
+ enabled: true,
252
+ colors: {
253
+ aries: "#ff6666",
254
+ taurus: "#66cc66",
255
+ gemini: "#ffcc66",
256
+ cancer: "#6699cc",
257
+ leo: "#ff9900",
258
+ virgo: "#996633",
259
+ libra: "#6699ff",
260
+ scorpio: "#cc3366",
261
+ sagittarius: "#cc66ff",
262
+ capricorn: "#339966",
263
+ aquarius: "#3399ff",
264
+ pisces: "#9966cc"
265
+ },
266
+ fontSize: 10
267
+ };
268
+
269
+ // Radii for different chart layers
270
+ this.radius = {
271
+ innermost: 90, // Innermost circle (for dual charts, transits, synastry)
272
+ zodiacInner: 120, // Inner circle (aspect container)
273
+ zodiacMiddle: 150, // Middle circle (house boundaries)
274
+ zodiacOuter: 180, // Outer circle (zodiac ring)
275
+ planet: 105, // Default planet placement radius
276
+ aspectInner: 20, // Center space for aspects
277
+ aspectOuter: 120, // Outer boundary for aspects
278
+ houseNumberRadius: 210 // Radius for house numbers
279
+ };
280
+
281
+ // SVG settings
282
+ this.svg = {
283
+ width: 460,
284
+ height: 460,
285
+ viewBox: "0 0 460 460",
286
+ center: { x: 230, y: 230 }
287
+ };
288
+
289
+ // Assets settings
290
+ this.assets = {
291
+ basePath: "./assets/",
292
+ zodiacIconPath: "svg/zodiac/",
293
+ planetIconPath: "svg/zodiac/"
294
+ };
295
+
296
+ // Theme settings
297
+ this.theme = {
298
+ backgroundColor: "transparent",
299
+ textColor: "#333333",
300
+ lineColor: "#666666",
301
+ lightLineColor: "#cccccc",
302
+ fontFamily: "'Arial', sans-serif"
303
+ };
304
+
305
+ // House cusps cache - will be populated by HouseCalculator
306
+ this.houseCusps = [];
307
+
308
+ // Merge custom config with defaults (deep merge)
309
+ this.mergeConfig(customConfig);
310
+
311
+ // Initialize house cusps if we have enough data
312
+ this._initializeHouseCusps();
313
+ }
314
+
315
+ /**
316
+ * Merges custom configuration with the default configuration
317
+ * @param {Object} customConfig - Custom configuration object
318
+ */
319
+ mergeConfig(customConfig) {
320
+ // Helper function for deep merge
321
+ const deepMerge = (target, source) => {
322
+ if (typeof source !== 'object' || source === null) {
323
+ return source;
324
+ }
325
+
326
+ for (const key in source) {
327
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
328
+ if (typeof source[key] === 'object' && source[key] !== null &&
329
+ typeof target[key] === 'object' && target[key] !== null) {
330
+ // If both are objects, recurse
331
+ target[key] = deepMerge(target[key], source[key]);
332
+ } else {
333
+ // Otherwise just copy
334
+ target[key] = source[key];
335
+ }
336
+ }
337
+ }
338
+
339
+ return target;
340
+ };
341
+
342
+ // Apply deep merge to all top-level properties
343
+ for (const key in customConfig) {
344
+ if (Object.prototype.hasOwnProperty.call(customConfig, key) &&
345
+ Object.prototype.hasOwnProperty.call(this, key)) {
346
+ this[key] = deepMerge(this[key], customConfig[key]);
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Initializes house cusps using the HouseCalculator
353
+ * @private
354
+ */
355
+ _initializeHouseCusps() {
356
+ // Only calculate if we have the necessary data
357
+ if (typeof this.astronomicalData.ascendant === 'number' &&
358
+ typeof this.astronomicalData.mc === 'number') {
359
+
360
+ try {
361
+ // Create calculator instance
362
+ const houseCalculator = new HouseCalculator();
363
+
364
+ // Calculate house cusps using the current house system
365
+ this.houseCusps = houseCalculator.calculateHouseCusps(
366
+ this.astronomicalData.ascendant,
367
+ this.astronomicalData.houseSystem,
368
+ {
369
+ latitude: this.astronomicalData.latitude,
370
+ mc: this.astronomicalData.mc
371
+ }
372
+ );
373
+ } catch (error) {
374
+ console.error("Failed to calculate house cusps:", error);
375
+ // Set empty cusps array if calculation fails
376
+ this.houseCusps = [];
377
+ }
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Gets settings for a specific planet
383
+ * @param {string} planetName - Name of the planet
384
+ * @returns {Object} - Planet settings
385
+ */
386
+ getPlanetSettings(planetName) {
387
+ const planetLower = planetName.toLowerCase();
388
+ return {
389
+ color: this.planetSettings.colors[planetLower] || "#000000",
390
+ size: this.planetSettings.size,
391
+ visible: this.planetSettings.visible[planetLower] !== false
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Gets settings for a specific aspect
397
+ * @param {string} aspectType - Type of aspect
398
+ * @returns {Object} - Aspect settings
399
+ */
400
+ getAspectSettings(aspectType) {
401
+ const aspectLower = aspectType.toLowerCase();
402
+ return this.aspectSettings.types[aspectLower] ||
403
+ { angle: 0, orb: this.aspectSettings.orb, color: "#999999", enabled: true };
404
+ }
405
+
406
+ /**
407
+ * Gets settings for a specific zodiac sign
408
+ * @param {string} signName - Name of the zodiac sign
409
+ * @returns {Object} - Zodiac sign settings
410
+ */
411
+ getZodiacSettings(signName) {
412
+ const signLower = signName.toLowerCase();
413
+ return {
414
+ color: this.zodiacSettings.colors[signLower] || "#666666",
415
+ fontSize: this.zodiacSettings.fontSize
416
+ };
417
+ }
418
+
419
+ /**
420
+ * Updates aspect settings
421
+ * @param {Object} settings - New aspect settings
422
+ */
423
+ updateAspectSettings(settings) {
424
+ this.aspectSettings = { ...this.aspectSettings, ...settings };
425
+ }
426
+
427
+ /**
428
+ * Updates planet settings
429
+ * @param {Object} settings - New planet settings
430
+ */
431
+ updatePlanetSettings(settings) {
432
+ this.planetSettings = { ...this.planetSettings, ...settings };
433
+ }
434
+
435
+ /**
436
+ * Updates house settings (visual settings only)
437
+ * @param {Object} settings - New house settings
438
+ */
439
+ updateHouseSettings(settings) {
440
+ this.houseSettings = { ...this.houseSettings, ...settings };
441
+ }
442
+
443
+ /**
444
+ * Updates zodiac settings
445
+ * @param {Object} settings - New zodiac settings
446
+ */
447
+ updateZodiacSettings(settings) {
448
+ this.zodiacSettings = { ...this.zodiacSettings, ...settings };
449
+ }
450
+
451
+ /**
452
+ * Sets radius for a specific layer
453
+ * @param {string} layerName - Layer name
454
+ * @param {number} value - Radius value
455
+ */
456
+ setRadius(layerName, value) {
457
+ if (Object.prototype.hasOwnProperty.call(this.radius, layerName)) {
458
+ this.radius[layerName] = value;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Toggles the visibility of a planet
464
+ * @param {string} planetName - Name of the planet
465
+ * @param {boolean} visible - Whether the planet should be visible
466
+ */
467
+ togglePlanetVisibility(planetName, visible) {
468
+ if (this.planetSettings && this.planetSettings.visible) {
469
+ this.planetSettings.visible[planetName] = visible;
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Toggles the visibility of houses
475
+ * @param {boolean} visible - Whether houses should be visible
476
+ */
477
+ toggleHousesVisibility(visible) {
478
+ this.houseSettings.enabled = visible;
479
+ }
480
+
481
+ /**
482
+ * Toggles the visibility of aspects
483
+ * @param {boolean} visible - Whether aspects should be visible
484
+ */
485
+ toggleAspectsVisibility(visible) {
486
+ this.aspectSettings.enabled = visible;
487
+ }
488
+
489
+ /**
490
+ * Toggles the visibility of primary planets (inner circle)
491
+ * @param {boolean} visible - Whether primary planets should be visible
492
+ */
493
+ togglePrimaryPlanetsVisibility(visible) {
494
+ this.planetSettings.primaryEnabled = visible;
495
+ }
496
+
497
+ /**
498
+ * Toggles the visibility of secondary planets (innermost circle)
499
+ * @param {boolean} visible - Whether secondary planets should be visible
500
+ */
501
+ toggleSecondaryPlanetsVisibility(visible) {
502
+ this.planetSettings.secondaryEnabled = visible;
503
+ }
504
+
505
+ /**
506
+ * Sets the current house system and recalculates house cusps
507
+ * @param {string} systemName - Name of the house system to use
508
+ * @returns {boolean} - Success status
509
+ */
510
+ setHouseSystem(systemName) {
511
+ // Update the house system name
512
+ this.astronomicalData.houseSystem = systemName;
513
+
514
+ // Recalculate house cusps with new system
515
+ this._initializeHouseCusps();
516
+
517
+ return true;
518
+ }
519
+
520
+ /**
521
+ * Gets the current house cusps
522
+ * @returns {Array} - Array of house cusps
523
+ */
524
+ getHouseCusps() {
525
+ // If we don't have house cusps data yet, calculate it
526
+ if (!this.houseCusps || this.houseCusps.length === 0) {
527
+ this._initializeHouseCusps();
528
+ }
529
+
530
+ // Ensure we're returning the correct format - convert to legacy format if needed
531
+ if (this.houseCusps.length > 0 && typeof this.houseCusps[0] === 'number') {
532
+ return this.houseCusps.map(longitude => ({ lon: longitude }));
533
+ }
534
+
535
+ return this.houseCusps;
536
+ }
537
+
538
+ /**
539
+ * Sets the Ascendant position and recalculates house cusps
540
+ * @param {number} ascendant - Ascendant longitude in degrees
541
+ * @returns {boolean} - Success status
542
+ */
543
+ setAscendant(ascendant) {
544
+ if (typeof ascendant !== 'number' || ascendant < 0 || ascendant >= 360) {
545
+ return false;
546
+ }
547
+
548
+ this.astronomicalData.ascendant = ascendant;
549
+ this._initializeHouseCusps();
550
+ return true;
551
+ }
552
+
553
+ /**
554
+ * Sets the Midheaven position and recalculates house cusps
555
+ * @param {number} mc - Midheaven longitude in degrees
556
+ * @returns {boolean} - Success status
557
+ */
558
+ setMidheaven(mc) {
559
+ if (typeof mc !== 'number' || mc < 0 || mc >= 360) {
560
+ return false;
561
+ }
562
+
563
+ this.astronomicalData.mc = mc;
564
+ this._initializeHouseCusps();
565
+ return true;
566
+ }
567
+
568
+ /**
569
+ * Sets the geographic latitude and recalculates house cusps
570
+ * @param {number} latitude - Geographic latitude in degrees
571
+ * @returns {boolean} - Success status
572
+ */
573
+ setLatitude(latitude) {
574
+ if (typeof latitude !== 'number' || latitude < -90 || latitude > 90) {
575
+ return false;
576
+ }
577
+
578
+ this.astronomicalData.latitude = latitude;
579
+ this._initializeHouseCusps();
580
+ return true;
581
+ }
582
+
583
+ /**
584
+ * Sets a planet's position
585
+ * @param {string} planetName - Name of the planet
586
+ * @param {number} longitude - Longitude in degrees
587
+ * @returns {boolean} - Success status
588
+ */
589
+ setPlanetPosition(planetName, longitude) {
590
+ const planetLower = planetName.toLowerCase();
591
+
592
+ if (typeof longitude !== 'number' || longitude < 0 || longitude >= 360) {
593
+ return false;
594
+ }
595
+
596
+ if (this.astronomicalData.planets.hasOwnProperty(planetLower)) {
597
+ this.astronomicalData.planets[planetLower] = longitude;
598
+ return true;
599
+ }
600
+
601
+ return false;
602
+ }
603
+
604
+ /**
605
+ * Gets a planet's position
606
+ * @param {string} planetName - Name of the planet
607
+ * @returns {number|null} - Planet longitude or null if not found
608
+ */
609
+ getPlanetPosition(planetName) {
610
+ const planetLower = planetName.toLowerCase();
611
+
612
+ if (this.astronomicalData.planets.hasOwnProperty(planetLower)) {
613
+ return this.astronomicalData.planets[planetLower];
614
+ }
615
+
616
+ return null;
617
+ }
618
+
619
+ /**
620
+ * Gets the current house system
621
+ * @returns {string} - Name of the current house system
622
+ */
623
+ getHouseSystem() {
624
+ return this.astronomicalData.houseSystem;
625
+ }
626
+
627
+ /**
628
+ * Gets the available house systems by creating a temporary calculator
629
+ * @returns {Array} - Array of available house system names
630
+ */
631
+ getAvailableHouseSystems() {
632
+ const calculator = new HouseCalculator();
633
+ return calculator.getAvailableHouseSystems();
634
+ }
635
+ }
636
+
637
+ /**
638
+ * SvgUtils.js
639
+ * Utility class for working with SVG elements
640
+ */
641
+ class SvgUtils {
642
+ constructor() {
643
+ this.svgNS = "http://www.w3.org/2000/svg";
644
+ }
645
+
646
+ /**
647
+ * Creates an SVG element with the specified tag
648
+ * @param {string} tagName - Name of the SVG tag
649
+ * @param {Object} attributes - Object with attributes to set
650
+ * @returns {Element} Created SVG element
651
+ */
652
+ createSVGElement(tagName, attributes = {}) {
653
+ const element = document.createElementNS(this.svgNS, tagName);
654
+
655
+ for (const [key, value] of Object.entries(attributes)) {
656
+ element.setAttribute(key, value);
657
+ }
658
+
659
+ return element;
660
+ }
661
+
662
+ /**
663
+ * Adds a tooltip (title) to an SVG element
664
+ * @param {Element} element - SVG element
665
+ * @param {string} text - Tooltip text
666
+ * @returns {Element} The element with tooltip
667
+ */
668
+ addTooltip(element, text) {
669
+ const title = document.createElementNS(this.svgNS, "title");
670
+ title.textContent = text;
671
+ element.appendChild(title);
672
+ return element;
673
+ }
674
+
675
+ /**
676
+ * Calculates coordinates of a point on a circle
677
+ * @param {number} centerX - X coordinate of the center
678
+ * @param {number} centerY - Y coordinate of the center
679
+ * @param {number} radius - Circle radius
680
+ * @param {number} angle - Angle in degrees
681
+ * @returns {Object} Object with x and y coordinates
682
+ */
683
+ pointOnCircle(centerX, centerY, radius, angle) {
684
+ // Convert angle to radians (accounting for 0 starting at the top)
685
+ const radians = (angle - 90) * (Math.PI / 180);
686
+
687
+ return {
688
+ x: centerX + radius * Math.cos(radians),
689
+ y: centerY + radius * Math.sin(radians)
690
+ };
691
+ }
692
+ }
693
+
694
+ /**
695
+ * SVGManager.js
696
+ * Handles the creation, management, and querying of the main SVG element and its layer groups.
697
+ */
698
+
699
+ class SVGManager {
700
+ /**
701
+ * Constructor
702
+ * @param {Object} options - Manager options
703
+ * @param {SvgUtils} [options.svgUtils] - SvgUtils instance (optional, will use from registry if not provided)
704
+ */
705
+ constructor(options = {}) {
706
+ this.svgNS = "http://www.w3.org/2000/svg";
707
+ this.svg = null; // Reference to the main SVG element
708
+ this.groups = {}; // References to the layer groups (g elements)
709
+
710
+ // Use injected svgUtils or get from registry
711
+ this.svgUtils = options.svgUtils || ServiceRegistry.getSvgUtils();
712
+
713
+ // Define standard group order (bottom to top)
714
+ this.groupOrder = [
715
+ 'zodiac',
716
+ 'houseDivisions',
717
+ 'aspects', // Render aspects below planets and houses
718
+ 'primaryPlanets', // Inner circle planets
719
+ 'secondaryPlanets', // Innermost circle planets
720
+ 'houses' // House numbers on top
721
+ // Add other groups if needed, e.g., 'tooltips'
722
+ ];
723
+ }
724
+
725
+ /**
726
+ * Initializes the main SVG element within a container.
727
+ * @param {string|Element} containerSelector - ID/CSS selector of the container element or the element itself.
728
+ * @param {Object} options - SVG attributes (e.g., viewBox, width, height, class, preserveAspectRatio).
729
+ * @returns {SVGElement | null} The created SVG element or null if container not found.
730
+ */
731
+ initialize(containerSelector, options = {}) {
732
+ // Handle both string selectors and DOM elements
733
+ let container;
734
+ if (typeof containerSelector === 'string') {
735
+ container = document.querySelector(containerSelector);
736
+ if (!container) {
737
+ console.error(`SVGManager: Container not found with selector: ${containerSelector}`);
738
+ return null;
739
+ }
740
+ } else if (containerSelector instanceof Element || containerSelector instanceof HTMLElement) {
741
+ container = containerSelector;
742
+ } else {
743
+ console.error(`SVGManager: Invalid container. Expected string selector or DOM element.`);
744
+ return null;
745
+ }
746
+
747
+ // Clear container
748
+ container.innerHTML = '';
749
+
750
+ // Default options
751
+ const defaultOptions = {
752
+ width: "100%",
753
+ height: "100%",
754
+ viewBox: "0 0 460 460", // Default size, should match ChartConfig ideally
755
+ preserveAspectRatio: "xMidYMid meet",
756
+ class: "nocturna-wheel-svg" // Default class
757
+ };
758
+
759
+ const svgOptions = { ...defaultOptions, ...options };
760
+
761
+ // Create SVG element
762
+ this.svg = document.createElementNS(this.svgNS, "svg");
763
+
764
+ // Set attributes
765
+ for (const [key, value] of Object.entries(svgOptions)) {
766
+ this.svg.setAttribute(key, value);
767
+ }
768
+
769
+ // Append to container
770
+ container.appendChild(this.svg);
771
+ console.log("SVGManager: SVG initialized");
772
+ return this.svg;
773
+ }
774
+
775
+ /**
776
+ * Creates the standard layer groups within the SVG in the predefined order.
777
+ */
778
+ createStandardGroups() {
779
+ if (!this.svg) {
780
+ console.error("SVGManager: Cannot create groups, SVG not initialized.");
781
+ return;
782
+ }
783
+
784
+ // Clear existing groups before creating new ones (or check if they exist)
785
+ this.groups = {};
786
+ // Remove existing group elements from SVG if any
787
+ this.svg.querySelectorAll('g').forEach(g => g.remove());
788
+
789
+ console.log("SVGManager: Creating standard groups:", this.groupOrder);
790
+ this.groupOrder.forEach(groupName => {
791
+ this.createGroup(groupName);
792
+ });
793
+
794
+ // Create a legacy 'planets' group for backward compatibility
795
+ // This will be deprecated in future versions
796
+ this.createGroup('planets');
797
+ }
798
+
799
+ /**
800
+ * Creates a named group (<g>) element and appends it to the SVG.
801
+ * If the group already exists, it returns the existing group.
802
+ * @param {string} name - The name (and ID) for the group.
803
+ * @returns {SVGElement | null} The created or existing group element, or null if SVG not initialized.
804
+ */
805
+ createGroup(name) {
806
+ if (!this.svg) {
807
+ console.error(`SVGManager: Cannot create group '${name}', SVG not initialized.`);
808
+ return null;
809
+ }
810
+ if (this.groups[name]) {
811
+ return this.groups[name]; // Return existing group
812
+ }
813
+
814
+ const group = document.createElementNS(this.svgNS, "g");
815
+ group.setAttribute("id", `group-${name}`); // Set ID for easy debugging/selection
816
+ group.setAttribute("class", `svg-group svg-group-${name}`); // Add class
817
+
818
+ this.svg.appendChild(group); // Append to SVG (order matters based on creation sequence)
819
+ this.groups[name] = group;
820
+ // console.log(`SVGManager: Created group '${name}'`);
821
+ return group;
822
+ }
823
+
824
+ /**
825
+ * Retrieves a previously created group element by name.
826
+ * @param {string} name - The name of the group.
827
+ * @returns {SVGElement | null} The group element or null if not found or not initialized.
828
+ */
829
+ getGroup(name) {
830
+ if (!this.svg) {
831
+ console.warn(`SVGManager: Cannot get group '${name}', SVG not initialized.`);
832
+ return null;
833
+ }
834
+
835
+ // For backward compatibility, map 'planets' to 'primaryPlanets'
836
+ if (name === 'planets') {
837
+ console.warn('SVGManager: Using deprecated "planets" group. Use "primaryPlanets" or "secondaryPlanets" instead.');
838
+ name = 'primaryPlanets';
839
+ }
840
+
841
+ if (!this.groups[name]) {
842
+ console.warn(`SVGManager: Group '${name}' not found. Creating it.`);
843
+ // Attempt to create if missing, might indicate an issue elsewhere
844
+ return this.createGroup(name);
845
+ }
846
+ return this.groups[name];
847
+ }
848
+
849
+ /**
850
+ * Retrieves all created group elements.
851
+ * @returns {Object<string, SVGElement>} An object mapping group names to their SVGElement references.
852
+ */
853
+ getAllGroups() {
854
+ return this.groups;
855
+ }
856
+
857
+ /**
858
+ * Returns the main SVG element.
859
+ * @returns {SVGElement | null} The main SVG element or null if not initialized.
860
+ */
861
+ getSVG() {
862
+ return this.svg;
863
+ }
864
+
865
+ } // End of SVGManager class
866
+
867
+ /**
868
+ * BaseRenderer.js
869
+ * Self-contained base class for all renderers.
870
+ * This version has all dependencies inlined to avoid import issues.
871
+ */
872
+
873
+ // Define a completely self-contained renderer base class
874
+ class BaseRenderer {
875
+ /**
876
+ * Constructor
877
+ * @param {Object} options - Renderer options.
878
+ * @param {string} options.svgNS - SVG namespace.
879
+ * @param {Object} options.config - Chart configuration object.
880
+ * @param {Object} options.svgUtils - SVG utility service.
881
+ */
882
+ constructor(options) {
883
+ if (!options || !options.svgNS || !options.config) {
884
+ throw new Error(`${this.constructor.name}: Missing required options (svgNS, config)`);
885
+ }
886
+
887
+ // Store base options
888
+ this.svgNS = options.svgNS;
889
+ this.config = options.config;
890
+ this.options = options;
891
+
892
+ // Use the provided svgUtils or create minimal internal SVG utilities
893
+ this.svgUtils = options.svgUtils || {
894
+ // Store SVG namespace
895
+ svgNS: this.svgNS,
896
+
897
+ // Create SVG element
898
+ createSVGElement: (tagName, attributes = {}) => {
899
+ const element = document.createElementNS(this.svgNS, tagName);
900
+ for (const [key, value] of Object.entries(attributes)) {
901
+ element.setAttribute(key, value);
902
+ }
903
+ return element;
904
+ },
905
+
906
+ // Add tooltip to element
907
+ addTooltip: (element, text) => {
908
+ const title = document.createElementNS(this.svgNS, "title");
909
+ title.textContent = text;
910
+ element.appendChild(title);
911
+ return element;
912
+ },
913
+
914
+ // Calculate point on circle
915
+ pointOnCircle: (centerX, centerY, radius, angle) => {
916
+ const radians = (angle - 90) * (Math.PI / 180);
917
+ return {
918
+ x: centerX + radius * Math.cos(radians),
919
+ y: centerY + radius * Math.sin(radians)
920
+ };
921
+ }
922
+ };
923
+
924
+ // Get dimensions from config
925
+ this.centerX = this.config.svg.center.x;
926
+ this.centerY = this.config.svg.center.y;
927
+ this.innerRadius = this.config.radius.zodiacInner;
928
+ this.middleRadius = this.config.radius.zodiacMiddle;
929
+ this.outerRadius = this.config.radius.zodiacOuter;
930
+ }
931
+
932
+ /**
933
+ * Clears all child nodes of the given parent SVG group.
934
+ * @param {Element} parentGroup - The SVG group to clear.
935
+ */
936
+ clearGroup(parentGroup) {
937
+ if (parentGroup) {
938
+ parentGroup.innerHTML = '';
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Abstract render method to be implemented by subclasses.
944
+ * @param {Element} parentGroup - The parent SVG group element.
945
+ * @returns {Array} Array of rendered SVG elements.
946
+ */
947
+ render(parentGroup) {
948
+ throw new Error(`${this.constructor.name}: render() not implemented.`);
949
+ }
950
+ }
951
+
952
+ /**
953
+ * AstrologyUtils.js
954
+ * Utility class for astrological calculations
955
+ */
956
+ class AstrologyUtils {
957
+ /**
958
+ * Capitalizes the first letter of a string
959
+ * @param {string} string - Input string
960
+ * @returns {string} String with capitalized first letter
961
+ */
962
+ static capitalizeFirstLetter(string) {
963
+ return string.charAt(0).toUpperCase() + string.slice(1);
964
+ }
965
+
966
+ /**
967
+ * Determines the house based on position and rotation angle
968
+ * @param {number} position - Position in degrees (0-359)
969
+ * @param {number} rotationAngle - Rotation angle of the house system
970
+ * @returns {number} House number (1-12)
971
+ */
972
+ static getHouseFromPosition(position, rotationAngle = 0) {
973
+ // Adjust position relative to house system rotation
974
+ const adjustedPosition = (position - rotationAngle + 360) % 360;
975
+ // Determine house
976
+ return Math.floor(adjustedPosition / 30) + 1;
977
+ }
978
+
979
+ /**
980
+ * Returns the list of zodiac sign names
981
+ * @returns {Array} Array of zodiac sign names
982
+ */
983
+ static getZodiacSigns() {
984
+ return [
985
+ "aries", "taurus", "gemini", "cancer", "leo", "virgo",
986
+ "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces"
987
+ ];
988
+ }
989
+
990
+ /**
991
+ * Returns the available house systems with descriptions
992
+ * @returns {Object} Object mapping house system names to their descriptions
993
+ */
994
+ static getHouseSystems() {
995
+ return {
996
+ "Placidus": "Most commonly used house system in Western astrology, based on time of day",
997
+ "Koch": "Developed by Walter Koch, a time-based system similar to Placidus",
998
+ "Campanus": "Medieval system dividing the prime vertical into equal parts",
999
+ "Regiomontanus": "Similar to Campanus, but using the celestial equator instead of prime vertical",
1000
+ "Equal": "Divides the ecliptic into 12 equal segments of 30° each from the Ascendant",
1001
+ "Whole Sign": "Assigns the entire rising sign to the 1st house, with subsequent signs as houses",
1002
+ "Porphyry": "Simple system that divides each quadrant into three equal parts",
1003
+ "Topocentric": "Modern system similar to Placidus but more accurate for extreme latitudes"
1004
+ };
1005
+ }
1006
+
1007
+ /**
1008
+ * Returns the list of planets
1009
+ * @returns {Array} Array of planet names
1010
+ */
1011
+ static getPlanets() {
1012
+ return [
1013
+ "sun", "moon", "mercury", "venus", "mars",
1014
+ "jupiter", "saturn", "uranus", "neptune", "pluto"
1015
+ ];
1016
+ }
1017
+
1018
+ /**
1019
+ * Converts a house number to a Roman numeral
1020
+ * @param {number} house - House number (1-12)
1021
+ * @returns {string} Roman numeral
1022
+ */
1023
+ static houseToRoman(house) {
1024
+ const romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
1025
+ return romanNumerals[house - 1] || '';
1026
+ }
1027
+
1028
+ /**
1029
+ * Returns the full name of a planet in the specified language
1030
+ * @param {string} planetCode - Planet code (sun, moon, etc.)
1031
+ * @param {string} language - Language code (default: 'en')
1032
+ * @returns {string} Full planet name
1033
+ */
1034
+ static getPlanetFullName(planetCode, language = 'en') {
1035
+ const planetNames = {
1036
+ en: {
1037
+ "sun": "Sun",
1038
+ "moon": "Moon",
1039
+ "mercury": "Mercury",
1040
+ "venus": "Venus",
1041
+ "mars": "Mars",
1042
+ "jupiter": "Jupiter",
1043
+ "saturn": "Saturn",
1044
+ "uranus": "Uranus",
1045
+ "neptune": "Neptune",
1046
+ "pluto": "Pluto"
1047
+ },
1048
+ ru: {
1049
+ "sun": "Солнце",
1050
+ "moon": "Луна",
1051
+ "mercury": "Меркурий",
1052
+ "venus": "Венера",
1053
+ "mars": "Марс",
1054
+ "jupiter": "Юпитер",
1055
+ "saturn": "Сатурн",
1056
+ "uranus": "Уран",
1057
+ "neptune": "Нептун",
1058
+ "pluto": "Плутон"
1059
+ }
1060
+ };
1061
+
1062
+ const names = planetNames[language] || planetNames.en;
1063
+ return names[planetCode.toLowerCase()] || this.capitalizeFirstLetter(planetCode);
1064
+ }
1065
+
1066
+ /**
1067
+ * Returns the full name of a zodiac sign in the specified language
1068
+ * @param {string} signCode - Zodiac sign code (aries, taurus, etc.)
1069
+ * @param {string} language - Language code (default: 'en')
1070
+ * @returns {string} Full zodiac sign name
1071
+ */
1072
+ static getZodiacSignFullName(signCode, language = 'en') {
1073
+ const signNames = {
1074
+ en: {
1075
+ "aries": "Aries",
1076
+ "taurus": "Taurus",
1077
+ "gemini": "Gemini",
1078
+ "cancer": "Cancer",
1079
+ "leo": "Leo",
1080
+ "virgo": "Virgo",
1081
+ "libra": "Libra",
1082
+ "scorpio": "Scorpio",
1083
+ "sagittarius": "Sagittarius",
1084
+ "capricorn": "Capricorn",
1085
+ "aquarius": "Aquarius",
1086
+ "pisces": "Pisces"
1087
+ },
1088
+ ru: {
1089
+ "aries": "Овен",
1090
+ "taurus": "Телец",
1091
+ "gemini": "Близнецы",
1092
+ "cancer": "Рак",
1093
+ "leo": "Лев",
1094
+ "virgo": "Дева",
1095
+ "libra": "Весы",
1096
+ "scorpio": "Скорпион",
1097
+ "sagittarius": "Стрелец",
1098
+ "capricorn": "Козерог",
1099
+ "aquarius": "Водолей",
1100
+ "pisces": "Рыбы"
1101
+ }
1102
+ };
1103
+
1104
+ const names = signNames[language] || signNames.en;
1105
+ return names[signCode.toLowerCase()] || this.capitalizeFirstLetter(signCode);
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ * ZodiacRenderer.js
1111
+ * Class for rendering the zodiac circle and signs.
1112
+ */
1113
+ class ZodiacRenderer extends BaseRenderer {
1114
+ /**
1115
+ * Constructor
1116
+ * @param {Object} options - Renderer options.
1117
+ * @param {string} options.svgNS - SVG namespace.
1118
+ * @param {ChartConfig} options.config - Chart configuration object.
1119
+ * @param {string} options.assetBasePath - Base path for assets.
1120
+ * @param {IconProvider} [options.iconProvider] - Icon provider service.
1121
+ */
1122
+ constructor(options) {
1123
+ super(options);
1124
+ if (!options.assetBasePath) {
1125
+ throw new Error("ZodiacRenderer: Missing required option assetBasePath");
1126
+ }
1127
+
1128
+ this.iconProvider = options.iconProvider; // Store the icon provider
1129
+ this.signIconRadius = (this.outerRadius + this.middleRadius) / 2;
1130
+ this.signIconSize = 30;
1131
+ }
1132
+
1133
+ /**
1134
+ * Renders the zodiac wheel components.
1135
+ * @param {Element} parentGroup - The parent SVG group element.
1136
+ * @returns {Array} Array of rendered SVG elements (or empty array).
1137
+ */
1138
+ render(parentGroup) {
1139
+ if (!parentGroup) {
1140
+ console.error("ZodiacRenderer: parentGroup is null or undefined.");
1141
+ return [];
1142
+ }
1143
+ this.clearGroup(parentGroup);
1144
+
1145
+ const renderedElements = [];
1146
+
1147
+ renderedElements.push(...this.renderBaseCircles(parentGroup));
1148
+ renderedElements.push(...this.renderDivisionLines(parentGroup));
1149
+ renderedElements.push(...this.renderZodiacSigns(parentGroup));
1150
+
1151
+ console.log("ZodiacRenderer: Rendering complete.");
1152
+ return renderedElements;
1153
+ }
1154
+
1155
+ /**
1156
+ * Renders the base circles for the chart layout.
1157
+ * @param {Element} parentGroup - The parent SVG group.
1158
+ * @returns {Array<Element>} Array containing the created circle elements.
1159
+ */
1160
+ renderBaseCircles(parentGroup) {
1161
+ const elements = [];
1162
+ const circles = [
1163
+ { r: this.outerRadius, class: "chart-outer-circle" },
1164
+ { r: this.middleRadius, class: "chart-middle-circle" },
1165
+ { r: this.innerRadius, class: "chart-inner-circle" }
1166
+ ];
1167
+
1168
+ circles.forEach(circleData => {
1169
+ const circle = this.svgUtils.createSVGElement("circle", {
1170
+ cx: this.centerX,
1171
+ cy: this.centerY,
1172
+ r: circleData.r,
1173
+ class: `zodiac-element ${circleData.class}` // Add base class
1174
+ });
1175
+ parentGroup.appendChild(circle);
1176
+ elements.push(circle);
1177
+ });
1178
+ return elements;
1179
+ }
1180
+
1181
+ /**
1182
+ * Renders the division lines between zodiac signs.
1183
+ * @param {Element} parentGroup - The parent SVG group.
1184
+ * @returns {Array<Element>} Array containing the created line elements.
1185
+ */
1186
+ renderDivisionLines(parentGroup) {
1187
+ const elements = [];
1188
+ for (let i = 0; i < 12; i++) {
1189
+ const angle = i * 30; // 30 degrees per sign
1190
+
1191
+ // Lines span from the inner zodiac ring to the outer zodiac ring
1192
+ const point1 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.middleRadius, angle);
1193
+ const point2 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.outerRadius, angle);
1194
+
1195
+ // Instead of skipping lines, use special classes for cardinal points
1196
+ let specialClass = "";
1197
+ if (angle === 0) specialClass = "aries-point"; // Aries point (0°)
1198
+ else if (angle === 90) specialClass = "cancer-point"; // Cancer point (90°)
1199
+ else if (angle === 180) specialClass = "libra-point"; // Libra point (180°)
1200
+ else if (angle === 270) specialClass = "capricorn-point"; // Capricorn point (270°)
1201
+
1202
+ const line = this.svgUtils.createSVGElement("line", {
1203
+ x1: point1.x,
1204
+ y1: point1.y,
1205
+ x2: point2.x,
1206
+ y2: point2.y,
1207
+ class: `zodiac-element zodiac-division-line ${specialClass}`
1208
+ });
1209
+
1210
+ parentGroup.appendChild(line);
1211
+ elements.push(line);
1212
+ }
1213
+ return elements;
1214
+ }
1215
+
1216
+ /**
1217
+ * Renders the zodiac sign icons.
1218
+ * @param {Element} parentGroup - The parent SVG group.
1219
+ * @returns {Array<Element>} Array containing the created image elements.
1220
+ */
1221
+ renderZodiacSigns(parentGroup) {
1222
+ const elements = [];
1223
+ const zodiacSigns = AstrologyUtils.getZodiacSigns(); // Assumes AstrologyUtils is available
1224
+
1225
+ for (let i = 0; i < 12; i++) {
1226
+ const signName = zodiacSigns[i];
1227
+ // Place icon in the middle of the sign's 30-degree sector
1228
+ const angle = i * 30 + 15;
1229
+
1230
+ // Calculate position for the icon center
1231
+ const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.signIconRadius, angle);
1232
+
1233
+ // Get icon path using IconProvider if available
1234
+ let iconHref;
1235
+ if (this.iconProvider) {
1236
+ iconHref = this.iconProvider.getZodiacIconPath(signName);
1237
+ } else {
1238
+ // Fallback to old path construction
1239
+ iconHref = `${this.options.assetBasePath}svg/zodiac/zodiac-sign-${signName}.svg`;
1240
+ }
1241
+
1242
+ console.log(`Loading zodiac sign: ${iconHref}`);
1243
+
1244
+ const icon = this.svgUtils.createSVGElement("image", {
1245
+ x: point.x - this.signIconSize / 2, // Offset to center the icon
1246
+ y: point.y - this.signIconSize / 2,
1247
+ width: this.signIconSize,
1248
+ height: this.signIconSize,
1249
+ href: iconHref,
1250
+ class: `zodiac-element zodiac-sign zodiac-sign-${signName}` // Add base and specific class
1251
+ });
1252
+
1253
+ // Fallback for missing icons
1254
+ icon.addEventListener('error', () => {
1255
+ console.warn(`Zodiac sign icon not found: ${iconHref}`);
1256
+ icon.setAttribute('href', ''); // Remove broken link
1257
+
1258
+ // Create a text fallback
1259
+ const fallbackText = signName.substring(0, 3).toUpperCase();
1260
+
1261
+ // Use IconProvider's createTextFallback if available
1262
+ let textElement;
1263
+ if (this.iconProvider) {
1264
+ textElement = this.iconProvider.createTextFallback(
1265
+ this.svgUtils,
1266
+ {
1267
+ x: point.x,
1268
+ y: point.y,
1269
+ size: '10px',
1270
+ color: '#ccc',
1271
+ className: `zodiac-element zodiac-sign zodiac-sign-${signName}`
1272
+ },
1273
+ fallbackText
1274
+ );
1275
+ } else {
1276
+ // Legacy fallback
1277
+ textElement = this.svgUtils.createSVGElement('text', {
1278
+ x: point.x,
1279
+ y: point.y,
1280
+ 'text-anchor': 'middle',
1281
+ 'dominant-baseline': 'central',
1282
+ 'font-size': '10px',
1283
+ fill: '#ccc',
1284
+ class: `zodiac-element zodiac-sign zodiac-sign-${signName}`
1285
+ });
1286
+ textElement.textContent = fallbackText;
1287
+ }
1288
+
1289
+ parentGroup.appendChild(textElement);
1290
+ elements.push(textElement); // Add placeholder to rendered elements
1291
+ });
1292
+
1293
+
1294
+ // Add tooltip with the full sign name
1295
+ this.svgUtils.addTooltip(icon, AstrologyUtils.getZodiacSignFullName(signName));
1296
+
1297
+ parentGroup.appendChild(icon);
1298
+ elements.push(icon);
1299
+ }
1300
+ return elements;
1301
+ }
1302
+ } // End of ZodiacRenderer class
1303
+
1304
+ /**
1305
+ * HouseRenderer.js
1306
+ * Class for rendering astrological houses in a natal chart.
1307
+ */
1308
+ class HouseRenderer extends BaseRenderer {
1309
+ /**
1310
+ * Constructor
1311
+ * @param {Object} options - Renderer options.
1312
+ * @param {string} options.svgNS - SVG namespace.
1313
+ * @param {ChartConfig} options.config - Chart configuration object.
1314
+ * @param {Object} options.houseData - House cusps data (optional).
1315
+ */
1316
+ constructor(options) {
1317
+ super(options);
1318
+ this.houseData = options.houseData || [];
1319
+ // Specific radii for house renderer
1320
+ this.extendedRadius = this.outerRadius + 25;
1321
+ this.numberRadius = this.outerRadius + 30;
1322
+ }
1323
+
1324
+ /**
1325
+ * Renders the house components.
1326
+ * @param {Element} parentGroup - The parent SVG group element.
1327
+ * @param {number} rotationAngle - Rotation angle for the house system (default: 0).
1328
+ * @returns {Array} Array of rendered SVG elements.
1329
+ */
1330
+ render(parentGroup, rotationAngle = 0) {
1331
+ if (!parentGroup) {
1332
+ console.error("HouseRenderer: parentGroup is null or undefined.");
1333
+ return [];
1334
+ }
1335
+ // Clear the group before rendering
1336
+ this.clearGroup(parentGroup);
1337
+
1338
+ const renderedElements = [];
1339
+
1340
+ renderedElements.push(...this.renderDivisions(parentGroup, rotationAngle));
1341
+ renderedElements.push(...this.renderNumbers(parentGroup, rotationAngle));
1342
+
1343
+ console.log("HouseRenderer: Rendering complete.");
1344
+ return renderedElements;
1345
+ }
1346
+
1347
+ /**
1348
+ * Renders the house division lines.
1349
+ * @param {Element} parentGroup - The parent SVG group.
1350
+ * @param {number} rotationAngle - Rotation angle for the house system.
1351
+ * @returns {Array<Element>} Array containing the created line elements.
1352
+ */
1353
+ renderDivisions(parentGroup, rotationAngle) {
1354
+ const elements = [];
1355
+ let ascendantAlignmentOffset = 0;
1356
+
1357
+ // Log house data for debugging
1358
+ console.log("HouseRenderer: House data for divisions:", this.houseData);
1359
+
1360
+ // Calculate offset only if house data is present
1361
+ if (this.houseData && this.houseData.length >= 12) {
1362
+ // Handle both legacy format { lon: value } and new format [value1, value2, ...]
1363
+ const ascendantLon = this.getHouseLongitude(this.houseData[0]);
1364
+ // Offset needed to place Ascendant (house 1 cusp) at 0 degrees (top side)
1365
+ ascendantAlignmentOffset = (360 - ascendantLon) % 360;
1366
+ }
1367
+
1368
+ // Add division lines for 12 houses with rotation
1369
+ for (let i = 0; i < 12; i++) {
1370
+ // Use custom house positions if available, otherwise evenly distribute
1371
+ let baseAngle;
1372
+ if (this.houseData && this.houseData.length >= 12) {
1373
+ baseAngle = this.getHouseLongitude(this.houseData[i]);
1374
+ } else {
1375
+ baseAngle = i * 30; // Default if no data
1376
+ }
1377
+ // Apply alignment offset and user rotation
1378
+ const angle = (baseAngle + ascendantAlignmentOffset + rotationAngle) % 360;
1379
+
1380
+ // Determine the house type/class for styling
1381
+ let axisClass = '';
1382
+ if (i === 0) axisClass = 'axis asc'; // ASC - Ascendant (1st house cusp)
1383
+ else if (i === 3) axisClass = 'axis ic'; // IC - Imum Coeli (4th house cusp)
1384
+ else if (i === 6) axisClass = 'axis dsc'; // DSC - Descendant (7th house cusp)
1385
+ else if (i === 9) axisClass = 'axis mc'; // MC - Medium Coeli (10th house cusp)
1386
+
1387
+ // Calculate points for inner to middle circle lines
1388
+ const innerPoint = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.innerRadius, angle);
1389
+ const middlePoint = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.middleRadius, angle);
1390
+
1391
+ // Create line from inner to middle circle
1392
+ const innerLine = this.svgUtils.createSVGElement("line", {
1393
+ x1: innerPoint.x,
1394
+ y1: innerPoint.y,
1395
+ x2: middlePoint.x,
1396
+ y2: middlePoint.y,
1397
+ class: `house-element house-division-line ${axisClass}`
1398
+ });
1399
+
1400
+ // Ensure cardinal points render above zodiac lines
1401
+ if (axisClass.includes('axis')) {
1402
+ innerLine.style.zIndex = 10;
1403
+ }
1404
+
1405
+ parentGroup.appendChild(innerLine);
1406
+ elements.push(innerLine);
1407
+
1408
+ // Calculate points for middle to outer circle lines
1409
+ const outerPoint = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.outerRadius, angle);
1410
+
1411
+ // We don't render the middle segment anymore (between middle and outer circles)
1412
+ // to avoid overlapping with zodiac signs
1413
+
1414
+ // Calculate point for extended line outside of the outer circle
1415
+ const extendedPoint = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.extendedRadius, angle);
1416
+
1417
+ // Create line from outer circle to extended point
1418
+ const outerLine = this.svgUtils.createSVGElement("line", {
1419
+ x1: outerPoint.x,
1420
+ y1: outerPoint.y,
1421
+ x2: extendedPoint.x,
1422
+ y2: extendedPoint.y,
1423
+ class: `house-element house-division-line outer ${axisClass}`
1424
+ });
1425
+
1426
+ // Ensure cardinal points render above zodiac lines
1427
+ if (axisClass.includes('axis')) {
1428
+ outerLine.style.zIndex = 10;
1429
+ }
1430
+
1431
+ parentGroup.appendChild(outerLine);
1432
+ elements.push(outerLine);
1433
+ }
1434
+
1435
+ return elements;
1436
+ }
1437
+
1438
+ /**
1439
+ * Renders the house numbers.
1440
+ * @param {Element} parentGroup - The parent SVG group.
1441
+ * @param {number} rotationAngle - Rotation angle for the house system.
1442
+ * @returns {Array<Element>} Array containing the created text elements.
1443
+ */
1444
+ renderNumbers(parentGroup, rotationAngle) {
1445
+ const elements = [];
1446
+ let ascendantAlignmentOffset = 0;
1447
+
1448
+ // Log house data for debugging
1449
+ console.log("HouseRenderer: House data for numbers:", this.houseData);
1450
+
1451
+ // Calculate offset only if house data is present
1452
+ if (this.houseData && this.houseData.length >= 12) {
1453
+ const ascendantLon = this.getHouseLongitude(this.houseData[0]);
1454
+ // Offset needed to place Ascendant (house 1 cusp) at 0 degrees (top side)
1455
+ ascendantAlignmentOffset = (360 - ascendantLon) % 360;
1456
+ }
1457
+
1458
+ // Add Roman numerals for house numbers
1459
+ for (let i = 0; i < 12; i++) {
1460
+ // Get house angle with rotation
1461
+ let baseHouseAngle;
1462
+ if (this.houseData && this.houseData.length >= 12) {
1463
+ baseHouseAngle = this.getHouseLongitude(this.houseData[i]);
1464
+ } else {
1465
+ baseHouseAngle = i * 30; // Default if no data
1466
+ }
1467
+ // Apply alignment offset and user rotation
1468
+ const houseAngle = (baseHouseAngle + ascendantAlignmentOffset + rotationAngle) % 360;
1469
+
1470
+ // Offset text from house line clockwise
1471
+ const angle = (houseAngle + 15) % 360; // Place in middle of house segment, apply modulo
1472
+
1473
+ // Calculate position for house number
1474
+ const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.numberRadius, angle);
1475
+
1476
+ // Create text element for house number
1477
+ const text = this.svgUtils.createSVGElement("text", {
1478
+ x: point.x,
1479
+ y: point.y,
1480
+ class: "house-element house-number"
1481
+ });
1482
+
1483
+ // Set text alignment based on position
1484
+ if ((angle >= 315 || angle < 45)) {
1485
+ text.setAttribute("text-anchor", "middle");
1486
+ text.setAttribute("dominant-baseline", "auto");
1487
+ }
1488
+ // For right side (houses on right)
1489
+ else if (angle >= 45 && angle < 135) {
1490
+ text.setAttribute("text-anchor", "start");
1491
+ text.setAttribute("dominant-baseline", "middle");
1492
+ }
1493
+ // For bottom half (houses at bottom)
1494
+ else if (angle >= 135 && angle < 225) {
1495
+ text.setAttribute("text-anchor", "middle");
1496
+ text.setAttribute("dominant-baseline", "hanging");
1497
+ }
1498
+ // For left side (houses on left)
1499
+ else {
1500
+ text.setAttribute("text-anchor", "end");
1501
+ text.setAttribute("dominant-baseline", "middle");
1502
+ }
1503
+
1504
+ // Set house number as text (Roman numeral)
1505
+ text.textContent = AstrologyUtils.houseToRoman(i + 1);
1506
+
1507
+ // Add tooltip with house information
1508
+ this.svgUtils.addTooltip(text, `House ${i + 1}`);
1509
+
1510
+ parentGroup.appendChild(text);
1511
+ elements.push(text);
1512
+ }
1513
+
1514
+ return elements;
1515
+ }
1516
+
1517
+ /**
1518
+ * Helper method to get the longitude from various house data formats
1519
+ * @param {Object|number} houseData - House cusp data (can be object with lon property or direct number)
1520
+ * @returns {number} The house cusp longitude in degrees
1521
+ */
1522
+ getHouseLongitude(houseData) {
1523
+ // Handle different data formats
1524
+ if (houseData === null || houseData === undefined) {
1525
+ console.warn("HouseRenderer: Null or undefined house data");
1526
+ return 0;
1527
+ }
1528
+
1529
+ // If it's an object with a lon property (legacy format)
1530
+ if (typeof houseData === 'object' && houseData !== null && 'lon' in houseData) {
1531
+ return houseData.lon;
1532
+ }
1533
+
1534
+ // If it's a direct number (new format from HouseCalculator)
1535
+ if (typeof houseData === 'number') {
1536
+ return houseData;
1537
+ }
1538
+
1539
+ // If it's some other format we don't recognize
1540
+ console.warn("HouseRenderer: Unrecognized house data format", houseData);
1541
+ return 0;
1542
+ }
1543
+ }
1544
+
1545
+ /**
1546
+ * BasePlanetRenderer.js
1547
+ * Base class for all planet renderers, extending BaseRenderer.
1548
+ * Contains common functionality used by specialized planet renderers.
1549
+ */
1550
+ class BasePlanetRenderer extends BaseRenderer {
1551
+ /**
1552
+ * Constructor
1553
+ * @param {Object} options - Renderer options.
1554
+ * @param {string} options.svgNS - SVG namespace.
1555
+ * @param {ChartConfig} options.config - Chart configuration object.
1556
+ * @param {string} options.assetBasePath - Base path for assets.
1557
+ * @param {IconProvider} [options.iconProvider] - Icon provider service.
1558
+ */
1559
+ constructor(options) {
1560
+ super(options);
1561
+ if (!options.assetBasePath) {
1562
+ throw new Error(`${this.constructor.name}: Missing required option assetBasePath`);
1563
+ }
1564
+
1565
+ // Store the icon provider
1566
+ this.iconProvider = options.iconProvider;
1567
+ }
1568
+
1569
+ /**
1570
+ * Calculate base positions (dot and icon) for planets.
1571
+ * @param {Array} planets - Array of planet objects.
1572
+ * @param {number} dotRadius - Radius for planet dots
1573
+ * @param {number} iconRadius - Radius for planet icons
1574
+ */
1575
+ calculateBasePositions(planets, dotRadius, iconRadius) {
1576
+ planets.forEach(planet => {
1577
+ // Convert position to radians (0 degrees = East, anti-clockwise)
1578
+ // Subtract 90 to make 0 degrees = North (top)
1579
+ const radians = (planet.position - 90) * (Math.PI / 180);
1580
+ planet.radians = radians;
1581
+
1582
+ // Calculate position on the dot radius circle
1583
+ const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, dotRadius, planet.position);
1584
+ planet.x = point.x;
1585
+ planet.y = point.y;
1586
+
1587
+ // Calculate base position for the planet icon
1588
+ const iconPoint = this.svgUtils.pointOnCircle(this.centerX, this.centerY, iconRadius, planet.position);
1589
+ planet.iconX = iconPoint.x;
1590
+ planet.iconY = iconPoint.y;
1591
+
1592
+ // Initialize adjusted coordinates to base coordinates
1593
+ planet.adjustedIconX = planet.iconX;
1594
+ planet.adjustedIconY = planet.iconY;
1595
+ });
1596
+ }
1597
+
1598
+ /**
1599
+ * Prepare planet data with common properties.
1600
+ * @param {Array} planetsData - Raw planet data
1601
+ * @param {string} planetType - Type of planet ('primary' or 'secondary')
1602
+ * @param {number} dotRadius - Radius for planet dots
1603
+ * @param {number} iconRadius - Radius for planet icons
1604
+ * @returns {Array} - Prepared planet data with calculated properties
1605
+ */
1606
+ preparePlanetData(planetsData, planetType, dotRadius, iconRadius) {
1607
+ return planetsData.map(p => ({
1608
+ ...p,
1609
+ name: p.name || 'unknown',
1610
+ position: p.position !== undefined ? p.position : 0,
1611
+ // Fields to be calculated:
1612
+ x: 0, y: 0, // Dot coordinates
1613
+ iconX: 0, iconY: 0, // Base icon coordinates
1614
+ adjustedIconX: 0, adjustedIconY: 0, // Icon coordinates after overlap adjustment
1615
+ radians: 0,
1616
+ // Calculate sign index and get name from AstrologyUtils
1617
+ zodiacSign: AstrologyUtils.getZodiacSigns()[Math.floor(p.position / 30) % 12],
1618
+ position_in_sign: p.position % 30, // Calculate position within sign
1619
+ isPrimary: planetType === 'primary',
1620
+ type: planetType,
1621
+ color: p.color || '#000000',
1622
+ // Track which radius this planet is being rendered at
1623
+ dotRadius: dotRadius,
1624
+ iconRadius: iconRadius
1625
+ }));
1626
+ }
1627
+
1628
+ /**
1629
+ * Abstract method to be implemented by subclasses.
1630
+ * @param {Element} parentGroup - The parent SVG group element.
1631
+ * @param {Array} planetsData - Array of planet objects { name: string, position: number }.
1632
+ * @param {number} houseRotationAngle - House system rotation angle.
1633
+ * @param {Object} options - Additional rendering options.
1634
+ * @returns {Array} - Array of planet objects with added coordinates.
1635
+ */
1636
+ render(parentGroup, planetsData, houseRotationAngle = 0, options = {}) {
1637
+ throw new Error(`${this.constructor.name}: render() not implemented.`);
1638
+ }
1639
+ }
1640
+
1641
+ /**
1642
+ * PlanetSymbolRenderer.js
1643
+ * Class for rendering planet symbols and glyphs.
1644
+ */
1645
+ class PlanetSymbolRenderer extends BasePlanetRenderer {
1646
+ /**
1647
+ * Constructor
1648
+ * @param {Object} options - Renderer options.
1649
+ */
1650
+ constructor(options) {
1651
+ super(options);
1652
+ }
1653
+
1654
+ /**
1655
+ * Renders a symbol for a planet
1656
+ * @param {Element} parentGroup - The parent SVG group
1657
+ * @param {Object} planet - Planet object with calculated positions
1658
+ * @param {number} iconSize - Size of the icon in pixels
1659
+ * @returns {Element} - The created symbol element
1660
+ */
1661
+ renderPlanetSymbol(parentGroup, planet, iconSize = 24) {
1662
+ // Get the icon path using IconProvider if available
1663
+ let iconPath;
1664
+ if (this.iconProvider) {
1665
+ iconPath = this.iconProvider.getPlanetIconPath(planet.name);
1666
+ } else {
1667
+ // Fallback to old path construction if IconProvider is not available
1668
+ iconPath = `${this.options.assetBasePath}svg/zodiac/zodiac-planet-${planet.name.toLowerCase()}.svg`;
1669
+ }
1670
+
1671
+ // Calculate top-left position of the icon (centered on the calculated point)
1672
+ const iconX = planet.adjustedIconX - iconSize/2;
1673
+ const iconY = planet.adjustedIconY - iconSize/2;
1674
+
1675
+ // Get planet type for CSS classes
1676
+ const typeClass = planet.isPrimary ? 'primary' : 'secondary';
1677
+
1678
+ const icon = this.svgUtils.createSVGElement("image", {
1679
+ x: iconX,
1680
+ y: iconY,
1681
+ width: iconSize,
1682
+ height: iconSize,
1683
+ href: iconPath,
1684
+ class: `planet-icon planet-${planet.name}-icon planet-${typeClass}-icon`
1685
+ });
1686
+
1687
+ // Add error handling for missing icons
1688
+ icon.addEventListener('error', () => {
1689
+ console.warn(`Planet icon not found: ${iconPath}`);
1690
+ icon.setAttribute('href', ''); // Remove broken link
1691
+
1692
+ // Create a text fallback with first letter
1693
+ const fallbackText = planet.name.charAt(0).toUpperCase();
1694
+
1695
+ // Use IconProvider's createTextFallback if available
1696
+ let textIcon;
1697
+ if (this.iconProvider) {
1698
+ textIcon = this.iconProvider.createTextFallback(
1699
+ this.svgUtils,
1700
+ {
1701
+ x: planet.adjustedIconX,
1702
+ y: planet.adjustedIconY,
1703
+ size: `${iconSize}px`,
1704
+ color: planet.color || '#000000',
1705
+ className: `planet-symbol planet-${planet.name}-symbol planet-${typeClass}-symbol`
1706
+ },
1707
+ fallbackText
1708
+ );
1709
+ } else {
1710
+ // Legacy fallback if IconProvider is not available
1711
+ textIcon = this.svgUtils.createSVGElement("text", {
1712
+ x: planet.adjustedIconX,
1713
+ y: planet.adjustedIconY,
1714
+ 'text-anchor': 'middle',
1715
+ 'dominant-baseline': 'middle',
1716
+ 'font-size': `${iconSize}px`,
1717
+ class: `planet-symbol planet-${planet.name}-symbol planet-${typeClass}-symbol`,
1718
+ fill: planet.color || '#000000'
1719
+ });
1720
+ textIcon.textContent = planet.name.charAt(0).toUpperCase();
1721
+ }
1722
+
1723
+ parentGroup.appendChild(textIcon);
1724
+ });
1725
+
1726
+ return icon;
1727
+ }
1728
+
1729
+ /**
1730
+ * Renders a dot to mark the exact planet position
1731
+ * @param {Element} parentGroup - The parent SVG group
1732
+ * @param {Object} planet - Planet object with calculated positions
1733
+ * @param {number} dotSize - Size of the dot in pixels
1734
+ * @returns {Element} - The created dot element
1735
+ */
1736
+ renderPlanetDot(parentGroup, planet, dotSize = 3) {
1737
+ // Get planet type for CSS classes
1738
+ const typeClass = planet.isPrimary ? 'primary' : 'secondary';
1739
+
1740
+ // Draw the position dot (small circle at exact planet position)
1741
+ const dot = this.svgUtils.createSVGElement("circle", {
1742
+ cx: planet.x,
1743
+ cy: planet.y,
1744
+ r: dotSize, // Small fixed size for position indicator
1745
+ class: `planet-dot planet-${planet.name}-dot planet-${typeClass}-dot`,
1746
+ fill: planet.color || '#000000'
1747
+ });
1748
+
1749
+ return dot;
1750
+ }
1751
+
1752
+ /**
1753
+ * Renders a connector line between the dot and symbol if they're far enough apart
1754
+ * @param {Element} parentGroup - The parent SVG group
1755
+ * @param {Object} planet - Planet object with calculated positions
1756
+ * @param {number} minDistance - Minimum distance required to draw a connector
1757
+ * @returns {Element|null} - The created connector element or null if not needed
1758
+ */
1759
+ renderConnector(parentGroup, planet, iconSize, minDistanceFactor = 0.3) {
1760
+ // Get planet type for CSS classes
1761
+ const typeClass = planet.isPrimary ? 'primary' : 'secondary';
1762
+
1763
+ // Calculate distance between dot and adjusted icon position
1764
+ const distX = planet.x - planet.adjustedIconX;
1765
+ const distY = planet.y - planet.adjustedIconY;
1766
+ const distance = Math.sqrt(distX * distX + distY * distY);
1767
+
1768
+ // Only draw connector if the distance is significant
1769
+ if (distance > iconSize * minDistanceFactor) {
1770
+ const connector = this.svgUtils.createSVGElement('line', {
1771
+ x1: planet.x,
1772
+ y1: planet.y,
1773
+ x2: planet.adjustedIconX,
1774
+ y2: planet.adjustedIconY,
1775
+ class: `planet-element planet-${planet.name.toLowerCase()} planet-connector planet-${typeClass}-connector`,
1776
+ stroke: planet.color || '#000000',
1777
+ 'stroke-width': 0.75,
1778
+ 'stroke-opacity': 0.5
1779
+ });
1780
+
1781
+ return connector;
1782
+ }
1783
+
1784
+ return null;
1785
+ }
1786
+
1787
+ /**
1788
+ * Adds a tooltip to a planet element
1789
+ * @param {Element} element - Element to add tooltip to
1790
+ * @param {Object} planet - Planet object
1791
+ */
1792
+ addPlanetTooltip(element, planet) {
1793
+ const typeLabel = planet.isPrimary ? "Primary" : "Secondary";
1794
+ const tooltipText = `${typeLabel} ${AstrologyUtils.getPlanetFullName(planet.name)}: ${planet.position.toFixed(1)}° ${planet.zodiacSign.toUpperCase()} (${planet.position_in_sign.toFixed(1)}°)`;
1795
+ this.svgUtils.addTooltip(element, tooltipText);
1796
+ }
1797
+
1798
+ /**
1799
+ * Render method implementation from BasePlanetRenderer
1800
+ * This should not be called directly in this class, but is needed to fulfill the interface
1801
+ */
1802
+ render(parentGroup, planetsData, houseRotationAngle = 0, options = {}) {
1803
+ throw new Error(`${this.constructor.name}: This is a utility renderer, use specific rendering methods instead.`);
1804
+ }
1805
+ }
1806
+
1807
+ /**
1808
+ * PlanetPositionCalculator.js
1809
+ * Utility class for calculating planet positions on any circle
1810
+ */
1811
+
1812
+ class PlanetPositionCalculator {
1813
+ /**
1814
+ * Calculate position for a planet on a circle
1815
+ * @param {Object} params - Position parameters
1816
+ * @param {number} params.centerX - X coordinate of center
1817
+ * @param {number} params.centerY - Y coordinate of center
1818
+ * @param {number} params.radius - Circle radius
1819
+ * @param {number} params.longitude - Planet longitude in degrees
1820
+ * @param {number} params.iconSize - Size of the icon (optional)
1821
+ * @returns {Object} Position data with dot and icon coordinates
1822
+ */
1823
+ static calculatePosition(params) {
1824
+ const { centerX, centerY, radius, longitude, iconSize = 24 } = params;
1825
+ const svgUtils = new SvgUtils();
1826
+
1827
+ // Calculate point on circle
1828
+ const point = svgUtils.pointOnCircle(centerX, centerY, radius, longitude);
1829
+
1830
+ // Calculate icon position (centered on the point)
1831
+ const iconX = point.x - (iconSize / 2);
1832
+ const iconY = point.y - (iconSize / 2);
1833
+
1834
+ return {
1835
+ x: point.x, // Dot center X
1836
+ y: point.y, // Dot center Y
1837
+ iconX: iconX, // Icon top-left X
1838
+ iconY: iconY, // Icon top-left Y
1839
+ iconCenterX: point.x, // Icon center X
1840
+ iconCenterY: point.y, // Icon center Y
1841
+ longitude: longitude, // Original longitude
1842
+ radius: radius // Original radius
1843
+ };
1844
+ }
1845
+
1846
+ /**
1847
+ * Detects if planets are too close to each other
1848
+ * @param {Array} positions - Array of position objects
1849
+ * @param {number} minDistance - Minimum distance between planets
1850
+ * @returns {Array} Arrays of overlapping planet indices
1851
+ */
1852
+ static detectOverlaps(positions, minDistance = 24) {
1853
+ const clusters = [];
1854
+ const processed = new Set();
1855
+
1856
+ // Check each pair of planets
1857
+ for (let i = 0; i < positions.length; i++) {
1858
+ if (processed.has(i)) continue;
1859
+
1860
+ const cluster = [i];
1861
+ processed.add(i);
1862
+
1863
+ for (let j = 0; j < positions.length; j++) {
1864
+ if (i === j || processed.has(j)) continue;
1865
+
1866
+ const distance = Math.sqrt(
1867
+ Math.pow(positions[i].x - positions[j].x, 2) +
1868
+ Math.pow(positions[i].y - positions[j].y, 2)
1869
+ );
1870
+
1871
+ if (distance < minDistance) {
1872
+ cluster.push(j);
1873
+ processed.add(j);
1874
+ }
1875
+ }
1876
+
1877
+ if (cluster.length > 1) {
1878
+ clusters.push(cluster);
1879
+ }
1880
+ }
1881
+
1882
+ return clusters;
1883
+ }
1884
+
1885
+ /**
1886
+ * Adjusts positions to resolve overlaps using improved clustering algorithm
1887
+ * @param {Array} positions - Array of position objects
1888
+ * @param {Object} options - Adjustment options
1889
+ * @returns {Array} Adjusted positions
1890
+ */
1891
+ static adjustOverlaps(positions, options = {}) {
1892
+ const {
1893
+ minDistance = 24,
1894
+ centerX,
1895
+ centerY,
1896
+ baseRadius,
1897
+ iconSize = 24
1898
+ } = options;
1899
+
1900
+ if (!positions || positions.length <= 1) {
1901
+ return positions; // Nothing to adjust with 0 or 1 planets
1902
+ }
1903
+
1904
+ if (!centerX || !centerY || !baseRadius) {
1905
+ console.error("PlanetPositionCalculator: Missing required parameters (centerX, centerY, or baseRadius)");
1906
+ return positions; // Return original positions if missing required parameters
1907
+ }
1908
+
1909
+ console.log(`PlanetPositionCalculator: Adjusting overlaps for ${positions.length} positions`);
1910
+
1911
+ // Make a copy to not modify originals
1912
+ const adjustedPositions = [...positions];
1913
+
1914
+ // The minimum angular distance needed to prevent overlap at base radius
1915
+ const minAngularDistance = (minDistance / baseRadius) * (180 / Math.PI);
1916
+ console.log(`PlanetPositionCalculator: Minimum angular distance: ${minAngularDistance.toFixed(2)}°`);
1917
+
1918
+ // Sort positions by longitude for overlap detection
1919
+ const sortedPositionIndices = adjustedPositions
1920
+ .map((pos, idx) => ({ pos, idx }))
1921
+ .sort((a, b) => a.pos.longitude - b.pos.longitude);
1922
+
1923
+ const sortedPositions = sortedPositionIndices.map(item => ({
1924
+ ...adjustedPositions[item.idx],
1925
+ originalIndex: item.idx
1926
+ }));
1927
+
1928
+ // Find clusters of planets that are too close angularly
1929
+ const clusters = this._findOverlappingClusters(sortedPositions, minAngularDistance);
1930
+ console.log(`PlanetPositionCalculator: Found ${clusters.length} clusters of overlapping positions`);
1931
+ clusters.forEach((cluster, i) => {
1932
+ console.log(`PlanetPositionCalculator: Cluster ${i+1} has ${cluster.length} positions`);
1933
+ });
1934
+
1935
+ // Process each cluster
1936
+ clusters.forEach((cluster, clusterIndex) => {
1937
+ console.log(`PlanetPositionCalculator: Processing cluster ${clusterIndex+1}`);
1938
+
1939
+ if (cluster.length <= 1) {
1940
+ // Single planet - just place at exact base radius with no angle change
1941
+ const planet = cluster[0];
1942
+ console.log(`PlanetPositionCalculator: Single position in cluster, keeping at original longitude ${planet.longitude.toFixed(2)}°`);
1943
+ this._setExactPosition(planet, planet.longitude, baseRadius, centerX, centerY, iconSize);
1944
+ } else {
1945
+ // Handle cluster with multiple planets - distribute by angle
1946
+ console.log(`PlanetPositionCalculator: Distributing ${cluster.length} positions in cluster`);
1947
+ this._distributeClusterByAngle(cluster, baseRadius, minAngularDistance, centerX, centerY, iconSize);
1948
+
1949
+ // Log the distributions
1950
+ cluster.forEach((pos, i) => {
1951
+ console.log(`PlanetPositionCalculator: Position ${i+1} in cluster ${clusterIndex+1} adjusted from ${pos.longitude.toFixed(2)}° to ${pos.adjustedLongitude.toFixed(2)}°`);
1952
+ });
1953
+ }
1954
+ });
1955
+
1956
+ // Copy adjusted positions back to the original array order
1957
+ sortedPositions.forEach(pos => {
1958
+ const origIndex = pos.originalIndex;
1959
+
1960
+ // Only copy if we have valid data
1961
+ if (origIndex !== undefined && origIndex >= 0 && origIndex < adjustedPositions.length) {
1962
+ adjustedPositions[origIndex].x = pos.x;
1963
+ adjustedPositions[origIndex].y = pos.y;
1964
+ adjustedPositions[origIndex].iconX = pos.iconX;
1965
+ adjustedPositions[origIndex].iconY = pos.iconY;
1966
+ adjustedPositions[origIndex].iconCenterX = pos.iconCenterX;
1967
+ adjustedPositions[origIndex].iconCenterY = pos.iconCenterY;
1968
+
1969
+ // Also add any adjusted longitude for reference
1970
+ if (pos.adjustedLongitude !== undefined) {
1971
+ adjustedPositions[origIndex].adjustedLongitude = pos.adjustedLongitude;
1972
+ }
1973
+ }
1974
+ });
1975
+
1976
+ return adjustedPositions;
1977
+ }
1978
+
1979
+ /**
1980
+ * Find clusters of positions that are too close angularly
1981
+ * @private
1982
+ * @param {Array} sortedPositions - Positions sorted by longitude
1983
+ * @param {number} minAngularDistance - Minimum angular separation needed
1984
+ * @returns {Array} Array of arrays containing positions in each cluster
1985
+ */
1986
+ static _findOverlappingClusters(sortedPositions, minAngularDistance) {
1987
+ if (!sortedPositions.length) return [];
1988
+ if (sortedPositions.length === 1) return [sortedPositions];
1989
+
1990
+ const clusters = [];
1991
+ let currentCluster = [sortedPositions[0]];
1992
+ const posCount = sortedPositions.length;
1993
+
1994
+ // Check for wrap-around at the edges (e.g., planet at 359° and another at 1°)
1995
+ // Add the first planet to the end of the array for checking wraparound
1996
+ [...sortedPositions];
1997
+ ({...sortedPositions[0], longitude: sortedPositions[0].longitude + 360});
1998
+
1999
+ // First identify standard clusters within the 0-360° range
2000
+ for (let i = 1; i < posCount; i++) {
2001
+ const prevPosition = sortedPositions[i-1];
2002
+ const currPosition = sortedPositions[i];
2003
+
2004
+ // Check angular distance, considering wrap-around at 360°
2005
+ let angleDiff = currPosition.longitude - prevPosition.longitude;
2006
+ if (angleDiff < 0) angleDiff += 360;
2007
+
2008
+ if (angleDiff < minAngularDistance) {
2009
+ // Too close - add to current cluster
2010
+ currentCluster.push(currPosition);
2011
+ } else {
2012
+ // Far enough - finish current cluster and start a new one
2013
+ if (currentCluster.length > 0) {
2014
+ clusters.push(currentCluster);
2015
+ }
2016
+ currentCluster = [currPosition];
2017
+ }
2018
+ }
2019
+
2020
+ // Add the final regular cluster if it exists
2021
+ if (currentCluster.length > 0) {
2022
+ clusters.push(currentCluster);
2023
+ }
2024
+
2025
+ // Check for a wrap-around cluster (where last and first planets are close)
2026
+ const lastPlanet = sortedPositions[posCount - 1];
2027
+ const firstPlanetOriginal = sortedPositions[0];
2028
+
2029
+ let wrapDiff = (firstPlanetOriginal.longitude + 360) - lastPlanet.longitude;
2030
+ if (wrapDiff < 0) wrapDiff += 360;
2031
+
2032
+ if (wrapDiff < minAngularDistance) {
2033
+ // We have a wraparound cluster
2034
+ // If first and last clusters both exist, merge them
2035
+ if (clusters.length >= 2) {
2036
+ const firstCluster = clusters[0];
2037
+ const lastCluster = clusters[clusters.length - 1];
2038
+
2039
+ // If first element is in first cluster and last element is in last cluster
2040
+ if (firstCluster.includes(firstPlanetOriginal) && lastCluster.includes(lastPlanet)) {
2041
+ // Merge the first and last clusters
2042
+ const mergedCluster = [...lastCluster, ...firstCluster];
2043
+ clusters.pop(); // Remove last cluster
2044
+ clusters[0] = mergedCluster; // Replace first cluster with merged
2045
+ }
2046
+ }
2047
+ }
2048
+
2049
+ return clusters;
2050
+ }
2051
+
2052
+ /**
2053
+ * Distribute positions in a cluster by adjusting only their angles
2054
+ * @private
2055
+ * @param {Array} positions - Array of positions in the cluster
2056
+ * @param {number} radius - The exact radius to place all positions
2057
+ * @param {number} minAngularDistance - Minimum angular distance needed
2058
+ * @param {number} centerX - X coordinate of center
2059
+ * @param {number} centerY - Y coordinate of center
2060
+ * @param {number} iconSize - Size of the icon
2061
+ */
2062
+ static _distributeClusterByAngle(positions, radius, minAngularDistance, centerX, centerY, iconSize) {
2063
+ const n = positions.length;
2064
+
2065
+ // Sort positions by their original longitude to maintain order
2066
+ positions.sort((a, b) => a.longitude - b.longitude);
2067
+
2068
+ // Calculate central angle and total span needed
2069
+ const firstPos = positions[0].longitude;
2070
+ const lastPos = positions[n-1].longitude;
2071
+ let totalArc = lastPos - firstPos;
2072
+
2073
+ // Handle wrap-around case (e.g., positions at 350° and 10°)
2074
+ if (totalArc < 0 || totalArc > 180) {
2075
+ totalArc = (360 + lastPos - firstPos) % 360;
2076
+ }
2077
+
2078
+ // Calculate the center of the cluster
2079
+ let centerAngle = (firstPos + totalArc/2) % 360;
2080
+
2081
+ // Determine minimum arc needed for n planets with minimum spacing
2082
+ const minRequiredArc = (n - 1) * minAngularDistance;
2083
+
2084
+ // Calculate total span to use (either natural spacing or minimum required)
2085
+ const spanToUse = Math.max(totalArc, minRequiredArc);
2086
+
2087
+ // Calculate start angle (center - half of span)
2088
+ const startAngle = (centerAngle - spanToUse/2 + 360) % 360;
2089
+
2090
+ // Distribute planets evenly from the start angle
2091
+ for (let i = 0; i < n; i++) {
2092
+ // If only one planet, keep its original position
2093
+ if (n === 1) {
2094
+ this._setExactPosition(positions[i], positions[i].longitude, radius, centerX, centerY, iconSize);
2095
+ continue;
2096
+ }
2097
+
2098
+ const angle = (startAngle + i * (spanToUse / (n-1))) % 360;
2099
+ this._setExactPosition(positions[i], angle, radius, centerX, centerY, iconSize);
2100
+ }
2101
+ }
2102
+
2103
+ /**
2104
+ * Set a position's exact coordinates at the given angle and radius
2105
+ * @private
2106
+ * @param {Object} position - The position object to update
2107
+ * @param {number} angle - The angle in degrees (0-360)
2108
+ * @param {number} radius - The exact radius to place the position
2109
+ * @param {number} centerX - X coordinate of center
2110
+ * @param {number} centerY - Y coordinate of center
2111
+ * @param {number} iconSize - Size of the icon
2112
+ */
2113
+ static _setExactPosition(position, angle, radius, centerX, centerY, iconSize) {
2114
+ const svgUtils = new SvgUtils();
2115
+ const point = svgUtils.pointOnCircle(centerX, centerY, radius, angle);
2116
+
2117
+ position.x = point.x;
2118
+ position.y = point.y;
2119
+ position.iconCenterX = point.x;
2120
+ position.iconCenterY = point.y;
2121
+ position.iconX = point.x - (iconSize / 2);
2122
+ position.iconY = point.y - (iconSize / 2);
2123
+ position.adjustedLongitude = angle;
2124
+ }
2125
+ }
2126
+
2127
+ /**
2128
+ * PrimaryPlanetRenderer.js
2129
+ * Class for rendering primary planets on the inner circle of the chart.
2130
+ */
2131
+ class PrimaryPlanetRenderer extends BasePlanetRenderer {
2132
+ /**
2133
+ * Constructor
2134
+ * @param {Object} options - Renderer options.
2135
+ * @param {PlanetSymbolRenderer} options.symbolRenderer - The symbol renderer to use.
2136
+ */
2137
+ constructor(options) {
2138
+ super(options);
2139
+
2140
+ this.symbolRenderer = options.symbolRenderer;
2141
+
2142
+ // Define the circle configuration for primary planets
2143
+ this.circleConfig = {
2144
+ circle: 'inner'
2145
+ };
2146
+ }
2147
+
2148
+ /**
2149
+ * Calculates actual radius values based on circle configuration and chart dimensions
2150
+ * @param {Object} config - Chart configuration with radius values
2151
+ * @returns {Object} Object with calculated dotRadius and iconRadius
2152
+ */
2153
+ calculateRadii(config = null) {
2154
+ const baseRadius = config?.radius?.inner || this.innerRadius;
2155
+ const nextRadius = config?.radius?.middle || this.middleRadius;
2156
+
2157
+ // Fixed relationship between circle and dots/icons
2158
+ // - Dots are placed exactly on the circle
2159
+ // - Icons are placed halfway between this circle and the next
2160
+ const dotRadius = baseRadius;
2161
+ const iconRadius = baseRadius + (nextRadius - baseRadius) / 2;
2162
+
2163
+ return { dotRadius, iconRadius };
2164
+ }
2165
+
2166
+ /**
2167
+ * Adjusts the positions of overlapping planets.
2168
+ * @param {Array} planets - Array of planet objects.
2169
+ */
2170
+ adjustOverlappingPlanets(planets) {
2171
+ if (planets.length <= 1) return; // Nothing to adjust with 0 or 1 planets
2172
+
2173
+ // Define parameters for collision detection and distribution
2174
+ const iconSize = 24;
2175
+ const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
2176
+ const minDistance = iconSize * 1.2;
2177
+
2178
+ // Prepare planets array in format expected by PlanetPositionCalculator
2179
+ const positions = planets.map((planet, index) => ({
2180
+ x: planet.iconX,
2181
+ y: planet.iconY,
2182
+ iconX: planet.iconX,
2183
+ iconY: planet.iconY,
2184
+ iconCenterX: planet.iconX,
2185
+ iconCenterY: planet.iconY,
2186
+ longitude: planet.position,
2187
+ radius: baseRadius,
2188
+ originalIndex: index,
2189
+ name: planet.name
2190
+ }));
2191
+
2192
+ // Use the PlanetPositionCalculator for overlap adjustment
2193
+ const adjustedPositions = PlanetPositionCalculator.adjustOverlaps(positions, {
2194
+ minDistance: minDistance,
2195
+ centerX: this.centerX,
2196
+ centerY: this.centerY,
2197
+ baseRadius: baseRadius,
2198
+ iconSize: iconSize
2199
+ });
2200
+
2201
+ // Apply the adjusted positions back to the planets
2202
+ adjustedPositions.forEach((pos, idx) => {
2203
+ const planet = planets[idx];
2204
+ planet.position;
2205
+
2206
+ // Set the adjusted icon position
2207
+ planet.adjustedIconX = pos.iconCenterX;
2208
+ planet.adjustedIconY = pos.iconCenterY;
2209
+
2210
+ // If there was an adjustment, store it
2211
+ if (pos.adjustedLongitude !== undefined) {
2212
+ planet.adjustedPosition = pos.adjustedLongitude;
2213
+ }
2214
+ });
2215
+ }
2216
+
2217
+ /**
2218
+ * Renders primary planets on the chart.
2219
+ * @param {Element} parentGroup - The parent SVG group element.
2220
+ * @param {Array} planetsData - Array of planet objects { name: string, position: number }.
2221
+ * @param {number} houseRotationAngle - House system rotation angle.
2222
+ * @param {Object} options - Additional rendering options.
2223
+ * @returns {Array} Array of planet objects with added coordinates.
2224
+ */
2225
+ render(parentGroup, planetsData, houseRotationAngle = 0, options = {}) {
2226
+ if (!parentGroup) {
2227
+ console.error("PrimaryPlanetRenderer: parentGroup is null or undefined.");
2228
+ return [];
2229
+ }
2230
+
2231
+ // Calculate radii based on circle configuration
2232
+ const { dotRadius, iconRadius } = options.dotRadius && options.iconRadius
2233
+ ? { dotRadius: options.dotRadius, iconRadius: options.iconRadius }
2234
+ : this.calculateRadii(options.config || this.config);
2235
+
2236
+ if (!dotRadius || !iconRadius) {
2237
+ console.error("PrimaryPlanetRenderer: Could not determine radius values");
2238
+ return [];
2239
+ }
2240
+
2241
+ // Clear the group before rendering new planets
2242
+ this.clearGroup(parentGroup);
2243
+
2244
+ // Prepare planets with additional properties
2245
+ const planets = this.preparePlanetData(planetsData, 'primary', dotRadius, iconRadius);
2246
+
2247
+ // Sort planets by position for overlap calculations
2248
+ planets.sort((a, b) => a.position - b.position);
2249
+
2250
+ // Calculate base positions (dot and icon)
2251
+ this.calculateBasePositions(planets, dotRadius, iconRadius);
2252
+
2253
+ // Calculate adjustments for overlapping planets
2254
+ this.adjustOverlappingPlanets(planets);
2255
+
2256
+ // Draw the planets onto the SVG group
2257
+ this.drawPlanets(parentGroup, planets);
2258
+
2259
+ // Return the planets array with calculated coordinates
2260
+ return planets;
2261
+ }
2262
+
2263
+ /**
2264
+ * Draw the planets on the SVG group with their dots and icons.
2265
+ * @param {Element} parentGroup - The parent SVG group.
2266
+ * @param {Array} planets - Array of planet objects with calculated positions.
2267
+ */
2268
+ drawPlanets(parentGroup, planets) {
2269
+ const iconSize = 24; // Standard icon size
2270
+
2271
+ planets.forEach(planet => {
2272
+ // Create group for this planet (contains dot, symbol, and connector)
2273
+ const planetGroup = this.svgUtils.createSVGElement("g", {
2274
+ 'data-planet': planet.name,
2275
+ 'data-type': planet.type,
2276
+ class: `planet-element planet-${planet.name} planet-primary`,
2277
+ transform: `translate(0,0)`
2278
+ });
2279
+
2280
+ // Render planet dot
2281
+ const dot = this.symbolRenderer.renderPlanetDot(planetGroup, planet);
2282
+ planetGroup.appendChild(dot);
2283
+
2284
+ // Render planet symbol
2285
+ const icon = this.symbolRenderer.renderPlanetSymbol(planetGroup, planet, iconSize);
2286
+ planetGroup.appendChild(icon);
2287
+
2288
+ // Add tooltip
2289
+ this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
2290
+
2291
+ // Render connector if needed
2292
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
2293
+ if (connector) {
2294
+ planetGroup.appendChild(connector);
2295
+ }
2296
+
2297
+ // Add to parent group
2298
+ parentGroup.appendChild(planetGroup);
2299
+ });
2300
+ }
2301
+ }
2302
+
2303
+ /**
2304
+ * SecondaryPlanetRenderer.js
2305
+ * Class for rendering secondary planets on the innermost circle of the chart.
2306
+ */
2307
+ class SecondaryPlanetRenderer extends BasePlanetRenderer {
2308
+ /**
2309
+ * Constructor
2310
+ * @param {Object} options - Renderer options.
2311
+ * @param {PlanetSymbolRenderer} options.symbolRenderer - The symbol renderer to use.
2312
+ */
2313
+ constructor(options) {
2314
+ super(options);
2315
+
2316
+ this.symbolRenderer = options.symbolRenderer;
2317
+
2318
+ // Define the circle configuration for secondary planets
2319
+ this.circleConfig = {
2320
+ circle: 'innermost'
2321
+ };
2322
+ }
2323
+
2324
+ /**
2325
+ * Calculates actual radius values based on circle configuration and chart dimensions
2326
+ * @param {Object} config - Chart configuration with radius values
2327
+ * @returns {Object} Object with calculated dotRadius and iconRadius
2328
+ */
2329
+ calculateRadii(config = null) {
2330
+ const baseRadius = config?.radius?.innermost || (this.innerRadius * 0.5); // Fallback
2331
+ const nextRadius = config?.radius?.zodiacInner || this.innerRadius;
2332
+
2333
+ // Fixed relationship between circle and dots/icons
2334
+ // - Dots are placed exactly on the circle
2335
+ // - Icons are placed halfway between this circle and the next
2336
+ const dotRadius = baseRadius;
2337
+ const iconRadius = baseRadius + (nextRadius - baseRadius) / 2;
2338
+
2339
+ return { dotRadius, iconRadius };
2340
+ }
2341
+
2342
+ /**
2343
+ * Adjusts the positions of overlapping planets.
2344
+ * @param {Array} planets - Array of planet objects.
2345
+ */
2346
+ adjustOverlappingPlanets(planets) {
2347
+ if (planets.length <= 1) return; // Nothing to adjust with 0 or 1 planets
2348
+
2349
+ // Define parameters for collision detection and distribution
2350
+ const iconSize = 18; // Smaller size for secondary planets
2351
+ const baseRadius = planets[0].iconRadius; // Use the iconRadius from the first planet
2352
+ const minDistance = iconSize * 1.1; // Slightly tighter packing
2353
+
2354
+ // Prepare planets array in format expected by PlanetPositionCalculator
2355
+ const positions = planets.map((planet, index) => ({
2356
+ x: planet.iconX,
2357
+ y: planet.iconY,
2358
+ iconX: planet.iconX,
2359
+ iconY: planet.iconY,
2360
+ iconCenterX: planet.iconX,
2361
+ iconCenterY: planet.iconY,
2362
+ longitude: planet.position,
2363
+ radius: baseRadius,
2364
+ originalIndex: index,
2365
+ name: planet.name
2366
+ }));
2367
+
2368
+ // Use the PlanetPositionCalculator for overlap adjustment
2369
+ const adjustedPositions = PlanetPositionCalculator.adjustOverlaps(positions, {
2370
+ minDistance: minDistance,
2371
+ centerX: this.centerX,
2372
+ centerY: this.centerY,
2373
+ baseRadius: baseRadius,
2374
+ iconSize: iconSize
2375
+ });
2376
+
2377
+ // Apply the adjusted positions back to the planets
2378
+ adjustedPositions.forEach((pos, idx) => {
2379
+ const planet = planets[idx];
2380
+
2381
+ // Set the adjusted icon position
2382
+ planet.adjustedIconX = pos.iconCenterX;
2383
+ planet.adjustedIconY = pos.iconCenterY;
2384
+
2385
+ // If there was an adjustment, store it
2386
+ if (pos.adjustedLongitude !== undefined) {
2387
+ planet.adjustedPosition = pos.adjustedLongitude;
2388
+ }
2389
+ });
2390
+ }
2391
+
2392
+ /**
2393
+ * Renders secondary planets on the chart.
2394
+ * @param {Element} parentGroup - The parent SVG group element.
2395
+ * @param {Array} planetsData - Array of planet objects { name: string, position: number }.
2396
+ * @param {number} houseRotationAngle - House system rotation angle.
2397
+ * @param {Object} options - Additional rendering options.
2398
+ * @returns {Array} Array of planet objects with added coordinates.
2399
+ */
2400
+ render(parentGroup, planetsData, houseRotationAngle = 0, options = {}) {
2401
+ if (!parentGroup) {
2402
+ console.error("SecondaryPlanetRenderer: parentGroup is null or undefined.");
2403
+ return [];
2404
+ }
2405
+
2406
+ // Calculate radii based on circle configuration
2407
+ const { dotRadius, iconRadius } = options.dotRadius && options.iconRadius
2408
+ ? { dotRadius: options.dotRadius, iconRadius: options.iconRadius }
2409
+ : this.calculateRadii(options.config || this.config);
2410
+
2411
+ if (!dotRadius || !iconRadius) {
2412
+ console.error("SecondaryPlanetRenderer: Could not determine radius values");
2413
+ return [];
2414
+ }
2415
+
2416
+ // Clear the group before rendering new planets
2417
+ this.clearGroup(parentGroup);
2418
+
2419
+ // Prepare planets with additional properties
2420
+ const planets = this.preparePlanetData(planetsData, 'secondary', dotRadius, iconRadius);
2421
+
2422
+ // Sort planets by position for overlap calculations
2423
+ planets.sort((a, b) => a.position - b.position);
2424
+
2425
+ // Calculate base positions (dot and icon)
2426
+ this.calculateBasePositions(planets, dotRadius, iconRadius);
2427
+
2428
+ // Calculate adjustments for overlapping planets
2429
+ this.adjustOverlappingPlanets(planets);
2430
+
2431
+ // Draw the planets onto the SVG group
2432
+ this.drawPlanets(parentGroup, planets);
2433
+
2434
+ // Return the planets array with calculated coordinates
2435
+ return planets;
2436
+ }
2437
+
2438
+ /**
2439
+ * Draw the planets on the SVG group with their dots and icons.
2440
+ * @param {Element} parentGroup - The parent SVG group.
2441
+ * @param {Array} planets - Array of planet objects with calculated positions.
2442
+ */
2443
+ drawPlanets(parentGroup, planets) {
2444
+ const iconSize = 18; // Smaller icon size for secondary planets
2445
+
2446
+ planets.forEach(planet => {
2447
+ // Create group for this planet (contains dot, symbol, and connector)
2448
+ const planetGroup = this.svgUtils.createSVGElement("g", {
2449
+ 'data-planet': planet.name,
2450
+ 'data-type': planet.type,
2451
+ class: `planet-element planet-${planet.name} planet-secondary`,
2452
+ transform: `translate(0,0)`
2453
+ });
2454
+
2455
+ // Render planet dot with smaller size
2456
+ const dot = this.symbolRenderer.renderPlanetDot(planetGroup, planet, 2);
2457
+ planetGroup.appendChild(dot);
2458
+
2459
+ // Render planet symbol with smaller size
2460
+ const icon = this.symbolRenderer.renderPlanetSymbol(planetGroup, planet, iconSize);
2461
+ planetGroup.appendChild(icon);
2462
+
2463
+ // Add tooltip
2464
+ this.symbolRenderer.addPlanetTooltip(planetGroup, planet);
2465
+
2466
+ // Render connector if needed
2467
+ const connector = this.symbolRenderer.renderConnector(planetGroup, planet, iconSize);
2468
+ if (connector) {
2469
+ planetGroup.appendChild(connector);
2470
+ }
2471
+
2472
+ // Add to parent group
2473
+ parentGroup.appendChild(planetGroup);
2474
+ });
2475
+ }
2476
+ }
2477
+
2478
+ /**
2479
+ * PlanetRendererCoordinator.js
2480
+ * Coordinator class that maintains the original PlanetRenderer API
2481
+ * but delegates rendering tasks to specialized renderers.
2482
+ */
2483
+ class PlanetRendererCoordinator extends BasePlanetRenderer {
2484
+ /**
2485
+ * Constructor
2486
+ * @param {Object} options - Renderer options.
2487
+ * @param {PrimaryPlanetRenderer} options.primaryRenderer - Primary planet renderer.
2488
+ * @param {SecondaryPlanetRenderer} options.secondaryRenderer - Secondary planet renderer.
2489
+ * @param {PlanetSymbolRenderer} options.symbolRenderer - Planet symbol renderer.
2490
+ */
2491
+ constructor(options) {
2492
+ super(options);
2493
+
2494
+ this.primaryRenderer = options.primaryRenderer;
2495
+ this.secondaryRenderer = options.secondaryRenderer;
2496
+ this.symbolRenderer = options.symbolRenderer;
2497
+
2498
+ // Define the circle configurations - which circle each planet type should be placed on
2499
+ this.circleConfigs = {
2500
+ // Primary planets placed on inner circle
2501
+ primary: {
2502
+ circle: 'inner'
2503
+ },
2504
+ // Secondary planets placed on innermost circle
2505
+ secondary: {
2506
+ circle: 'innermost'
2507
+ }
2508
+ };
2509
+ }
2510
+
2511
+ /**
2512
+ * Calculates actual radius values based on circle configuration and chart dimensions
2513
+ * @param {string} planetType - Type of planet (primary, secondary, etc.)
2514
+ * @param {Object} config - Chart configuration with radius values
2515
+ * @returns {Object} Object with calculated dotRadius and iconRadius
2516
+ */
2517
+ calculateRadii(planetType, config = null) {
2518
+ const circleConfig = this.circleConfigs[planetType];
2519
+ if (!circleConfig) {
2520
+ console.error(`PlanetRendererCoordinator: No circle configuration found for planet type: ${planetType}`);
2521
+ return { dotRadius: null, iconRadius: null };
2522
+ }
2523
+
2524
+ switch (planetType) {
2525
+ case 'primary':
2526
+ return this.primaryRenderer.calculateRadii(config);
2527
+ case 'secondary':
2528
+ return this.secondaryRenderer.calculateRadii(config);
2529
+ default:
2530
+ console.error(`PlanetRendererCoordinator: Unknown planet type: ${planetType}`);
2531
+ return { dotRadius: null, iconRadius: null };
2532
+ }
2533
+ }
2534
+
2535
+ /**
2536
+ * Renders planets on the chart. Delegates to the appropriate specialized renderer.
2537
+ * @param {Element} parentGroup - The parent SVG group element.
2538
+ * @param {Array} planetsData - Array of planet objects { name: string, position: number }.
2539
+ * @param {number} houseRotationAngle - House system rotation angle.
2540
+ * @param {Object} options - Additional rendering options.
2541
+ * @returns {Array} Array of planet objects with added coordinates.
2542
+ */
2543
+ render(parentGroup, planetsData, houseRotationAngle = 0, options = {}) {
2544
+ // Determine planet type (primary or secondary)
2545
+ const planetType = options.type || 'primary';
2546
+
2547
+ console.log(`PlanetRendererCoordinator: Rendering ${planetType} planets, count:`, planetsData.length);
2548
+
2549
+ // Delegate to appropriate renderer based on planet type
2550
+ switch (planetType) {
2551
+ case 'primary':
2552
+ return this.primaryRenderer.render(parentGroup, planetsData, houseRotationAngle, options);
2553
+ case 'secondary':
2554
+ return this.secondaryRenderer.render(parentGroup, planetsData, houseRotationAngle, options);
2555
+ default:
2556
+ console.error(`PlanetRendererCoordinator: Unknown planet type: ${planetType}`);
2557
+ return [];
2558
+ }
2559
+ }
2560
+
2561
+ /**
2562
+ * Renders both primary and secondary planets in one call.
2563
+ * This consolidated method handles rendering all planets.
2564
+ *
2565
+ * @param {Object} params - Rendering parameters
2566
+ * @param {SVGManager} params.svgManager - SVG manager instance
2567
+ * @param {Object} params.planetsData - Planet data object
2568
+ * @param {Object} params.config - Chart configuration with radii
2569
+ * @param {boolean} params.primaryEnabled - Whether primary planets are enabled
2570
+ * @param {boolean} params.secondaryEnabled - Whether secondary planets are enabled
2571
+ * @returns {Object} Object containing both sets of rendered planets
2572
+ */
2573
+ renderAllPlanetTypes(params) {
2574
+ const {
2575
+ svgManager,
2576
+ planetsData,
2577
+ config,
2578
+ enabledTypes = { primary: true, secondary: true }
2579
+ } = params;
2580
+
2581
+ if (!svgManager || !planetsData || !config) {
2582
+ console.error("PlanetRendererCoordinator: Missing required parameters");
2583
+ return {};
2584
+ }
2585
+
2586
+ // Convert planet data to array format, filtering by visibility
2587
+ const planetsArray = Object.entries(planetsData)
2588
+ .filter(([name, data]) => config.planetSettings.visible?.[name] !== false)
2589
+ .map(([name, data]) => ({
2590
+ name: name,
2591
+ position: data.lon,
2592
+ color: data.color || '#000000'
2593
+ }));
2594
+
2595
+ const result = {};
2596
+
2597
+ // Save and update center coordinates
2598
+ const originalCenterX = this.centerX;
2599
+ const originalCenterY = this.centerY;
2600
+ this.centerX = config.svg.center.x;
2601
+ this.centerY = config.svg.center.y;
2602
+
2603
+ // Update center coordinates in specialized renderers too
2604
+ this.primaryRenderer.centerX = this.centerX;
2605
+ this.primaryRenderer.centerY = this.centerY;
2606
+ this.secondaryRenderer.centerX = this.centerX;
2607
+ this.secondaryRenderer.centerY = this.centerY;
2608
+
2609
+ // Render primary planets if enabled
2610
+ if (enabledTypes.primary) {
2611
+ const primaryGroup = svgManager.getGroup('primaryPlanets');
2612
+ result.primary = this.primaryRenderer.render(primaryGroup, planetsArray, 0, {
2613
+ config: config
2614
+ });
2615
+ console.log(`PlanetRendererCoordinator: Rendered ${result.primary.length} primary planets`);
2616
+ } else {
2617
+ result.primary = [];
2618
+ }
2619
+
2620
+ // Render secondary planets if enabled
2621
+ if (enabledTypes.secondary) {
2622
+ const secondaryGroup = svgManager.getGroup('secondaryPlanets');
2623
+ result.secondary = this.secondaryRenderer.render(secondaryGroup, planetsArray, 0, {
2624
+ config: config
2625
+ });
2626
+ console.log(`PlanetRendererCoordinator: Rendered ${result.secondary.length} secondary planets`);
2627
+ } else {
2628
+ result.secondary = [];
2629
+ }
2630
+
2631
+ // Restore original center values
2632
+ this.centerX = originalCenterX;
2633
+ this.centerY = originalCenterY;
2634
+ this.primaryRenderer.centerX = originalCenterX;
2635
+ this.primaryRenderer.centerY = originalCenterY;
2636
+ this.secondaryRenderer.centerX = originalCenterX;
2637
+ this.secondaryRenderer.centerY = originalCenterY;
2638
+
2639
+ return result;
2640
+ }
2641
+
2642
+ /**
2643
+ * Legacy method - deprecated, use renderAllPlanetTypes instead
2644
+ */
2645
+ renderAllPlanets(params) {
2646
+ console.warn("PlanetRendererCoordinator: renderAllPlanets is deprecated, use renderAllPlanetTypes instead");
2647
+ return this.renderAllPlanetTypes(params);
2648
+ }
2649
+ }
2650
+
2651
+ /**
2652
+ * ClientSideAspectRenderer.js
2653
+ * Renders aspects based on client-side calculations using planet coordinates.
2654
+ */
2655
+ class ClientSideAspectRenderer extends BaseRenderer { // No longer extends IAspectRenderer
2656
+ /**
2657
+ * Constructor
2658
+ * @param {Object} options - Renderer options.
2659
+ * @param {string} options.svgNS - SVG namespace.
2660
+ * @param {ChartConfig} options.config - Chart configuration object. Contains aspect settings like orb.
2661
+ * @param {string} options.assetBasePath - Base path for assets (unused here but standard).
2662
+ * @param {IconProvider} [options.iconProvider] - Icon provider service.
2663
+ */
2664
+ constructor(options) {
2665
+ super(options);
2666
+ this.astrologyUtils = AstrologyUtils; // Assuming AstrologyUtils is available
2667
+ this.renderedAspects = []; // Store calculated aspects
2668
+ this._aspectCacheKey = null; // Cache key for aspect calculations
2669
+ this._aspectCache = []; // Cached aspect results
2670
+ this.assetBasePath = options.assetBasePath || ''; // Store asset base path
2671
+ this.iconProvider = options.iconProvider; // Store the icon provider
2672
+
2673
+ // Define major aspects and their angles (can be overridden/extended by config)
2674
+ this.defaultAspectDefinitions = {
2675
+ 'conjunction': { angle: 0, orb: 8, color: '#FF4500', abbr: 'CON' }, // OrangeRed
2676
+ 'opposition': { angle: 180, orb: 6, color: '#DC143C', abbr: 'OPP' }, // Crimson
2677
+ 'trine': { angle: 120, orb: 6, color: '#2E8B57', abbr: 'TRI' }, // SeaGreen
2678
+ 'square': { angle: 90, orb: 6, color: '#FF0000', abbr: 'SQR' }, // Red
2679
+ 'sextile': { angle: 60, orb: 4, color: '#4682B4', abbr: 'SEX' }, // SteelBlue
2680
+ // Add minors if needed
2681
+ // 'semisextile': { angle: 30, orb: 2, color: '#90EE90', abbr: 'SSX' }, // LightGreen
2682
+ // 'quincunx': { angle: 150, orb: 2, color: '#DAA520', abbr: 'QCX' } // Goldenrod
2683
+ };
2684
+ }
2685
+
2686
+ /**
2687
+ * Calculates the angular distance between two positions (0-180 degrees).
2688
+ * @param {number} pos1 - Position 1 (degrees).
2689
+ * @param {number} pos2 - Position 2 (degrees).
2690
+ * @returns {number} The smallest angle between pos1 and pos2 (0-180).
2691
+ */
2692
+ _angularDistance(pos1, pos2) {
2693
+ const diff = Math.abs(pos1 - pos2) % 360;
2694
+ return Math.min(diff, 360 - diff);
2695
+ }
2696
+
2697
+ /**
2698
+ * Calculates aspects between planets based on their positions.
2699
+ * @param {Array} planets - Array of planet objects MUST include `position` property.
2700
+ * @returns {Array} Array of calculated aspect objects.
2701
+ */
2702
+ calculateAspects(planets) {
2703
+ const aspects = [];
2704
+ if (!planets || planets.length < 2) {
2705
+ return aspects;
2706
+ }
2707
+
2708
+ // Generate a cache key based on aspectSettings and planet positions
2709
+ const settingsString = JSON.stringify(this.config.aspectSettings);
2710
+ const planetKey = planets.map(p => `${p.name}:${p.position}`).join('|');
2711
+ const cacheKey = `${settingsString}|${planetKey}`;
2712
+ if (cacheKey === this._aspectCacheKey) {
2713
+ console.log(`ClientSideAspectRenderer: Using cached aspects (${this._aspectCache.length})`);
2714
+ return this._aspectCache;
2715
+ }
2716
+
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;
2721
+
2722
+ // Iterate through all unique pairs of planets
2723
+ for (let i = 0; i < planets.length; i++) {
2724
+ for (let j = i + 1; j < planets.length; j++) {
2725
+ const p1 = planets[i];
2726
+ const p2 = planets[j];
2727
+
2728
+ const angleDiff = this._angularDistance(p1.position, p2.position);
2729
+
2730
+ // Check against each defined aspect type
2731
+ for (const aspectName in aspectTypes) {
2732
+ const aspectDef = aspectTypes[aspectName];
2733
+ const targetAngle = aspectDef.angle;
2734
+ const orb = aspectDef.orb !== undefined ? aspectDef.orb : calculationOrb; // Use specific orb or default
2735
+
2736
+ if (Math.abs(angleDiff - targetAngle) <= orb) {
2737
+ // Aspect found!
2738
+ aspects.push({
2739
+ planet1: p1.name,
2740
+ planet2: p2.name,
2741
+ type: aspectName,
2742
+ angle: targetAngle, // The ideal angle of the aspect type
2743
+ angleDiff: angleDiff, // The actual angle difference between planets
2744
+ orb: Math.abs(angleDiff - targetAngle), // How exact the aspect is
2745
+ // Include planet objects for coordinate lookup during rendering
2746
+ p1: p1,
2747
+ p2: p2,
2748
+ // Add default color/style from definition
2749
+ color: aspectDef.color || '#888', // Default color
2750
+ lineStyle: aspectDef.lineStyle, // e.g., 'dashed', 'dotted' (used later in styling)
2751
+ abbr: aspectDef.abbr || aspectName.substring(0, 3).toUpperCase() // Use abbreviation from definition or default
2752
+ });
2753
+ }
2754
+ }
2755
+ }
2756
+ }
2757
+ console.log(`ClientSideAspectRenderer: Calculated ${aspects.length} aspects.`);
2758
+ // Cache results and key
2759
+ this._aspectCacheKey = cacheKey;
2760
+ this._aspectCache = aspects;
2761
+ return aspects;
2762
+ }
2763
+
2764
+
2765
+ /**
2766
+ * Renders aspect lines based on planet coordinates.
2767
+ * @param {Element} parentGroup - The parent SVG group for aspect lines.
2768
+ * @param {Array} planetsWithCoords - Array of planet objects returned by PlanetRenderer, MUST include `x`, `y`, `name`, and `position`.
2769
+ * @returns {Array<Element>} Array containing the created line elements.
2770
+ */
2771
+ render(parentGroup, planetsWithCoords) {
2772
+ if (!parentGroup) {
2773
+ console.error("ClientSideAspectRenderer.render: parentGroup is null or undefined.");
2774
+ return [];
2775
+ }
2776
+ this.clearGroup(parentGroup); // Clear previous aspects
2777
+ const renderedElements = [];
2778
+
2779
+ if (!planetsWithCoords || planetsWithCoords.length < 2) {
2780
+ console.warn("ClientSideAspectRenderer: Not enough planet data with coordinates to render aspects.");
2781
+ this.renderedAspects = [];
2782
+ return [];
2783
+ }
2784
+
2785
+ // Calculate aspects based on planet positions
2786
+ const aspects = this.calculateAspects(planetsWithCoords);
2787
+ this.renderedAspects = aspects; // Store the calculated aspects
2788
+
2789
+ console.log(`ClientSideAspectRenderer: Rendering ${aspects.length} aspects.`);
2790
+
2791
+ // Get enabled aspect types from config
2792
+ const aspectSettings = this.config.aspectSettings || {};
2793
+ const aspectTypesConfig = aspectSettings.types || {};
2794
+
2795
+ // Map planets by name for quick coordinate lookup
2796
+ const planetCoords = {};
2797
+ planetsWithCoords.forEach(p => {
2798
+ planetCoords[p.name] = { x: p.x, y: p.y }; // Use the dot coordinates (p.x, p.y)
2799
+ });
2800
+
2801
+ aspects.forEach(aspect => {
2802
+ const coords1 = planetCoords[aspect.planet1];
2803
+ const coords2 = planetCoords[aspect.planet2];
2804
+
2805
+ // Check if aspect type is enabled in config (and handle default case)
2806
+ const aspectDef = aspectTypesConfig[aspect.type];
2807
+ const isEnabled = aspectDef ? (aspectDef.enabled !== false) : true; // Default to true if not specified
2808
+ const lineStyle = aspectDef ? aspectDef.lineStyle : 'solid'; // Get style from config or default to solid
2809
+
2810
+ if (!isEnabled || lineStyle === 'none') {
2811
+ // If aspect type is disabled or style is none, don't render line or icon
2812
+ return;
2813
+ }
2814
+
2815
+ if (!coords1 || !coords2) {
2816
+ console.warn(`ClientSideAspectRenderer: Could not find coordinates for aspect: ${aspect.planet1} ${aspect.type} ${aspect.planet2}`);
2817
+ return; // Skip this aspect if coordinates are missing
2818
+ }
2819
+
2820
+ // Sanitize planet names for CSS classes
2821
+ const p1SafeName = (aspect.planet1 || '').toLowerCase().replace(/[^a-z0-9]/g, '-');
2822
+ const p2SafeName = (aspect.planet2 || '').toLowerCase().replace(/[^a-z0-9]/g, '-');
2823
+
2824
+ // Define stroke dash array based on lineStyle
2825
+ let strokeDasharray = 'none';
2826
+ if (lineStyle === 'dashed') {
2827
+ strokeDasharray = '5, 5';
2828
+ } else if (lineStyle === 'dotted') {
2829
+ strokeDasharray = '1, 3';
2830
+ }
2831
+
2832
+ const line = this.svgUtils.createSVGElement("line", {
2833
+ x1: coords1.x,
2834
+ y1: coords1.y,
2835
+ x2: coords2.x,
2836
+ y2: coords2.y,
2837
+ class: `aspect-element aspect-line aspect-${aspect.type} aspect-planet-${p1SafeName} aspect-planet-${p2SafeName}`, // Add classes for type and involved planets
2838
+ stroke: aspect.color || '#888888', // Apply color from definition or default
2839
+ 'stroke-dasharray': strokeDasharray // Apply calculated dash style
2840
+ });
2841
+
2842
+ // Add tooltip with details
2843
+ const tooltipText = `${this.astrologyUtils.capitalizeFirstLetter(aspect.planet1)} ${aspect.type} ${this.astrologyUtils.capitalizeFirstLetter(aspect.planet2)} (${aspect.angleDiff.toFixed(1)}°, orb ${aspect.orb.toFixed(1)}°)`;
2844
+ this.svgUtils.addTooltip(line, tooltipText);
2845
+
2846
+ parentGroup.appendChild(line);
2847
+ renderedElements.push(line);
2848
+
2849
+ // Optionally add aspect glyphs at the midpoint
2850
+ this._addAspectIcon(parentGroup, aspect, coords1, coords2, tooltipText);
2851
+ });
2852
+
2853
+ return renderedElements;
2854
+ }
2855
+
2856
+ /**
2857
+ * Overrides the BaseRenderer clearGroup method to ensure custom cleanup.
2858
+ * @param {Element} parentGroup - The parent SVG group to clear.
2859
+ */
2860
+ clearGroup(parentGroup) {
2861
+ super.clearGroup(parentGroup);
2862
+ // Additional cleanup here if needed
2863
+ }
2864
+
2865
+ /**
2866
+ * Gets the currently rendered aspects
2867
+ * @returns {Array} Array of aspect objects
2868
+ */
2869
+ getCurrentAspects() {
2870
+ return this.renderedAspects;
2871
+ }
2872
+
2873
+ /**
2874
+ * Adds an aspect icon at the midpoint of the aspect line
2875
+ * @private
2876
+ * @param {Element} parentGroup - The SVG group for aspect lines
2877
+ * @param {Object} aspect - The aspect object
2878
+ * @param {Object} coords1 - Coordinates of first planet
2879
+ * @param {Object} coords2 - Coordinates of second planet
2880
+ * @param {string} tooltipText - Tooltip text
2881
+ */
2882
+ _addAspectIcon(parentGroup, aspect, coords1, coords2, tooltipText) {
2883
+ // Calculate midpoint of the line
2884
+ const midX = (coords1.x + coords2.x) / 2;
2885
+ const midY = (coords1.y + coords2.y) / 2;
2886
+
2887
+ // Calculate distance from center to place the icon correctly
2888
+ const dx = midX - this.centerX;
2889
+ const dy = midY - this.centerY;
2890
+ const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
2891
+
2892
+ // Only add icon if not too close to center (to avoid clutter)
2893
+ if (distanceFromCenter < 20) {
2894
+ return; // Skip icon if too close to center
2895
+ }
2896
+
2897
+ // Define the icon size
2898
+ const iconSize = 16;
2899
+
2900
+ // Get icon path using IconProvider if available
2901
+ let iconPath;
2902
+ if (this.iconProvider) {
2903
+ iconPath = this.iconProvider.getAspectIconPath(aspect.type);
2904
+ } else {
2905
+ // Fallback to old path construction
2906
+ iconPath = `${this.assetBasePath}/svg/zodiac/zodiac-aspect-${aspect.type}.svg`;
2907
+ }
2908
+
2909
+ // Create the aspect glyph/symbol image
2910
+ const symbol = this.svgUtils.createSVGElement("image", {
2911
+ x: midX - iconSize/2, // Offset by half the icon width
2912
+ y: midY - iconSize/2, // Offset by half the icon height
2913
+ width: iconSize,
2914
+ height: iconSize,
2915
+ href: iconPath,
2916
+ class: `aspect-element aspect-symbol aspect-${aspect.type}`
2917
+ });
2918
+
2919
+ // Add error handling for image loading
2920
+ symbol.addEventListener('error', () => {
2921
+ console.warn(`Failed to load aspect icon for ${aspect.type} from path: ${iconPath}`);
2922
+
2923
+ // Get the abbreviation for text fallback from aspect definition or default to first 3 letters
2924
+ const aspectDef = this.defaultAspectDefinitions[aspect.type];
2925
+ const fallbackText = aspectDef && aspectDef.abbr
2926
+ ? aspectDef.abbr
2927
+ : aspect.type.substring(0, 3).toUpperCase();
2928
+
2929
+ // Use IconProvider's createTextFallback if available
2930
+ let textSymbol;
2931
+ if (this.iconProvider) {
2932
+ textSymbol = this.iconProvider.createTextFallback(
2933
+ this.svgUtils,
2934
+ {
2935
+ x: midX,
2936
+ y: midY,
2937
+ size: '12px',
2938
+ color: aspect.color || '#888888',
2939
+ className: `aspect-element aspect-symbol aspect-${aspect.type}`
2940
+ },
2941
+ fallbackText
2942
+ );
2943
+ } else {
2944
+ // Legacy fallback
2945
+ textSymbol = this.svgUtils.createSVGElement("text", {
2946
+ x: midX,
2947
+ y: midY,
2948
+ 'text-anchor': 'middle',
2949
+ 'dominant-baseline': 'middle',
2950
+ 'font-size': '12px',
2951
+ 'font-weight': 'bold',
2952
+ class: `aspect-element aspect-symbol aspect-${aspect.type}`,
2953
+ fill: aspect.color || '#888888'
2954
+ });
2955
+ textSymbol.textContent = fallbackText;
2956
+ }
2957
+
2958
+ // Add tooltip
2959
+ this.svgUtils.addTooltip(textSymbol, tooltipText);
2960
+
2961
+ // Add to parent group
2962
+ parentGroup.appendChild(textSymbol);
2963
+ });
2964
+
2965
+ // Add tooltip
2966
+ this.svgUtils.addTooltip(symbol, tooltipText);
2967
+
2968
+ parentGroup.appendChild(symbol);
2969
+ }
2970
+ }
2971
+
2972
+ /**
2973
+ * RendererFactory.js
2974
+ * Factory class for creating renderer instances with proper dependency injection.
2975
+ */
2976
+
2977
+ class RendererFactory {
2978
+ /**
2979
+ * Constructor
2980
+ * @param {Object} config - Chart configuration
2981
+ * @param {string} svgNS - SVG namespace
2982
+ */
2983
+ constructor(config, svgNS) {
2984
+ this.config = config;
2985
+ this.svgNS = svgNS;
2986
+ this.svgUtils = ServiceRegistry.getSvgUtils();
2987
+ this.iconProvider = ServiceRegistry.getIconProvider(config.assets?.basePath);
2988
+ }
2989
+
2990
+ /**
2991
+ * Creates a ZodiacRenderer instance
2992
+ * @param {Object} options - Additional options for the renderer
2993
+ * @returns {ZodiacRenderer} The ZodiacRenderer instance
2994
+ */
2995
+ createZodiacRenderer(options = {}) {
2996
+ return new ZodiacRenderer({
2997
+ svgNS: this.svgNS,
2998
+ config: this.config,
2999
+ svgUtils: this.svgUtils,
3000
+ iconProvider: this.iconProvider,
3001
+ ...options
3002
+ });
3003
+ }
3004
+
3005
+ /**
3006
+ * Creates a HouseRenderer instance
3007
+ * @param {Object} options - Additional options for the renderer
3008
+ * @returns {HouseRenderer} The HouseRenderer instance
3009
+ */
3010
+ createHouseRenderer(options = {}) {
3011
+ return new HouseRenderer({
3012
+ svgNS: this.svgNS,
3013
+ config: this.config,
3014
+ svgUtils: this.svgUtils,
3015
+ iconProvider: this.iconProvider,
3016
+ ...options
3017
+ });
3018
+ }
3019
+
3020
+ /**
3021
+ * Creates a PlanetRenderer instance
3022
+ * This now creates a PlanetRendererCoordinator with specialized renderers
3023
+ * @param {Object} options - Additional options for the renderer
3024
+ * @returns {PlanetRendererCoordinator} The PlanetRendererCoordinator instance
3025
+ */
3026
+ createPlanetRenderer(options = {}) {
3027
+ // First create the symbol renderer
3028
+ const symbolRenderer = new PlanetSymbolRenderer({
3029
+ svgNS: this.svgNS,
3030
+ config: this.config,
3031
+ svgUtils: this.svgUtils,
3032
+ iconProvider: this.iconProvider,
3033
+ assetBasePath: options.assetBasePath || this.config.assets?.basePath,
3034
+ ...options
3035
+ });
3036
+
3037
+ // Create primary planet renderer
3038
+ const primaryRenderer = new PrimaryPlanetRenderer({
3039
+ svgNS: this.svgNS,
3040
+ config: this.config,
3041
+ svgUtils: this.svgUtils,
3042
+ iconProvider: this.iconProvider,
3043
+ symbolRenderer: symbolRenderer,
3044
+ assetBasePath: options.assetBasePath || this.config.assets?.basePath,
3045
+ ...options
3046
+ });
3047
+
3048
+ // Create secondary planet renderer
3049
+ const secondaryRenderer = new SecondaryPlanetRenderer({
3050
+ svgNS: this.svgNS,
3051
+ config: this.config,
3052
+ svgUtils: this.svgUtils,
3053
+ iconProvider: this.iconProvider,
3054
+ symbolRenderer: symbolRenderer,
3055
+ assetBasePath: options.assetBasePath || this.config.assets?.basePath,
3056
+ ...options
3057
+ });
3058
+
3059
+ // Create and return the coordinator
3060
+ return new PlanetRendererCoordinator({
3061
+ svgNS: this.svgNS,
3062
+ config: this.config,
3063
+ svgUtils: this.svgUtils,
3064
+ iconProvider: this.iconProvider,
3065
+ primaryRenderer: primaryRenderer,
3066
+ secondaryRenderer: secondaryRenderer,
3067
+ symbolRenderer: symbolRenderer,
3068
+ assetBasePath: options.assetBasePath || this.config.assets?.basePath,
3069
+ ...options
3070
+ });
3071
+ }
3072
+
3073
+ /**
3074
+ * Creates a ClientSideAspectRenderer instance
3075
+ * @param {Object} options - Additional options for the renderer
3076
+ * @returns {ClientSideAspectRenderer} The ClientSideAspectRenderer instance
3077
+ */
3078
+ createAspectRenderer(options = {}) {
3079
+ return new ClientSideAspectRenderer({
3080
+ svgNS: this.svgNS,
3081
+ config: this.config,
3082
+ svgUtils: this.svgUtils,
3083
+ iconProvider: this.iconProvider,
3084
+ ...options
3085
+ });
3086
+ }
3087
+ }
3088
+
3089
+ /**
3090
+ * RenderingCoordinator.js
3091
+ * Coordinates all rendering operations for the chart
3092
+ */
3093
+
3094
+ class RenderingCoordinator {
3095
+ /**
3096
+ * Constructor
3097
+ * @param {Object} options - Configuration options
3098
+ * @param {Object} options.config - Chart configuration
3099
+ * @param {Object} options.svgManager - SVG manager instance
3100
+ * @param {Object} options.planets - Planet data
3101
+ * @param {Array} options.houses - House data
3102
+ */
3103
+ constructor(options) {
3104
+ this.config = options.config;
3105
+ this.svgManager = options.svgManager;
3106
+ this.planets = options.planets || {};
3107
+ this.houses = options.houses || [];
3108
+
3109
+ // Initialize renderers dictionary
3110
+ this.renderers = {};
3111
+
3112
+ // Initialize renderers if SVG manager is available
3113
+ if (this.svgManager && this.svgManager.getSVG()) {
3114
+ this._initializeRenderers();
3115
+ }
3116
+ }
3117
+
3118
+ /**
3119
+ * Initializes all renderers using the factory
3120
+ * @private
3121
+ */
3122
+ _initializeRenderers() {
3123
+ // Create renderer factory
3124
+ this.rendererFactory = new RendererFactory(
3125
+ this.config,
3126
+ this.svgManager.svgNS
3127
+ );
3128
+
3129
+ // Initialize zodiac renderer
3130
+ this.renderers.zodiac = this.rendererFactory.createZodiacRenderer({
3131
+ assetBasePath: this.config.assets.basePath
3132
+ });
3133
+
3134
+ // Initialize house renderer with house data
3135
+ const houseCusps = this.config.getHouseCusps();
3136
+ const houseData = (houseCusps && houseCusps.length > 0)
3137
+ ? houseCusps
3138
+ : this.houses;
3139
+
3140
+ this.renderers.house = this.rendererFactory.createHouseRenderer({
3141
+ houseData: houseData
3142
+ });
3143
+
3144
+ // Initialize planet renderer
3145
+ this.renderers.planet = this.rendererFactory.createPlanetRenderer({
3146
+ assetBasePath: this.config.assets.basePath
3147
+ });
3148
+
3149
+ // Initialize aspect renderer
3150
+ this.renderers.aspect = this.rendererFactory.createAspectRenderer({
3151
+ assetBasePath: this.config.assets.basePath
3152
+ });
3153
+ }
3154
+
3155
+ /**
3156
+ * Updates planet data
3157
+ * @param {Object} planets - New planet data
3158
+ */
3159
+ updatePlanets(planets) {
3160
+ this.planets = planets;
3161
+ }
3162
+
3163
+ /**
3164
+ * Updates house data
3165
+ * @param {Array} houses - New house data
3166
+ */
3167
+ updateHouses(houses) {
3168
+ this.houses = houses;
3169
+ if (this.renderers.house) {
3170
+ this.renderers.house.houseData = this.houses;
3171
+ }
3172
+ }
3173
+
3174
+ /**
3175
+ * Renders all chart elements
3176
+ */
3177
+ renderAll() {
3178
+ if (!this.svgManager.getSVG()) {
3179
+ return;
3180
+ }
3181
+
3182
+ // Clear all groups
3183
+ Object.values(this.svgManager.getAllGroups()).forEach(group => {
3184
+ group.innerHTML = '';
3185
+ });
3186
+
3187
+ this.renderZodiac();
3188
+ this.renderHouses();
3189
+ const planetsWithCoords = this.renderPlanets();
3190
+ this.renderAspects(planetsWithCoords);
3191
+
3192
+ console.log("RenderingCoordinator: Chart rendered");
3193
+ }
3194
+
3195
+ /**
3196
+ * Renders the zodiac ring
3197
+ * @returns {boolean} Success indicator
3198
+ */
3199
+ renderZodiac() {
3200
+ if (!this.config.zodiacSettings.enabled || !this.renderers.zodiac) {
3201
+ return false;
3202
+ }
3203
+
3204
+ this.renderers.zodiac.render(this.svgManager.getGroup('zodiac'));
3205
+ return true;
3206
+ }
3207
+
3208
+ /**
3209
+ * Renders the houses
3210
+ * @returns {boolean} Success indicator
3211
+ */
3212
+ renderHouses() {
3213
+ if (!this.config.houseSettings.enabled || !this.renderers.house) {
3214
+ return false;
3215
+ }
3216
+
3217
+ const houseGroup = this.svgManager.getGroup('houses');
3218
+ const houseDivisionsGroup = this.svgManager.getGroup('houseDivisions');
3219
+
3220
+ // Get the current house cusps based on selected system
3221
+ const houseCusps = this.config.getHouseCusps();
3222
+ if (houseCusps && houseCusps.length > 0) {
3223
+ // Update house renderer with current house system cusps
3224
+ this.renderers.house.houseData = houseCusps;
3225
+ }
3226
+
3227
+ // Render house divisions and numbers
3228
+ this.renderers.house.renderDivisions(houseDivisionsGroup, this.config.houseSettings.rotationAngle);
3229
+ this.renderers.house.renderNumbers(houseGroup, this.config.houseSettings.rotationAngle);
3230
+
3231
+ return true;
3232
+ }
3233
+
3234
+ /**
3235
+ * Renders the planets
3236
+ * @returns {Array} Array of rendered planets with coordinates for aspect calculation
3237
+ */
3238
+ renderPlanets() {
3239
+ if (!this.config.planetSettings.enabled || !this.renderers.planet) {
3240
+ return [];
3241
+ }
3242
+
3243
+ // Get the enabled states for primary and secondary planets
3244
+ const primaryEnabled = this.config.planetSettings.primaryEnabled !== false;
3245
+ const secondaryEnabled = this.config.planetSettings.secondaryEnabled !== false;
3246
+
3247
+ // Use the consolidated renderAllPlanetTypes method
3248
+ const renderedPlanets = this.renderers.planet.renderAllPlanetTypes({
3249
+ svgManager: this.svgManager,
3250
+ planetsData: this.planets,
3251
+ config: this.config,
3252
+ primaryEnabled: primaryEnabled,
3253
+ secondaryEnabled: secondaryEnabled
3254
+ });
3255
+
3256
+ console.log(`RenderingCoordinator: Rendered ${renderedPlanets.primary.length} primary planets and ${renderedPlanets.secondary.length} secondary planets`);
3257
+
3258
+ // For aspect rendering, use primary planets by default
3259
+ return renderedPlanets.primary;
3260
+ }
3261
+
3262
+ /**
3263
+ * Renders the aspects between planets
3264
+ * @param {Array} planetsWithCoords - Array of planets with coordinates
3265
+ * @returns {boolean} Success indicator
3266
+ */
3267
+ renderAspects(planetsWithCoords) {
3268
+ if (!this.config.aspectSettings.enabled || !this.renderers.aspect) {
3269
+ return false;
3270
+ }
3271
+
3272
+ this.renderers.aspect.render(this.svgManager.getGroup('aspects'), planetsWithCoords);
3273
+ return true;
3274
+ }
3275
+ }
3276
+
3277
+ /**
3278
+ * ChartStateManager.js
3279
+ * Manages the state and configuration settings for the chart
3280
+ */
3281
+
3282
+ class ChartStateManager {
3283
+ /**
3284
+ * Constructor
3285
+ * @param {Object} options - Configuration options
3286
+ * @param {Object} options.config - Chart configuration
3287
+ * @param {Object} options.svgManager - SVG manager instance
3288
+ */
3289
+ constructor(options) {
3290
+ this.config = options.config;
3291
+ this.svgManager = options.svgManager;
3292
+ }
3293
+
3294
+ /**
3295
+ * Updates chart configuration
3296
+ * @param {Object} configUpdate - Configuration updates
3297
+ * @returns {boolean} Success indicator
3298
+ */
3299
+ updateConfig(configUpdate) {
3300
+ this.config.mergeConfig(configUpdate);
3301
+
3302
+ // Update aspect settings specifically if provided
3303
+ if (configUpdate.aspectSettings) {
3304
+ this.config.updateAspectSettings(configUpdate.aspectSettings);
3305
+ }
3306
+
3307
+ console.log("ChartStateManager: Updated configuration");
3308
+ return true;
3309
+ }
3310
+
3311
+ /**
3312
+ * Toggles the visibility of a planet
3313
+ * @param {string} planetName - Name of the planet to toggle
3314
+ * @param {boolean} visible - Visibility state
3315
+ * @returns {boolean} Success indicator
3316
+ */
3317
+ togglePlanetVisibility(planetName, visible) {
3318
+ this.config.togglePlanetVisibility(planetName, visible);
3319
+ return true;
3320
+ }
3321
+
3322
+ /**
3323
+ * Toggles the visibility of houses
3324
+ * @param {boolean} visible - Visibility state
3325
+ * @returns {boolean} Success indicator
3326
+ */
3327
+ toggleHousesVisibility(visible) {
3328
+ this.config.toggleHousesVisibility(visible);
3329
+ return true;
3330
+ }
3331
+
3332
+ /**
3333
+ * Toggles the visibility of aspects
3334
+ * @param {boolean} visible - Visibility state
3335
+ * @returns {boolean} Success indicator
3336
+ */
3337
+ toggleAspectsVisibility(visible) {
3338
+ this.config.toggleAspectsVisibility(visible);
3339
+ return true;
3340
+ }
3341
+
3342
+ /**
3343
+ * Sets the house system rotation angle
3344
+ * @param {number} angle - Rotation angle in degrees
3345
+ * @returns {boolean} Success indicator
3346
+ */
3347
+ setHouseRotation(angle) {
3348
+ this.config.houseSettings.rotationAngle = angle;
3349
+ return true;
3350
+ }
3351
+
3352
+ /**
3353
+ * Sets the house system
3354
+ * @param {string} systemName - Name of the house system to use
3355
+ * @returns {boolean} Success indicator
3356
+ */
3357
+ setHouseSystem(systemName) {
3358
+ return this.config.setHouseSystem(systemName);
3359
+ }
3360
+
3361
+ /**
3362
+ * Gets the available house systems
3363
+ * @returns {Array} Array of available house system names
3364
+ */
3365
+ getAvailableHouseSystems() {
3366
+ return this.config.getAvailableHouseSystems();
3367
+ }
3368
+
3369
+ /**
3370
+ * Gets the current house system
3371
+ * @returns {string} Current house system name
3372
+ */
3373
+ getCurrentHouseSystem() {
3374
+ return this.config.getHouseSystem();
3375
+ }
3376
+
3377
+ /**
3378
+ * Toggles the visibility of primary planets (inner circle)
3379
+ * @param {boolean} visible - Visibility state
3380
+ * @returns {boolean} Success indicator
3381
+ */
3382
+ togglePrimaryPlanets(visible) {
3383
+ // Update the config settings
3384
+ this.config.togglePrimaryPlanetsVisibility(visible);
3385
+
3386
+ // Update the group visibility in the DOM if svgManager is available
3387
+ if (this.svgManager) {
3388
+ const primaryGroup = this.svgManager.getGroup('primaryPlanets');
3389
+ if (primaryGroup) {
3390
+ primaryGroup.style.display = visible ? 'block' : 'none';
3391
+ }
3392
+ }
3393
+
3394
+ console.log(`ChartStateManager: Primary planets ${visible ? 'enabled' : 'disabled'}`);
3395
+ return true;
3396
+ }
3397
+
3398
+ /**
3399
+ * Toggles the visibility of secondary planets (innermost circle)
3400
+ * @param {boolean} visible - Visibility state
3401
+ * @returns {boolean} Success indicator
3402
+ */
3403
+ toggleSecondaryPlanets(visible) {
3404
+ // Update the config settings
3405
+ this.config.toggleSecondaryPlanetsVisibility(visible);
3406
+
3407
+ // Update the group visibility in the DOM if svgManager is available
3408
+ if (this.svgManager) {
3409
+ const secondaryGroup = this.svgManager.getGroup('secondaryPlanets');
3410
+ if (secondaryGroup) {
3411
+ secondaryGroup.style.display = visible ? 'block' : 'none';
3412
+ }
3413
+
3414
+ // Update the innermost circle visibility
3415
+ const innermostCircle = document.querySelector('.chart-innermost-circle');
3416
+ if (innermostCircle) {
3417
+ innermostCircle.style.display = visible ? 'block' : 'none';
3418
+ }
3419
+ }
3420
+
3421
+ console.log(`ChartStateManager: Secondary planets ${visible ? 'enabled' : 'disabled'}`);
3422
+ return true;
3423
+ }
3424
+ }
3425
+
3426
+ /**
3427
+ * ChartManager.js
3428
+ * Main class for the nocturna-wheel.js library.
3429
+ * Serves as the facade for all chart rendering operations.
3430
+ */
3431
+
3432
+ class ChartManager {
3433
+ /**
3434
+ * Constructor
3435
+ * @param {Object} options - Configuration options
3436
+ * @param {string|Element} options.container - Container element or selector
3437
+ * @param {Object} options.planets - Planet positions data
3438
+ * @param {Array} options.houses - House cusps data (optional)
3439
+ * @param {Object} options.aspectSettings - Aspect calculation settings (optional)
3440
+ * @param {Object} options.config - Additional configuration (optional)
3441
+ */
3442
+ constructor(options) {
3443
+ if (!options || !options.container) {
3444
+ throw new Error("ChartManager: Container element or selector is required");
3445
+ }
3446
+
3447
+ this.options = options;
3448
+ this.container = typeof options.container === 'string'
3449
+ ? document.querySelector(options.container)
3450
+ : options.container;
3451
+
3452
+ if (!this.container) {
3453
+ throw new Error(`ChartManager: Container not found: ${options.container}`);
3454
+ }
3455
+
3456
+ // Initialize configuration
3457
+ this.config = new ChartConfig(options.config || {});
3458
+
3459
+ // Set data
3460
+ this.planets = options.planets || {};
3461
+ this.houses = options.houses || [];
3462
+
3463
+ // Override aspect settings if provided
3464
+ if (options.aspectSettings) {
3465
+ this.config.updateAspectSettings(options.aspectSettings);
3466
+ }
3467
+
3468
+ // Initialize services
3469
+ ServiceRegistry.initializeServices();
3470
+
3471
+ // Initialize SVG manager with shared svgUtils instance
3472
+ const svgUtils = ServiceRegistry.getSvgUtils();
3473
+ this.svgManager = new SVGManager({ svgUtils });
3474
+
3475
+ // Initialize component classes
3476
+ this.renderingCoordinator = new RenderingCoordinator({
3477
+ config: this.config,
3478
+ svgManager: this.svgManager,
3479
+ planets: this.planets,
3480
+ houses: this.houses
3481
+ });
3482
+
3483
+ this.stateManager = new ChartStateManager({
3484
+ config: this.config,
3485
+ svgManager: this.svgManager
3486
+ });
3487
+
3488
+ console.log("ChartManager: Initialized");
3489
+ }
3490
+
3491
+ /**
3492
+ * Initializes and sets up the chart
3493
+ * @private
3494
+ */
3495
+ _initialize() {
3496
+ const svgOptions = {
3497
+ width: this.config.svg.width,
3498
+ height: this.config.svg.height,
3499
+ viewBox: this.config.svg.viewBox
3500
+ };
3501
+
3502
+ // Create SVG element
3503
+ this.svgManager.initialize(this.container, svgOptions);
3504
+
3505
+ // Create standard layer groups
3506
+ this.svgManager.createStandardGroups();
3507
+ }
3508
+
3509
+ /**
3510
+ * Renders the chart
3511
+ * @returns {ChartManager} - Instance for chaining
3512
+ */
3513
+ render() {
3514
+ if (!this.svgManager.getSVG()) {
3515
+ this._initialize();
3516
+ }
3517
+
3518
+ // Delegate to rendering coordinator
3519
+ this.renderingCoordinator.renderAll();
3520
+
3521
+ console.log("ChartManager: Chart rendered");
3522
+ return this;
3523
+ }
3524
+
3525
+ /**
3526
+ * Updates chart configuration
3527
+ * @param {Object} config - New configuration
3528
+ * @returns {ChartManager} - Instance for chaining
3529
+ */
3530
+ update(config) {
3531
+ if (config.planets) {
3532
+ this.planets = config.planets;
3533
+ this.renderingCoordinator.updatePlanets(this.planets);
3534
+ }
3535
+
3536
+ if (config.houses) {
3537
+ this.houses = config.houses;
3538
+ this.renderingCoordinator.updateHouses(this.houses);
3539
+ }
3540
+
3541
+ if (config.config) {
3542
+ this.stateManager.updateConfig(config.config);
3543
+ }
3544
+
3545
+ // Re-render the chart
3546
+ this.render();
3547
+
3548
+ return this;
3549
+ }
3550
+
3551
+ /**
3552
+ * Toggles the visibility of a planet
3553
+ * @param {string} planetName - Name of the planet to toggle
3554
+ * @param {boolean} visible - Visibility state
3555
+ * @returns {ChartManager} - Instance for chaining
3556
+ */
3557
+ togglePlanet(planetName, visible) {
3558
+ this.stateManager.togglePlanetVisibility(planetName, visible);
3559
+ this.render();
3560
+ return this;
3561
+ }
3562
+
3563
+ /**
3564
+ * Toggles the visibility of houses
3565
+ * @param {boolean} visible - Visibility state
3566
+ * @returns {ChartManager} - Instance for chaining
3567
+ */
3568
+ toggleHouses(visible) {
3569
+ this.stateManager.toggleHousesVisibility(visible);
3570
+ this.render();
3571
+ return this;
3572
+ }
3573
+
3574
+ /**
3575
+ * Toggles the visibility of aspects
3576
+ * @param {boolean} visible - Visibility state
3577
+ * @returns {ChartManager} - Instance for chaining
3578
+ */
3579
+ toggleAspects(visible) {
3580
+ this.stateManager.toggleAspectsVisibility(visible);
3581
+ this.render();
3582
+ return this;
3583
+ }
3584
+
3585
+ /**
3586
+ * Sets the house system rotation angle
3587
+ * @param {number} angle - Rotation angle in degrees
3588
+ * @returns {ChartManager} - Instance for chaining
3589
+ */
3590
+ setHouseRotation(angle) {
3591
+ this.stateManager.setHouseRotation(angle);
3592
+ this.render();
3593
+ return this;
3594
+ }
3595
+
3596
+ /**
3597
+ * Destroys the chart and cleans up resources
3598
+ */
3599
+ destroy() {
3600
+ // Remove the SVG element
3601
+ if (this.svgManager.getSVG()) {
3602
+ this.svgManager.getSVG().remove();
3603
+ }
3604
+
3605
+ // Clear references
3606
+ this.renderingCoordinator = null;
3607
+ this.stateManager = null;
3608
+ this.planets = {};
3609
+ this.houses = [];
3610
+
3611
+ console.log("ChartManager: Destroyed");
3612
+ }
3613
+
3614
+ /**
3615
+ * Updates chart data (planets, houses)
3616
+ * @param {Object} data - Object containing new data, e.g., { planets: {...}, houses: [...] }
3617
+ * @returns {ChartManager} - Instance for chaining
3618
+ */
3619
+ updateData(data) {
3620
+ if (data.planets) {
3621
+ // Update internal planets data
3622
+ // Ensure it matches the format expected internally (object)
3623
+ if (typeof data.planets === 'object' && !Array.isArray(data.planets)) {
3624
+ this.planets = { ...this.planets, ...data.planets };
3625
+ this.renderingCoordinator.updatePlanets(this.planets);
3626
+ console.log("ChartManager: Updated planets data.");
3627
+ } else {
3628
+ console.warn("ChartManager.updateData: Invalid planets data format. Expected object.");
3629
+ }
3630
+ }
3631
+ if (data.houses) {
3632
+ // Update internal houses data
3633
+ if (Array.isArray(data.houses)) {
3634
+ this.houses = data.houses;
3635
+ this.renderingCoordinator.updateHouses(this.houses);
3636
+ console.log("ChartManager: Updated houses data.");
3637
+ } else {
3638
+ console.warn("ChartManager.updateData: Invalid houses data format. Expected array.");
3639
+ }
3640
+ }
3641
+ // Re-render the chart with updated data
3642
+ this.render();
3643
+ return this;
3644
+ }
3645
+
3646
+ /**
3647
+ * Updates chart configuration (aspects, assets, etc.)
3648
+ * @param {Object} configUpdate - Object containing configuration updates
3649
+ * @returns {ChartManager} - Instance for chaining
3650
+ */
3651
+ updateConfig(configUpdate) {
3652
+ this.stateManager.updateConfig(configUpdate);
3653
+
3654
+ console.log("ChartManager: Updated configuration.");
3655
+ // Re-render the chart with updated configuration
3656
+ this.render();
3657
+ return this;
3658
+ }
3659
+
3660
+ /**
3661
+ * Sets the house system
3662
+ * @param {string} systemName - Name of the house system to use
3663
+ * @returns {ChartManager} - Instance for chaining
3664
+ */
3665
+ setHouseSystem(systemName) {
3666
+ this.stateManager.setHouseSystem(systemName);
3667
+ this.render();
3668
+ return this;
3669
+ }
3670
+
3671
+ /**
3672
+ * Gets the available house systems
3673
+ * @returns {Array} - Array of available house system names
3674
+ */
3675
+ getAvailableHouseSystems() {
3676
+ return this.stateManager.getAvailableHouseSystems();
3677
+ }
3678
+
3679
+ /**
3680
+ * Gets the current house system
3681
+ * @returns {string} - Current house system name
3682
+ */
3683
+ getCurrentHouseSystem() {
3684
+ return this.stateManager.getCurrentHouseSystem();
3685
+ }
3686
+
3687
+ /**
3688
+ * Toggles the visibility of primary planets (inner circle)
3689
+ * @param {boolean} visible - Visibility state
3690
+ * @returns {ChartManager} - Instance for chaining
3691
+ */
3692
+ togglePrimaryPlanets(visible) {
3693
+ this.stateManager.togglePrimaryPlanets(visible);
3694
+ this.render();
3695
+ return this;
3696
+ }
3697
+
3698
+ /**
3699
+ * Toggles the visibility of secondary planets (innermost circle)
3700
+ * @param {boolean} visible - Visibility state
3701
+ * @returns {ChartManager} - Instance for chaining
3702
+ */
3703
+ toggleSecondaryPlanets(visible) {
3704
+ this.stateManager.toggleSecondaryPlanets(visible);
3705
+ this.render();
3706
+ return this;
3707
+ }
3708
+ }
3709
+
3710
+ /**
3711
+ * NocturnaWheel.js
3712
+ * Main class for the nocturna-wheel.js library.
3713
+ * Serves as the facade for all chart rendering operations.
3714
+ */
3715
+
3716
+ class NocturnaWheel {
3717
+ /**
3718
+ * Constructor
3719
+ * @param {Object} options - Configuration options
3720
+ * @param {string|Element} options.container - Container element or selector
3721
+ * @param {Object} options.planets - Planet positions data
3722
+ * @param {Array} options.houses - House cusps data (optional)
3723
+ * @param {Object} options.aspectSettings - Aspect calculation settings (optional)
3724
+ * @param {Object} options.config - Additional configuration (optional)
3725
+ */
3726
+ constructor(options) {
3727
+ if (!options || !options.container) {
3728
+ throw new Error("NocturnaWheel: Container element or selector is required");
3729
+ }
3730
+
3731
+ this.options = options;
3732
+ this.container = typeof options.container === 'string'
3733
+ ? document.querySelector(options.container)
3734
+ : options.container;
3735
+
3736
+ if (!this.container) {
3737
+ throw new Error(`NocturnaWheel: Container not found: ${options.container}`);
3738
+ }
3739
+
3740
+ // Initialize configuration
3741
+ this.config = new ChartConfig(options.config || {});
3742
+
3743
+ // Set data
3744
+ this.planets = options.planets || {};
3745
+ this.houses = options.houses || [];
3746
+
3747
+ // Override aspect settings if provided
3748
+ if (options.aspectSettings) {
3749
+ this.config.updateAspectSettings(options.aspectSettings);
3750
+ }
3751
+
3752
+ // Initialize services
3753
+ ServiceRegistry.initializeServices();
3754
+
3755
+ // Initialize SVG manager with shared svgUtils instance
3756
+ const svgUtils = ServiceRegistry.getSvgUtils();
3757
+ this.svgManager = new SVGManager({ svgUtils });
3758
+
3759
+ // Initialize renderers dictionary
3760
+ this.renderers = {};
3761
+
3762
+ console.log("NocturnaWheel: Initialized");
3763
+ }
3764
+
3765
+ /**
3766
+ * Initializes and sets up the chart
3767
+ * @private
3768
+ */
3769
+ _initialize() {
3770
+ const svgOptions = {
3771
+ width: this.config.svg.width,
3772
+ height: this.config.svg.height,
3773
+ viewBox: this.config.svg.viewBox
3774
+ };
3775
+
3776
+ // Create SVG element
3777
+ this.svgManager.initialize(this.container, svgOptions);
3778
+
3779
+ // Create standard layer groups
3780
+ this.svgManager.createStandardGroups();
3781
+
3782
+ // Initialize renderers
3783
+ this._initializeRenderers();
3784
+ }
3785
+
3786
+ /**
3787
+ * Initializes all renderers using the factory
3788
+ * @private
3789
+ */
3790
+ _initializeRenderers() {
3791
+ // Create renderer factory
3792
+ this.rendererFactory = new RendererFactory(
3793
+ this.config,
3794
+ this.svgManager.svgNS
3795
+ );
3796
+
3797
+ // Initialize zodiac renderer
3798
+ this.renderers.zodiac = this.rendererFactory.createZodiacRenderer({
3799
+ assetBasePath: this.config.assets.basePath
3800
+ });
3801
+
3802
+ // Initialize house renderer with house data
3803
+ const houseCusps = this.config.getHouseCusps();
3804
+ const houseData = (houseCusps && houseCusps.length > 0)
3805
+ ? houseCusps
3806
+ : this.houses;
3807
+
3808
+ this.renderers.house = this.rendererFactory.createHouseRenderer({
3809
+ houseData: houseData
3810
+ });
3811
+
3812
+ // Initialize planet renderer
3813
+ this.renderers.planet = this.rendererFactory.createPlanetRenderer({
3814
+ assetBasePath: this.config.assets.basePath
3815
+ });
3816
+
3817
+ // Initialize aspect renderer
3818
+ this.renderers.aspect = this.rendererFactory.createAspectRenderer({
3819
+ assetBasePath: this.config.assets.basePath
3820
+ });
3821
+ }
3822
+
3823
+ /**
3824
+ * Renders the chart
3825
+ */
3826
+ render() {
3827
+ if (!this.svgManager.getSVG()) {
3828
+ this._initialize();
3829
+ }
3830
+
3831
+ // Clear all groups
3832
+ Object.values(this.svgManager.getAllGroups()).forEach(group => {
3833
+ group.innerHTML = '';
3834
+ });
3835
+
3836
+ // Render zodiac if enabled
3837
+ if (this.config.zodiacSettings.enabled) {
3838
+ this.renderers.zodiac.render(this.svgManager.getGroup('zodiac'));
3839
+ }
3840
+
3841
+ // Render houses if enabled
3842
+ if (this.config.houseSettings.enabled) {
3843
+ const houseGroup = this.svgManager.getGroup('houses');
3844
+ const houseDivisionsGroup = this.svgManager.getGroup('houseDivisions');
3845
+
3846
+ // Get the current house cusps based on selected system
3847
+ const houseCusps = this.config.getHouseCusps();
3848
+ if (houseCusps && houseCusps.length > 0) {
3849
+ // Update house renderer with current house system cusps
3850
+ this.renderers.house.houseData = houseCusps;
3851
+ }
3852
+
3853
+ // Render house divisions and numbers
3854
+ this.renderers.house.renderDivisions(houseDivisionsGroup, this.config.houseSettings.rotationAngle);
3855
+ this.renderers.house.renderNumbers(houseGroup, this.config.houseSettings.rotationAngle);
3856
+ }
3857
+
3858
+ // Render planets using the consolidated approach
3859
+ let planetsWithCoords = [];
3860
+ if (this.config.planetSettings.enabled) {
3861
+ // Get the enabled states for primary and secondary planets
3862
+ const primaryEnabled = this.config.planetSettings.primaryEnabled !== false;
3863
+ const secondaryEnabled = this.config.planetSettings.secondaryEnabled !== false;
3864
+
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
+ });
3873
+
3874
+ // For aspect rendering, use primary planets by default
3875
+ planetsWithCoords = renderedPlanets.primary;
3876
+
3877
+ console.log(`NocturnaWheel: Rendered ${renderedPlanets.primary.length} primary planets and ${renderedPlanets.secondary.length} secondary planets`);
3878
+ }
3879
+
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);
3883
+ }
3884
+
3885
+ console.log("NocturnaWheel: Chart rendered");
3886
+ return this;
3887
+ }
3888
+
3889
+ /**
3890
+ * Updates chart configuration
3891
+ * @param {Object} config - New configuration
3892
+ * @returns {NocturnaWheel} - Instance for chaining
3893
+ */
3894
+ update(config) {
3895
+ if (config.planets) {
3896
+ this.planets = config.planets;
3897
+ }
3898
+
3899
+ if (config.houses) {
3900
+ this.houses = config.houses;
3901
+ if (this.renderers.house) {
3902
+ this.renderers.house.houseData = this.houses;
3903
+ }
3904
+ }
3905
+
3906
+ if (config.config) {
3907
+ this.config.mergeConfig(config.config);
3908
+ }
3909
+
3910
+ // Re-render the chart
3911
+ this.render();
3912
+
3913
+ return this;
3914
+ }
3915
+
3916
+ /**
3917
+ * Toggles the visibility of a planet
3918
+ * @param {string} planetName - Name of the planet to toggle
3919
+ * @param {boolean} visible - Visibility state
3920
+ * @returns {NocturnaWheel} - Instance for chaining
3921
+ */
3922
+ togglePlanet(planetName, visible) {
3923
+ this.config.togglePlanetVisibility(planetName, visible);
3924
+ this.render();
3925
+ return this;
3926
+ }
3927
+
3928
+ /**
3929
+ * Toggles the visibility of houses
3930
+ * @param {boolean} visible - Visibility state
3931
+ * @returns {NocturnaWheel} - Instance for chaining
3932
+ */
3933
+ toggleHouses(visible) {
3934
+ this.config.toggleHousesVisibility(visible);
3935
+ this.render();
3936
+ return this;
3937
+ }
3938
+
3939
+ /**
3940
+ * Toggles the visibility of aspects
3941
+ * @param {boolean} visible - Visibility state
3942
+ * @returns {NocturnaWheel} - Instance for chaining
3943
+ */
3944
+ toggleAspects(visible) {
3945
+ this.config.toggleAspectsVisibility(visible);
3946
+ this.render();
3947
+ return this;
3948
+ }
3949
+
3950
+ /**
3951
+ * Sets the house system rotation angle
3952
+ * @param {number} angle - Rotation angle in degrees
3953
+ * @returns {NocturnaWheel} - Instance for chaining
3954
+ */
3955
+ setHouseRotation(angle) {
3956
+ this.config.houseSettings.rotationAngle = angle;
3957
+ this.render();
3958
+ return this;
3959
+ }
3960
+
3961
+ /**
3962
+ * Destroys the chart and cleans up resources
3963
+ */
3964
+ destroy() {
3965
+ // Remove the SVG element
3966
+ if (this.svgManager.getSVG()) {
3967
+ this.svgManager.getSVG().remove();
3968
+ }
3969
+
3970
+ // Clear references
3971
+ this.renderers = {};
3972
+ this.planets = {};
3973
+ this.houses = [];
3974
+
3975
+ console.log("NocturnaWheel: Destroyed");
3976
+ }
3977
+
3978
+ /**
3979
+ * Updates chart data (planets, houses)
3980
+ * @param {Object} data - Object containing new data, e.g., { planets: {...}, houses: [...] }
3981
+ * @returns {NocturnaWheel} - Instance for chaining
3982
+ */
3983
+ updateData(data) {
3984
+ if (data.planets) {
3985
+ // Update internal planets data
3986
+ // Ensure it matches the format expected internally (object)
3987
+ if (typeof data.planets === 'object' && !Array.isArray(data.planets)) {
3988
+ this.planets = { ...this.planets, ...data.planets };
3989
+ console.log("NocturnaWheel: Updated planets data.");
3990
+ } else {
3991
+ console.warn("NocturnaWheel.updateData: Invalid planets data format. Expected object.");
3992
+ }
3993
+ }
3994
+ if (data.houses) {
3995
+ // Update internal houses data
3996
+ if (Array.isArray(data.houses)) {
3997
+ this.houses = data.houses;
3998
+ // Need to inform HouseRenderer if it holds its own copy
3999
+ if (this.renderers.house) {
4000
+ this.renderers.house.houseData = this.houses;
4001
+ }
4002
+ console.log("NocturnaWheel: Updated houses data.");
4003
+ } else {
4004
+ console.warn("NocturnaWheel.updateData: Invalid houses data format. Expected array.");
4005
+ }
4006
+ }
4007
+ // Re-render the chart with updated data
4008
+ this.render();
4009
+ return this;
4010
+ }
4011
+
4012
+ /**
4013
+ * Updates chart configuration (aspects, assets, etc.)
4014
+ * @param {Object} configUpdate - Object containing configuration updates
4015
+ * @returns {NocturnaWheel} - Instance for chaining
4016
+ */
4017
+ updateConfig(configUpdate) {
4018
+ this.config.mergeConfig(configUpdate); // Use the existing mergeConfig method
4019
+
4020
+ // Update aspect settings specifically if provided
4021
+ if (configUpdate.aspectSettings) {
4022
+ this.config.updateAspectSettings(configUpdate.aspectSettings);
4023
+ }
4024
+
4025
+ console.log("NocturnaWheel: Updated configuration.");
4026
+ // Re-render the chart with updated configuration
4027
+ this.render();
4028
+ return this;
4029
+ }
4030
+
4031
+ /**
4032
+ * Sets the house system
4033
+ * @param {string} systemName - Name of the house system to use
4034
+ * @returns {NocturnaWheel} - Instance for chaining
4035
+ */
4036
+ setHouseSystem(systemName) {
4037
+ this.config.setHouseSystem(systemName);
4038
+ this.render();
4039
+ return this;
4040
+ }
4041
+
4042
+ /**
4043
+ * Gets the available house systems
4044
+ * @returns {Array} - Array of available house system names
4045
+ */
4046
+ getAvailableHouseSystems() {
4047
+ return this.config.getAvailableHouseSystems();
4048
+ }
4049
+
4050
+ /**
4051
+ * Gets the current house system
4052
+ * @returns {string} - Current house system name
4053
+ */
4054
+ getCurrentHouseSystem() {
4055
+ return this.config.getHouseSystem();
4056
+ }
4057
+
4058
+ /**
4059
+ * Toggles the visibility of primary planets (inner circle)
4060
+ * @param {boolean} visible - Visibility state
4061
+ * @returns {NocturnaWheel} - Instance for chaining
4062
+ */
4063
+ togglePrimaryPlanets(visible) {
4064
+ // Use the config's method to update the settings
4065
+ this.config.togglePrimaryPlanetsVisibility(visible);
4066
+
4067
+ // Update the group visibility in the DOM
4068
+ const primaryGroup = this.svgManager.getGroup('primaryPlanets');
4069
+ if (primaryGroup) {
4070
+ primaryGroup.style.display = visible ? 'block' : 'none';
4071
+ }
4072
+
4073
+ console.log(`NocturnaWheel: Primary planets ${visible ? 'enabled' : 'disabled'}`);
4074
+ return this;
4075
+ }
4076
+
4077
+ /**
4078
+ * Toggles the visibility of secondary planets (innermost circle)
4079
+ * @param {boolean} visible - Visibility state
4080
+ * @returns {NocturnaWheel} - Instance for chaining
4081
+ */
4082
+ toggleSecondaryPlanets(visible) {
4083
+ // Use the config's method to update the settings
4084
+ this.config.toggleSecondaryPlanetsVisibility(visible);
4085
+
4086
+ // Update the group visibility in the DOM
4087
+ const secondaryGroup = this.svgManager.getGroup('secondaryPlanets');
4088
+ if (secondaryGroup) {
4089
+ secondaryGroup.style.display = visible ? 'block' : 'none';
4090
+ }
4091
+
4092
+ // Update the innermost circle visibility
4093
+ const innermostCircle = document.querySelector('.chart-innermost-circle');
4094
+ if (innermostCircle) {
4095
+ innermostCircle.style.display = visible ? 'block' : 'none';
4096
+ }
4097
+
4098
+ console.log(`NocturnaWheel: Secondary planets ${visible ? 'enabled' : 'disabled'}`);
4099
+ return this;
4100
+ }
4101
+ }
4102
+
4103
+ // Export for module environments or expose globally for browser
4104
+ // if (typeof module !== 'undefined' && module.exports) {
4105
+ // module.exports = NocturnaWheel;
4106
+ // } else {
4107
+ // window.NocturnaWheel = NocturnaWheel;
4108
+ // }
4109
+
4110
+ /**
4111
+ * ChartRenderer.js
4112
+ * Responsible for rendering chart elements
4113
+ */
4114
+
4115
+ class ChartRenderer {
4116
+ /**
4117
+ * Constructor
4118
+ * @param {WheelChart} wheelChart - WheelChart instance
4119
+ * @param {Object} options - Chart options
4120
+ */
4121
+ constructor(wheelChart, options = {}) {
4122
+ this.wheelChart = wheelChart; // Store reference to WheelChart instance
4123
+ this.chart = wheelChart.chart; // Store direct reference to underlying NocturnaWheel instance
4124
+ this.options = options;
4125
+ this.svgUtils = new SvgUtils();
4126
+ console.log("ChartRenderer: Initialized with chart", this.chart);
4127
+ }
4128
+
4129
+ /**
4130
+ * Renders inner circle and planets
4131
+ */
4132
+ renderInnerElements() {
4133
+ // Make sure we have a valid NocturnaWheel instance with svgManager
4134
+ if (!this.chart || !this.chart.svgManager) {
4135
+ console.error("ChartRenderer: Invalid chart or missing svgManager");
4136
+ return;
4137
+ }
4138
+
4139
+ const svg = this.chart.svgManager.getSVG();
4140
+ if (!svg) {
4141
+ console.error("ChartRenderer: SVG not found");
4142
+ return;
4143
+ }
4144
+
4145
+ const zodiacGroup = this.chart.svgManager.getGroup('zodiac');
4146
+ if (!zodiacGroup) {
4147
+ console.error("ChartRenderer: Zodiac group not found");
4148
+ return;
4149
+ }
4150
+
4151
+ // Remove only the previous innermost circle if it exists
4152
+ const prevCircle = zodiacGroup.querySelector('.chart-innermost-circle');
4153
+ if (prevCircle) {
4154
+ zodiacGroup.removeChild(prevCircle);
4155
+ }
4156
+
4157
+ const centerX = this.chart.config.svg.center.x;
4158
+ const centerY = this.chart.config.svg.center.y;
4159
+ const c3Radius = this.chart.config.radius.innermost; // C3
4160
+
4161
+ // Only draw the innermost circle if secondary planets are enabled
4162
+ if (this.chart.config.planetSettings.secondaryEnabled !== false) {
4163
+ this.drawInnermostCircle(zodiacGroup, this.svgUtils, centerX, centerY, c3Radius);
4164
+ }
4165
+
4166
+ // Note: Planet rendering is now fully handled by NocturnaWheel.js
4167
+ // using the PlanetRenderer.renderAllPlanetTypes method
4168
+ console.log('ChartRenderer: Innermost circle rendered (if enabled). Planet rendering is now handled by NocturnaWheel.');
4169
+ }
4170
+
4171
+ /**
4172
+ * Draws the innermost circle
4173
+ * @param {SVGElement} zodiacGroup - SVG group for zodiac elements
4174
+ * @param {SvgUtils} svgUtils - SVG utilities
4175
+ * @param {number} centerX - X coordinate of center
4176
+ * @param {number} centerY - Y coordinate of center
4177
+ * @param {number} radius - Radius of circle
4178
+ */
4179
+ drawInnermostCircle(zodiacGroup, svgUtils, centerX, centerY, radius) {
4180
+ const innermostCircle = svgUtils.createSVGElement('circle', {
4181
+ cx: centerX,
4182
+ cy: centerY,
4183
+ r: radius,
4184
+ class: 'zodiac-element chart-innermost-circle'
4185
+ });
4186
+ zodiacGroup.appendChild(innermostCircle);
4187
+ }
4188
+
4189
+ /**
4190
+ * @deprecated Planet rendering is now fully handled by NocturnaWheel
4191
+ */
4192
+ renderPlanets(centerX, centerY, dotRadius, iconRadius) {
4193
+ console.warn('ChartRenderer: renderPlanets is deprecated. Planet rendering is now handled by NocturnaWheel.');
4194
+ return [];
4195
+ }
4196
+
4197
+ /**
4198
+ * @deprecated Use renderPlanets instead
4199
+ */
4200
+ drawPlanetsAndIcons(svgUtils, centerX, centerY, dotRadius, iconRadius) {
4201
+ console.warn('ChartRenderer: drawPlanetsAndIcons is deprecated, use renderPlanets instead');
4202
+ return this.renderPlanets(centerX, centerY, dotRadius, iconRadius);
4203
+ }
4204
+
4205
+ /**
4206
+ * @deprecated
4207
+ */
4208
+ drawPlanetDot(svgUtils, planetGroup, x, y, name, color) {
4209
+ console.warn('ChartRenderer: drawPlanetDot is deprecated');
4210
+ // No implementation - method kept for backward compatibility
4211
+ }
4212
+
4213
+ /**
4214
+ * @deprecated
4215
+ */
4216
+ drawPlanetIcon(svgUtils, planetGroup, x, y, name) {
4217
+ console.warn('ChartRenderer: drawPlanetIcon is deprecated');
4218
+ // No implementation - method kept for backward compatibility
4219
+ }
4220
+
4221
+ /**
4222
+ * @deprecated
4223
+ */
4224
+ drawConnector(svgUtils, planetGroup, x1, y1, x2, y2, name, color) {
4225
+ console.warn('ChartRenderer: drawConnector is deprecated');
4226
+ // No implementation - method kept for backward compatibility
4227
+ }
4228
+ }
4229
+
4230
+ /**
4231
+ * WheelChart.js
4232
+ * Component class for rendering astrological wheel chart with customizable circles
4233
+ */
4234
+
4235
+ class WheelChart {
4236
+ /**
4237
+ * Constructor
4238
+ * @param {Object} options - Configuration options
4239
+ * @param {string|Element} options.container - Container element or selector
4240
+ * @param {Object} options.planets - Planet positions data
4241
+ * @param {Array} options.houses - House cusps data (optional)
4242
+ * @param {Object} options.aspectSettings - Aspect calculation settings (optional)
4243
+ * @param {Object} options.config - Additional configuration (optional)
4244
+ * @param {Function} [chartFactory=null] - Factory function to create the chart instance
4245
+ * Function signature: (options) => ChartInstance
4246
+ */
4247
+ constructor(options, chartFactory = null) {
4248
+ if (!options || !options.container) {
4249
+ throw new Error("WheelChart: Container element or selector is required");
4250
+ }
4251
+
4252
+ this.options = options;
4253
+
4254
+ // Initialize configuration
4255
+ const config = options.config || {};
4256
+
4257
+ // Use the factory function if provided
4258
+ if (typeof chartFactory === 'function') {
4259
+ console.log("WheelChart: Using provided chart factory function");
4260
+ this.chart = chartFactory(options);
4261
+ } else {
4262
+ // Fallback to the original behavior for backward compatibility
4263
+ console.log("WheelChart: Using default chart creation");
4264
+
4265
+ // Create a new NocturnaWheel instance directly using the imported class
4266
+ this.chart = new NocturnaWheel({
4267
+ ...options,
4268
+ config: config
4269
+ });
4270
+ }
4271
+
4272
+ // Validate the created chart instance has required methods
4273
+ if (!this.chart || typeof this.chart.render !== 'function') {
4274
+ throw new Error("WheelChart: Invalid chart instance created. Missing required methods.");
4275
+ }
4276
+
4277
+ // Create the renderer
4278
+ this.renderer = new ChartRenderer(this, options);
4279
+
4280
+ console.log("WheelChart: Initialized");
4281
+ }
4282
+
4283
+ /**
4284
+ * Renders the chart
4285
+ * @returns {WheelChart} - Instance for chaining
4286
+ */
4287
+ render() {
4288
+ // First render the base chart
4289
+ this.chart.render();
4290
+
4291
+ // Add the innermost circle after rendering the main chart
4292
+ // Wrap in setTimeout to ensure the chart rendering has completed
4293
+ setTimeout(() => {
4294
+ try {
4295
+ console.log("WheelChart: Rendering inner elements");
4296
+ this.renderer.renderInnerElements();
4297
+ } catch (error) {
4298
+ console.error("WheelChart: Error rendering inner elements:", error);
4299
+ }
4300
+ }, 0);
4301
+
4302
+ console.log("WheelChart: Chart rendered with inner circle");
4303
+ return this;
4304
+ }
4305
+
4306
+ /**
4307
+ * Updates chart configuration
4308
+ * @param {Object} config - New configuration
4309
+ * @returns {WheelChart} - Instance for chaining
4310
+ */
4311
+ update(config) {
4312
+ this.chart.update(config);
4313
+
4314
+ // Redraw the innermost circle
4315
+ this.renderer.renderInnerElements();
4316
+
4317
+ return this;
4318
+ }
4319
+
4320
+ /**
4321
+ * Delegates method calls to the NocturnaWheel instance
4322
+ * @param {string} method - Method name
4323
+ * @param {Array} args - Method arguments
4324
+ * @returns {*} - Return value from the method
4325
+ */
4326
+ _delegateToChart(method, ...args) {
4327
+ if (typeof this.chart[method] === 'function') {
4328
+ return this.chart[method](...args);
4329
+ }
4330
+ throw new Error(`WheelChart: Method ${method} not found in NocturnaWheel`);
4331
+ }
4332
+
4333
+ /**
4334
+ * Delegates method calls and redraws inner circle
4335
+ * @param {string} method - Method name
4336
+ * @param {Array} args - Method arguments
4337
+ * @returns {*} - Return value from the method
4338
+ * @private
4339
+ */
4340
+ _delegateAndRedraw(method, ...args) {
4341
+ const result = this._delegateToChart(method, ...args);
4342
+ this.renderer.renderInnerElements();
4343
+ return result;
4344
+ }
4345
+
4346
+ // Delegate common methods to the NocturnaWheel instance
4347
+ togglePlanet(planetName, visible) {
4348
+ return this._delegateAndRedraw('togglePlanet', planetName, visible);
4349
+ }
4350
+
4351
+ toggleHouses(visible) {
4352
+ return this._delegateAndRedraw('toggleHouses', visible);
4353
+ }
4354
+
4355
+ toggleAspects(visible) {
4356
+ return this._delegateAndRedraw('toggleAspects', visible);
4357
+ }
4358
+
4359
+ /**
4360
+ * Toggles the visibility of primary planets (inner circle)
4361
+ * @param {boolean} visible - Visibility state
4362
+ * @returns {WheelChart} - Instance for chaining
4363
+ */
4364
+ togglePrimaryPlanets(visible) {
4365
+ return this._delegateAndRedraw('togglePrimaryPlanets', visible);
4366
+ }
4367
+
4368
+ /**
4369
+ * Toggles the visibility of secondary planets (innermost circle)
4370
+ * @param {boolean} visible - Visibility state
4371
+ * @returns {WheelChart} - Instance for chaining
4372
+ */
4373
+ toggleSecondaryPlanets(visible) {
4374
+ // First delegate to NocturnaWheel instance
4375
+ const result = this._delegateToChart('toggleSecondaryPlanets', visible);
4376
+
4377
+ // Explicitly handle innermost circle visibility
4378
+ try {
4379
+ const container = typeof this.options.container === 'string' ?
4380
+ document.querySelector(this.options.container) :
4381
+ this.options.container;
4382
+
4383
+ if (container) {
4384
+ const innermostCircle = container.querySelector('.chart-innermost-circle');
4385
+ if (innermostCircle) {
4386
+ console.log(`WheelChart: Setting innermost circle display to ${visible ? 'block' : 'none'}`);
4387
+ innermostCircle.style.display = visible ? 'block' : 'none';
4388
+ } else {
4389
+ console.log('WheelChart: No innermost circle found to toggle visibility');
4390
+ }
4391
+ }
4392
+ } catch (error) {
4393
+ console.error('WheelChart: Error toggling innermost circle visibility:', error);
4394
+ }
4395
+
4396
+ // Also redraw inner elements to ensure everything is in sync
4397
+ this.renderer.renderInnerElements();
4398
+
4399
+ return result;
4400
+ }
4401
+
4402
+ setHouseRotation(angle) {
4403
+ return this._delegateAndRedraw('setHouseRotation', angle);
4404
+ }
4405
+
4406
+ destroy() {
4407
+ return this._delegateToChart('destroy');
4408
+ }
4409
+
4410
+ setHouseSystem(systemName) {
4411
+ return this._delegateAndRedraw('setHouseSystem', systemName);
4412
+ }
4413
+
4414
+ // Add direct access to the chart's config for convenience
4415
+ get config() {
4416
+ return this.chart.config;
4417
+ }
4418
+
4419
+ // Add direct access to updateData method
4420
+ updateData(data) {
4421
+ return this._delegateAndRedraw('updateData', data);
4422
+ }
4423
+
4424
+ // Add direct access to getCurrentHouseSystem method
4425
+ getCurrentHouseSystem() {
4426
+ return this.chart.getCurrentHouseSystem();
4427
+ }
4428
+ }
4429
+
4430
+ /**
4431
+ * Nocturna Wheel JS - Main Entry Point
4432
+ *
4433
+ * This file serves as the primary entry point for the Nocturna Wheel library.
4434
+ * It provides a clean public API with ES module exports.
4435
+ *
4436
+ * Factory Injection Pattern:
4437
+ * The WheelChart supports Factory Injection for better decoupling:
4438
+ *
4439
+ * ```javascript
4440
+ * // Example usage with factory injection
4441
+ * const chartFactory = (opts) => new CustomChart(opts);
4442
+ * const chart = new WheelChart(options, chartFactory);
4443
+ * ```
4444
+ *
4445
+ * See examples/factory-injection-example.js for more advanced usage patterns.
4446
+ */
4447
+
4448
+ // Initialize core services immediately
4449
+ ServiceRegistry.initializeServices();
4450
+
4451
+ // Library version
4452
+ const VERSION = '0.2.0';
4453
+
4454
+ /**
4455
+ * Main entry point for the Nocturna Wheel development environment
4456
+ * This file sets up the chart and handles hot module replacement for Vite
4457
+ */
4458
+
4459
+ // Sample chart data - for development and testing
4460
+ const chartData = {
4461
+ planets: {
4462
+ sun: { lon: 0, lat: 0, color: '#F9A825' },
4463
+ moon: { lon: 45, lat: 0, color: '#CFD8DC' },
4464
+ mercury: { lon: 15, lat: 0, color: '#7E57C2' },
4465
+ venus: { lon: 30, lat: 0, color: '#26A69A' },
4466
+ mars: { lon: 60, lat: 0, color: '#EF5350' },
4467
+ jupiter: { lon: 90, lat: 0, color: '#5C6BC0' },
4468
+ saturn: { lon: 120, lat: 0, color: '#455A64' },
4469
+ uranus: { lon: 150, lat: 0, color: '#42A5F5' },
4470
+ neptune: { lon: 180, lat: 0, color: '#29B6F6' },
4471
+ pluto: { lon: 210, lat: 0, color: '#8D6E63' }
4472
+ },
4473
+ houses: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
4474
+ config: {
4475
+ zodiacSettings: {
4476
+ enabled: true
4477
+ },
4478
+ houseSettings: {
4479
+ enabled: true,
4480
+ rotationAngle: 0
4481
+ },
4482
+ planetSettings: {
4483
+ enabled: true
4484
+ },
4485
+ aspectSettings: {
4486
+ enabled: true
4487
+ }
4488
+ }
4489
+ };
4490
+
4491
+ // Chart instance
4492
+ let chart;
4493
+
4494
+ /**
4495
+ * Initialize the chart
4496
+ */
4497
+ function initChart() {
4498
+ // Get the container element
4499
+ const container = document.getElementById('chart-container');
4500
+
4501
+ // Configuration for the chart
4502
+ const options = {
4503
+ container: container,
4504
+ planets: chartData.planets,
4505
+ houses: chartData.houses,
4506
+ config: chartData.config
4507
+ };
4508
+
4509
+ // Initialize the chart with the new ChartManager
4510
+ chart = new ChartManager(options);
4511
+
4512
+ // Render the chart
4513
+ chart.render();
4514
+
4515
+ // Setup event listeners for controls
4516
+ setupEventListeners();
4517
+ }
4518
+
4519
+ /**
4520
+ * Setup event listeners for various controls
4521
+ */
4522
+ function setupEventListeners() {
4523
+ // Example: House system selector
4524
+ const houseSystemSelect = document.getElementById('house-system');
4525
+ if (houseSystemSelect) {
4526
+ houseSystemSelect.addEventListener('change', (e) => {
4527
+ chart.setHouseSystem(e.target.value);
4528
+ });
4529
+ }
4530
+
4531
+ // Initialize secondary planets toggle to match current state (enabled)
4532
+ const secondaryPlanetsToggle = document.getElementById('toggle-secondary-planets');
4533
+ if (secondaryPlanetsToggle) {
4534
+ secondaryPlanetsToggle.checked = true;
4535
+ }
4536
+
4537
+ // Add more event listeners for other controls as needed
4538
+ }
4539
+
4540
+ /**
4541
+ * Clean up resources when the module is replaced (for Vite HMR)
4542
+ */
4543
+ function cleanup() {
4544
+ if (chart) {
4545
+ chart.destroy();
4546
+ chart = null;
4547
+ }
4548
+ }
4549
+
4550
+ // Initialize the chart when the DOM is ready
4551
+ document.addEventListener('DOMContentLoaded', initChart);
4552
+
4553
+ // Handle hot module replacement for Vite
4554
+ if (import.meta.hot) {
4555
+ import.meta.hot.accept((newModule) => {
4556
+ console.log('HMR update for main.js');
4557
+ cleanup();
4558
+ initChart();
4559
+ });
4560
+
4561
+ // Cleanup when disposing
4562
+ import.meta.hot.dispose(() => {
4563
+ console.log('HMR dispose for main.js');
4564
+ cleanup();
4565
+ });
4566
+ }
4567
+
4568
+ // Default export for easier imports
4569
+ var main = {
4570
+ ChartManager,
4571
+ NocturnaWheel, // For backward compatibility
4572
+ WheelChart,
4573
+ ChartConfig,
4574
+ ServiceRegistry,
4575
+ SvgUtils,
4576
+ AstrologyUtils
4577
+ };
4578
+
4579
+ export { AstrologyUtils, ChartConfig, ChartManager, NocturnaWheel, ServiceRegistry, SvgUtils, VERSION, WheelChart, main as default };
4580
+ //# sourceMappingURL=nocturna-wheel.es.js.map