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