ether-to-astro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.env.example +13 -0
  2. package/.github/pull_request_template.md +16 -0
  3. package/.github/workflows/release.yml +35 -0
  4. package/.github/workflows/test.yml +32 -0
  5. package/AGENTS.md +99 -0
  6. package/LICENSE +18 -0
  7. package/NOTICE.md +45 -0
  8. package/README.md +301 -0
  9. package/SETUP.md +70 -0
  10. package/TESTING_SUMMARY.md +238 -0
  11. package/TEST_SUITE_STATUS.md +218 -0
  12. package/biome.json +48 -0
  13. package/dist/astro-service.d.ts +98 -0
  14. package/dist/astro-service.js +496 -0
  15. package/dist/chart-types.d.ts +52 -0
  16. package/dist/chart-types.js +51 -0
  17. package/dist/charts.d.ts +125 -0
  18. package/dist/charts.js +324 -0
  19. package/dist/cli.d.ts +7 -0
  20. package/dist/cli.js +472 -0
  21. package/dist/constants.d.ts +81 -0
  22. package/dist/constants.js +76 -0
  23. package/dist/eclipses.d.ts +85 -0
  24. package/dist/eclipses.js +184 -0
  25. package/dist/ephemeris.d.ts +120 -0
  26. package/dist/ephemeris.js +379 -0
  27. package/dist/formatter.d.ts +2 -0
  28. package/dist/formatter.js +22 -0
  29. package/dist/houses.d.ts +82 -0
  30. package/dist/houses.js +169 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.js +150 -0
  33. package/dist/loader.d.ts +2 -0
  34. package/dist/loader.js +31 -0
  35. package/dist/logger.d.ts +25 -0
  36. package/dist/logger.js +73 -0
  37. package/dist/profile-store.d.ts +48 -0
  38. package/dist/profile-store.js +156 -0
  39. package/dist/riseset.d.ts +82 -0
  40. package/dist/riseset.js +185 -0
  41. package/dist/storage.d.ts +10 -0
  42. package/dist/storage.js +40 -0
  43. package/dist/time-utils.d.ts +68 -0
  44. package/dist/time-utils.js +136 -0
  45. package/dist/tool-registry.d.ts +35 -0
  46. package/dist/tool-registry.js +307 -0
  47. package/dist/tool-result.d.ts +175 -0
  48. package/dist/tool-result.js +188 -0
  49. package/dist/transits.d.ts +108 -0
  50. package/dist/transits.js +263 -0
  51. package/dist/types.d.ts +450 -0
  52. package/dist/types.js +161 -0
  53. package/example-usage.md +131 -0
  54. package/natal-chart.json +187 -0
  55. package/package.json +61 -0
  56. package/scripts/download-ephemeris.js +115 -0
  57. package/setup.sh +21 -0
  58. package/src/astro-service.ts +710 -0
  59. package/src/chart-types.ts +125 -0
  60. package/src/charts.ts +399 -0
  61. package/src/cli.ts +694 -0
  62. package/src/constants.ts +89 -0
  63. package/src/eclipses.ts +226 -0
  64. package/src/ephemeris.ts +437 -0
  65. package/src/formatter.ts +25 -0
  66. package/src/houses.ts +202 -0
  67. package/src/index.ts +170 -0
  68. package/src/loader.ts +36 -0
  69. package/src/logger.ts +104 -0
  70. package/src/profile-store.ts +285 -0
  71. package/src/riseset.ts +229 -0
  72. package/src/time-utils.ts +167 -0
  73. package/src/tool-registry.ts +357 -0
  74. package/src/tool-result.ts +283 -0
  75. package/src/transits.ts +352 -0
  76. package/src/types.ts +547 -0
  77. package/tests/README.md +173 -0
  78. package/tests/TESTING_STRATEGY.md +178 -0
  79. package/tests/fixtures/bowen-yang-chart.ts +69 -0
  80. package/tests/fixtures/calculate-expected.ts +81 -0
  81. package/tests/fixtures/expected-results.ts +117 -0
  82. package/tests/fixtures/generate-expected-simple.ts +94 -0
  83. package/tests/helpers/date-fixtures.ts +15 -0
  84. package/tests/helpers/ephem.ts +11 -0
  85. package/tests/helpers/temp.ts +9 -0
  86. package/tests/setup.ts +11 -0
  87. package/tests/unit/astro-service.test.ts +323 -0
  88. package/tests/unit/chart-types.test.ts +18 -0
  89. package/tests/unit/charts-errors.test.ts +42 -0
  90. package/tests/unit/charts.test.ts +157 -0
  91. package/tests/unit/cli-commands.test.ts +82 -0
  92. package/tests/unit/cli-profiles.test.ts +128 -0
  93. package/tests/unit/cli.test.ts +191 -0
  94. package/tests/unit/constants.test.ts +26 -0
  95. package/tests/unit/correctness-critical.test.ts +408 -0
  96. package/tests/unit/eclipses.test.ts +108 -0
  97. package/tests/unit/ephemeris.test.ts +213 -0
  98. package/tests/unit/error-handling.test.ts +116 -0
  99. package/tests/unit/formatter.test.ts +29 -0
  100. package/tests/unit/houses-errors.test.ts +27 -0
  101. package/tests/unit/houses-validation.test.ts +164 -0
  102. package/tests/unit/houses.test.ts +205 -0
  103. package/tests/unit/profile-store.test.ts +163 -0
  104. package/tests/unit/real-user-charts.test.ts +148 -0
  105. package/tests/unit/riseset.test.ts +106 -0
  106. package/tests/unit/solver-edges.test.ts +197 -0
  107. package/tests/unit/time-utils-temporal.test.ts +303 -0
  108. package/tests/unit/time-utils.test.ts +173 -0
  109. package/tests/unit/tool-registry.test.ts +222 -0
  110. package/tests/unit/tool-result.test.ts +45 -0
  111. package/tests/unit/transit-correctness.test.ts +78 -0
  112. package/tests/unit/transits.test.ts +238 -0
  113. package/tests/validation/README.md +32 -0
  114. package/tests/validation/adapters/astrolog.ts +306 -0
  115. package/tests/validation/adapters/internal.ts +184 -0
  116. package/tests/validation/compare/eclipses.ts +47 -0
  117. package/tests/validation/compare/houses.ts +76 -0
  118. package/tests/validation/compare/positions.ts +104 -0
  119. package/tests/validation/compare/riseSet.ts +48 -0
  120. package/tests/validation/compare/roots.ts +90 -0
  121. package/tests/validation/compare/transits.ts +69 -0
  122. package/tests/validation/fixtures/astrolog-parity/core.ts +194 -0
  123. package/tests/validation/fixtures/eclipses/core.ts +14 -0
  124. package/tests/validation/fixtures/houses/core.ts +47 -0
  125. package/tests/validation/fixtures/positions/core.ts +159 -0
  126. package/tests/validation/fixtures/rise-set/core.ts +20 -0
  127. package/tests/validation/fixtures/roots/core.ts +47 -0
  128. package/tests/validation/fixtures/transits/core.ts +61 -0
  129. package/tests/validation/fixtures/transits/dst.ts +21 -0
  130. package/tests/validation/oracle.spec.ts +129 -0
  131. package/tests/validation/utils/denseRootOracle.ts +269 -0
  132. package/tests/validation/utils/fixtureTypes.ts +146 -0
  133. package/tests/validation/utils/report.ts +60 -0
  134. package/tests/validation/utils/tolerances.ts +23 -0
  135. package/tests/validation/validation.spec.ts +836 -0
  136. package/tools/color-picker.html +388 -0
  137. package/tsconfig.json +17 -0
  138. package/vitest.config.ts +31 -0
@@ -0,0 +1,125 @@
1
+ // Type definitions for AstroChart library data structures
2
+ import {
3
+ DARK_ASPECT_COLORS,
4
+ DARK_THEME_COLORS,
5
+ LIGHT_ASPECT_COLORS,
6
+ LIGHT_THEME_COLORS,
7
+ } from './constants.js';
8
+
9
+ // Type for AstroChart library
10
+ export interface AstroChartConstructor {
11
+ new (
12
+ elementId: string,
13
+ width: number,
14
+ height: number,
15
+ settings: Partial<AstroChartSettings>
16
+ ): AstroChartInstance;
17
+ default?: AstroChartConstructor;
18
+ }
19
+
20
+ export interface AstroChartInstance {
21
+ radix(data: AstroChartData): RadixChart;
22
+ }
23
+
24
+ export interface RadixChart {
25
+ aspects(): void;
26
+ transit(data: AstroChartData): void;
27
+ }
28
+
29
+ export interface AstroChartData {
30
+ planets: AstroChartPlanets;
31
+ cusps: number[];
32
+ }
33
+
34
+ export interface AstroChartPlanets {
35
+ [planetName: string]: number[];
36
+ }
37
+
38
+ export interface AstroChartSettings {
39
+ SYMBOL_SCALE?: number;
40
+ STROKE_ONLY?: boolean;
41
+ COLOR_BACKGROUND?: string;
42
+ CIRCLE_COLOR?: string;
43
+ LINE_COLOR?: string;
44
+ POINTS_COLOR?: string;
45
+ SIGNS_COLOR?: string;
46
+ CUSPS_FONT_COLOR?: string;
47
+ SYMBOL_AXIS_FONT_COLOR?: string;
48
+ COLOR_ARIES?: string;
49
+ COLOR_TAURUS?: string;
50
+ COLOR_GEMINI?: string;
51
+ COLOR_CANCER?: string;
52
+ COLOR_LEO?: string;
53
+ COLOR_VIRGO?: string;
54
+ COLOR_LIBRA?: string;
55
+ COLOR_SCORPIO?: string;
56
+ COLOR_SAGITTARIUS?: string;
57
+ COLOR_CAPRICORN?: string;
58
+ COLOR_AQUARIUS?: string;
59
+ COLOR_PISCES?: string;
60
+ COLOR_SIGNS?: string[];
61
+ ASPECTS?: {
62
+ [key: string]: {
63
+ degree: number;
64
+ orbit: number;
65
+ color: string;
66
+ };
67
+ };
68
+ }
69
+
70
+ export type ChartTheme = 'light' | 'dark';
71
+ export type ChartFormat = 'svg' | 'png' | 'webp';
72
+
73
+ export function getThemeSettings(
74
+ theme: ChartTheme,
75
+ transparent = false
76
+ ): Partial<AstroChartSettings> {
77
+ if (theme === 'dark') {
78
+ return {
79
+ COLOR_BACKGROUND: transparent ? 'transparent' : '#282c34',
80
+ CIRCLE_COLOR: '#4b5263',
81
+ LINE_COLOR: '#4b5263',
82
+ POINTS_COLOR: '#abb2bf',
83
+ SIGNS_COLOR: '#d7dae0',
84
+ CUSPS_FONT_COLOR: '#abb2bf',
85
+ SYMBOL_AXIS_FONT_COLOR: '#abb2bf',
86
+ ASPECTS: DARK_ASPECT_COLORS,
87
+ COLOR_ARIES: DARK_THEME_COLORS[0],
88
+ COLOR_TAURUS: DARK_THEME_COLORS[1],
89
+ COLOR_GEMINI: DARK_THEME_COLORS[2],
90
+ COLOR_CANCER: DARK_THEME_COLORS[3],
91
+ COLOR_LEO: DARK_THEME_COLORS[4],
92
+ COLOR_VIRGO: DARK_THEME_COLORS[5],
93
+ COLOR_LIBRA: DARK_THEME_COLORS[6],
94
+ COLOR_SCORPIO: DARK_THEME_COLORS[7],
95
+ COLOR_SAGITTARIUS: DARK_THEME_COLORS[8],
96
+ COLOR_CAPRICORN: DARK_THEME_COLORS[9],
97
+ COLOR_AQUARIUS: DARK_THEME_COLORS[10],
98
+ COLOR_PISCES: DARK_THEME_COLORS[11],
99
+ };
100
+ }
101
+
102
+ // Light theme (defaults)
103
+ return {
104
+ COLOR_BACKGROUND: transparent ? 'transparent' : '#ffffff',
105
+ CIRCLE_COLOR: '#333333',
106
+ LINE_COLOR: '#333333',
107
+ POINTS_COLOR: '#000000',
108
+ SIGNS_COLOR: '#000000',
109
+ CUSPS_FONT_COLOR: '#000000',
110
+ SYMBOL_AXIS_FONT_COLOR: '#333333',
111
+ ASPECTS: LIGHT_ASPECT_COLORS,
112
+ COLOR_ARIES: LIGHT_THEME_COLORS[0],
113
+ COLOR_TAURUS: LIGHT_THEME_COLORS[1],
114
+ COLOR_GEMINI: LIGHT_THEME_COLORS[2],
115
+ COLOR_CANCER: LIGHT_THEME_COLORS[3],
116
+ COLOR_LEO: LIGHT_THEME_COLORS[4],
117
+ COLOR_VIRGO: LIGHT_THEME_COLORS[5],
118
+ COLOR_LIBRA: LIGHT_THEME_COLORS[6],
119
+ COLOR_SCORPIO: LIGHT_THEME_COLORS[7],
120
+ COLOR_SAGITTARIUS: LIGHT_THEME_COLORS[8],
121
+ COLOR_CAPRICORN: LIGHT_THEME_COLORS[9],
122
+ COLOR_AQUARIUS: LIGHT_THEME_COLORS[10],
123
+ COLOR_PISCES: LIGHT_THEME_COLORS[11],
124
+ };
125
+ }
package/src/charts.ts ADDED
@@ -0,0 +1,399 @@
1
+ import Chart from '@astrodraw/astrochart';
2
+ import { JSDOM } from 'jsdom';
3
+ import sharp from 'sharp';
4
+ import {
5
+ type AstroChartConstructor,
6
+ type AstroChartData,
7
+ type AstroChartSettings,
8
+ type ChartFormat,
9
+ type ChartTheme,
10
+ getThemeSettings,
11
+ } from './chart-types.js';
12
+ import type { EphemerisCalculator } from './ephemeris.js';
13
+ import type { HouseCalculator } from './houses.js';
14
+ import { type NatalChart, PLANETS } from './types.js';
15
+
16
+ /**
17
+ * Renderer for astrological charts using SVG
18
+ *
19
+ * @remarks
20
+ * Generates natal and transit charts as SVG images using the astrochart library.
21
+ * Converts SVG to PNG/WebP formats for output. Uses JSDOM to provide
22
+ * browser environment for the chart library.
23
+ */
24
+ export class ChartRenderer {
25
+ /** Ephemeris calculator instance */
26
+ private ephem: EphemerisCalculator;
27
+ /** House calculator instance */
28
+ private houseCalc: HouseCalculator;
29
+ /** Virtual DOM for chart rendering */
30
+ private dom: JSDOM;
31
+
32
+ /**
33
+ * Create a new chart renderer
34
+ *
35
+ * @param ephem - Initialized ephemeris calculator
36
+ * @param houseCalc - Initialized house calculator
37
+ *
38
+ * @remarks
39
+ * Both calculators must be initialized before passing to the constructor.
40
+ * Sets up a virtual DOM environment for the astrochart library.
41
+ */
42
+ constructor(ephem: EphemerisCalculator, houseCalc: HouseCalculator) {
43
+ this.ephem = ephem;
44
+ this.houseCalc = houseCalc;
45
+
46
+ // Create virtual DOM
47
+ this.dom = new JSDOM(
48
+ '<!DOCTYPE html><html><body><div id="chart-container"></div></body></html>'
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Setup global browser variables for astrochart library
54
+ *
55
+ * @remarks
56
+ * The astrochart library expects browser globals like document and window.
57
+ * This method provides those from the virtual DOM. Required because
58
+ * astrochart was designed for browser environments.
59
+ */
60
+ private setupGlobals(): void {
61
+ // Set global document and window for astrochart
62
+ // Note: Required by astrochart library which expects browser globals
63
+ const g = global as typeof globalThis;
64
+ (g as any).document = this.dom.window.document;
65
+ (g as any).window = this.dom.window;
66
+ (g as any).SVGElement = this.dom.window.SVGElement;
67
+ (g as any).self = this.dom.window;
68
+ }
69
+
70
+ /**
71
+ * Clear the chart container element
72
+ *
73
+ * @remarks
74
+ * Removes any previous chart content to ensure clean rendering.
75
+ * Called before generating each new chart.
76
+ */
77
+ private clearContainer(): void {
78
+ const container = this.dom.window.document.getElementById('chart-container');
79
+ if (container) {
80
+ container.innerHTML = '';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generate a natal chart visualization
86
+ *
87
+ * @param natalChart - Birth chart data with julianDay and houseSystem
88
+ * @param theme - Visual theme for the chart (default: 'light')
89
+ * @param format - Output format (default: 'svg')
90
+ * @returns Chart image as buffer or data URL
91
+ * @throws Error if natal chart is invalid or rendering fails
92
+ *
93
+ * @remarks
94
+ * Renders a complete natal chart with planets, houses, and aspects.
95
+ * Requires julianDay to be set in the natalChart.
96
+ */
97
+ async generateNatalChart(
98
+ natalChart: NatalChart,
99
+ theme: ChartTheme = 'light',
100
+ format: ChartFormat = 'svg'
101
+ ): Promise<Buffer | string> {
102
+ this.setupGlobals();
103
+ this.clearContainer();
104
+
105
+ // Require Julian Day (always set by set_natal_chart)
106
+ if (!natalChart.julianDay) {
107
+ throw new Error(
108
+ 'Natal chart missing Julian Day - chart may be from old session. Please call set_natal_chart again.'
109
+ );
110
+ }
111
+ const jd = natalChart.julianDay;
112
+
113
+ // Get only renderable planet positions (those mapped in getPlanetKey)
114
+ const renderablePlanetIds = [
115
+ PLANETS.SUN,
116
+ PLANETS.MOON,
117
+ PLANETS.MERCURY,
118
+ PLANETS.VENUS,
119
+ PLANETS.MARS,
120
+ PLANETS.JUPITER,
121
+ PLANETS.SATURN,
122
+ PLANETS.URANUS,
123
+ PLANETS.NEPTUNE,
124
+ PLANETS.PLUTO,
125
+ PLANETS.CHIRON,
126
+ PLANETS.MEAN_NODE, // Use mean node, not true node
127
+ ];
128
+ const positions = this.ephem.getAllPlanets(jd, renderablePlanetIds);
129
+
130
+ // Get houses using stored house system preference
131
+ const houseSystem = natalChart.houseSystem || 'P';
132
+ const houses = this.houseCalc.calculateHouses(
133
+ jd,
134
+ natalChart.location.latitude,
135
+ natalChart.location.longitude,
136
+ houseSystem
137
+ );
138
+
139
+ // Convert to AstroChart format
140
+ const data: AstroChartData = {
141
+ planets: {},
142
+ cusps: Array.from(houses.cusps).slice(1, 13), // Houses 1-12
143
+ };
144
+
145
+ // Map planet positions
146
+ positions.forEach((p) => {
147
+ const planetKey = this.getPlanetKey(p.planet);
148
+ if (planetKey) {
149
+ data.planets[planetKey] = [p.longitude];
150
+ }
151
+ });
152
+
153
+ // Create chart with theme colors
154
+ const settings: AstroChartSettings = {
155
+ SYMBOL_SCALE: 1.2,
156
+ STROKE_ONLY: false,
157
+ ...getThemeSettings(theme, false),
158
+ };
159
+ const ChartClass = ((Chart as unknown as AstroChartConstructor).default ||
160
+ Chart) as AstroChartConstructor;
161
+ const chart = new ChartClass('chart-container', 800, 800, settings);
162
+
163
+ // Generate SVG
164
+ const radix = chart.radix(data);
165
+ radix.aspects();
166
+ const svgString = this.extractSVG();
167
+
168
+ // Convert to requested format
169
+ if (format === 'svg') {
170
+ return svgString;
171
+ }
172
+ return this.convertToImage(svgString, format, theme);
173
+ }
174
+
175
+ /**
176
+ * Prepare chart data for astrochart library
177
+ *
178
+ * @param natalChart - Birth chart data
179
+ * @param transitDate - Optional date for transit calculations
180
+ * @returns Chart data in astrochart format
181
+ * @throws Error if julianDay is not set
182
+ *
183
+ * @remarks
184
+ * Converts internal chart format to astrochart's expected format.
185
+ * Includes planets, houses, and optionally transit positions.
186
+ */
187
+ private prepareChartData(natalChart: NatalChart, transitDate?: Date): AstroChartData {
188
+ // Require Julian Day (always set by set_natal_chart)
189
+ if (!natalChart.julianDay) {
190
+ throw new Error(
191
+ 'Natal chart missing Julian Day - chart may be from old session. Please call set_natal_chart again.'
192
+ );
193
+ }
194
+ const jd = natalChart.julianDay;
195
+
196
+ // Get houses using stored house system preference
197
+ const houseSystem = natalChart.houseSystem || 'P';
198
+ const houses = this.houseCalc.calculateHouses(
199
+ jd,
200
+ natalChart.location.latitude,
201
+ natalChart.location.longitude,
202
+ houseSystem
203
+ );
204
+
205
+ // Convert to AstroChart format
206
+ const data: AstroChartData = {
207
+ planets: {},
208
+ cusps: Array.from(houses.cusps).slice(1, 13), // Houses 1-12
209
+ };
210
+
211
+ // Define renderable planets for charts
212
+ const renderablePlanetIds = [
213
+ PLANETS.SUN,
214
+ PLANETS.MOON,
215
+ PLANETS.MERCURY,
216
+ PLANETS.VENUS,
217
+ PLANETS.MARS,
218
+ PLANETS.JUPITER,
219
+ PLANETS.SATURN,
220
+ PLANETS.URANUS,
221
+ PLANETS.NEPTUNE,
222
+ PLANETS.PLUTO,
223
+ PLANETS.CHIRON,
224
+ PLANETS.MEAN_NODE,
225
+ ];
226
+
227
+ // Prefill natal planets from julianDay to ensure all renderable planets have positions
228
+ // This avoids fake 0° placeholders for planets missing from natalChart.planets
229
+ const natalPositions = this.ephem.getAllPlanets(jd, renderablePlanetIds);
230
+ natalPositions.forEach((p) => {
231
+ const planetKey = this.getPlanetKey(p.planet);
232
+ if (planetKey) {
233
+ data.planets[planetKey] = [p.longitude];
234
+ }
235
+ });
236
+
237
+ // Add transit planets if transit date provided
238
+ if (transitDate) {
239
+ const transitJD = this.ephem.dateToJulianDay(transitDate);
240
+ const transitPositions = this.ephem.getAllPlanets(transitJD, renderablePlanetIds);
241
+
242
+ transitPositions.forEach((p) => {
243
+ const planetKey = this.getPlanetKey(p.planet);
244
+ if (planetKey) {
245
+ // Add transit position as second element in array
246
+ // Natal position already exists from prefill above
247
+ const current = data.planets[planetKey];
248
+ if (current) {
249
+ data.planets[planetKey] = [current[0], p.longitude];
250
+ }
251
+ }
252
+ });
253
+ }
254
+
255
+ return data;
256
+ }
257
+
258
+ /**
259
+ * Generate a transit chart visualization
260
+ *
261
+ * @param natalChart - Birth chart data with julianDay and houseSystem
262
+ * @param transitDate - Date for transit calculation
263
+ * @param theme - Visual theme for the chart (default: 'light')
264
+ * @param format - Output format (default: 'svg')
265
+ * @returns Chart image as buffer or data URL
266
+ * @throws Error if natal chart is invalid or rendering fails
267
+ *
268
+ * @remarks
269
+ * Shows both natal positions (inner wheel) and current transits (outer wheel).
270
+ * Requires julianDay to be set in the natalChart.
271
+ */
272
+ async generateTransitChart(
273
+ natalChart: NatalChart,
274
+ transitDate: Date,
275
+ theme: ChartTheme = 'light',
276
+ format: ChartFormat = 'svg'
277
+ ): Promise<Buffer | string> {
278
+ this.setupGlobals();
279
+ this.clearContainer();
280
+
281
+ const data = this.prepareChartData(natalChart, transitDate);
282
+
283
+ // Create chart with theme colors
284
+ const settings: AstroChartSettings = {
285
+ SYMBOL_SCALE: 1.2,
286
+ STROKE_ONLY: false,
287
+ ...getThemeSettings(theme, false),
288
+ };
289
+ const ChartClass = ((Chart as unknown as AstroChartConstructor).default ||
290
+ Chart) as AstroChartConstructor;
291
+ const chart = new ChartClass('chart-container', 800, 800, settings);
292
+
293
+ // Generate SVG
294
+ const radix = chart.radix(data);
295
+ radix.aspects();
296
+ radix.transit(data);
297
+ const svgString = this.extractSVG();
298
+
299
+ // Convert to requested format
300
+ if (format === 'svg') {
301
+ return svgString;
302
+ }
303
+ return this.convertToImage(svgString, format, theme);
304
+ }
305
+
306
+ /**
307
+ * Map internal planet names to astrochart keys
308
+ *
309
+ * @param planetName - Internal planet name
310
+ * @returns Astrochart planet key or null if not supported
311
+ *
312
+ * @remarks
313
+ * Maps our planet names to the keys expected by astrochart.
314
+ * Uses mean node to avoid collision with true node.
315
+ */
316
+ private getPlanetKey(planetName: string): string | null {
317
+ const mapping: { [key: string]: string } = {
318
+ Sun: 'Sun',
319
+ Moon: 'Moon',
320
+ Mercury: 'Mercury',
321
+ Venus: 'Venus',
322
+ Mars: 'Mars',
323
+ Jupiter: 'Jupiter',
324
+ Saturn: 'Saturn',
325
+ Uranus: 'Uranus',
326
+ Neptune: 'Neptune',
327
+ Pluto: 'Pluto',
328
+ Chiron: 'Chiron',
329
+ 'North Node (Mean)': 'NNode', // Use mean node for consistency
330
+ // True node intentionally omitted - prevents collision/overwrite
331
+ };
332
+
333
+ return mapping[planetName] || null;
334
+ }
335
+
336
+ /**
337
+ * Extract SVG string from virtual DOM
338
+ *
339
+ * @returns SVG string
340
+ * @throws Error if no SVG element found
341
+ *
342
+ * @remarks
343
+ * Finds the SVG element in the virtual DOM and returns
344
+ * its outer HTML. Throws if no chart was rendered.
345
+ */
346
+ private extractSVG(): string {
347
+ // Get the SVG element from the virtual DOM
348
+ const container = this.dom.window.document.getElementById('chart-container');
349
+ if (!container) {
350
+ throw new Error('Chart container not found - DOM setup failed');
351
+ }
352
+
353
+ const svg = container.querySelector('svg');
354
+ if (!svg) {
355
+ throw new Error('Chart rendering failed - no SVG element generated by astrochart library');
356
+ }
357
+
358
+ return svg.outerHTML;
359
+ }
360
+
361
+ /**
362
+ * Convert SVG to specified output format
363
+ *
364
+ * @param svg - SVG string or buffer
365
+ * @param format - Target output format
366
+ * @returns Converted image as buffer (png/webp) or data URL (svg)
367
+ * @throws Error if conversion fails
368
+ *
369
+ * @remarks
370
+ * Uses Sharp library for PNG/WebP conversion. SVG is returned
371
+ * as a data URL for direct use in web contexts.
372
+ */
373
+ private async convertToImage(
374
+ svg: string,
375
+ format: ChartFormat,
376
+ theme: ChartTheme
377
+ ): Promise<Buffer> {
378
+ const buffer = Buffer.from(svg);
379
+
380
+ // Use theme-appropriate background color
381
+ const bgColor =
382
+ theme === 'dark'
383
+ ? { r: 40, g: 44, b: 52, alpha: 1 } // #282c34
384
+ : { r: 255, g: 255, b: 255, alpha: 1 }; // #ffffff
385
+
386
+ if (format === 'png') {
387
+ return sharp(buffer)
388
+ .flatten({ background: bgColor })
389
+ .png({ quality: 100, compressionLevel: 6 })
390
+ .toBuffer();
391
+ }
392
+
393
+ // WebP
394
+ return sharp(buffer)
395
+ .flatten({ background: bgColor })
396
+ .webp({ quality: 95, effort: 6 })
397
+ .toBuffer();
398
+ }
399
+ }