chart2txt 0.6.0 → 0.7.1

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/README.md +103 -34
  2. package/dist/chart2txt.d.ts +9 -0
  3. package/dist/chart2txt.js +30 -0
  4. package/dist/chart2txt.min.js +1 -1
  5. package/dist/config/ChartSettings.d.ts +10 -6
  6. package/dist/config/ChartSettings.js +22 -11
  7. package/dist/constants.d.ts +17 -2
  8. package/dist/constants.js +303 -34
  9. package/dist/core/analysis.d.ts +6 -0
  10. package/dist/core/analysis.js +237 -0
  11. package/dist/core/aspectPatterns.d.ts +8 -3
  12. package/dist/core/aspectPatterns.js +234 -218
  13. package/dist/core/aspects.d.ts +14 -11
  14. package/dist/core/aspects.js +49 -32
  15. package/dist/core/dignities.d.ts +2 -27
  16. package/dist/core/dignities.js +56 -121
  17. package/dist/core/dispositors.d.ts +7 -19
  18. package/dist/core/dispositors.js +152 -126
  19. package/dist/core/grouping.d.ts +9 -0
  20. package/dist/core/grouping.js +45 -0
  21. package/dist/core/signDistributions.d.ts +20 -30
  22. package/dist/core/signDistributions.js +25 -122
  23. package/dist/core/stelliums.d.ts +10 -0
  24. package/dist/core/stelliums.js +108 -0
  25. package/dist/formatters/text/sections/aspectPatterns.d.ts +3 -1
  26. package/dist/formatters/text/sections/aspectPatterns.js +118 -94
  27. package/dist/formatters/text/sections/aspects.d.ts +3 -6
  28. package/dist/formatters/text/sections/aspects.js +35 -52
  29. package/dist/formatters/text/sections/dispositors.d.ts +4 -3
  30. package/dist/formatters/text/sections/dispositors.js +12 -8
  31. package/dist/formatters/text/sections/houseOverlays.d.ts +11 -6
  32. package/dist/formatters/text/sections/houseOverlays.js +37 -44
  33. package/dist/formatters/text/sections/metadata.d.ts +2 -0
  34. package/dist/formatters/text/sections/metadata.js +54 -0
  35. package/dist/formatters/text/sections/planets.d.ts +3 -5
  36. package/dist/formatters/text/sections/planets.js +11 -22
  37. package/dist/formatters/text/sections/signDistributions.d.ts +9 -25
  38. package/dist/formatters/text/sections/signDistributions.js +9 -55
  39. package/dist/formatters/text/textFormatter.d.ts +4 -5
  40. package/dist/formatters/text/textFormatter.js +86 -142
  41. package/dist/index.d.ts +7 -4
  42. package/dist/index.js +11 -6
  43. package/dist/types.d.ts +102 -15
  44. package/dist/types.js +15 -0
  45. package/dist/utils/formatting.d.ts +4 -0
  46. package/dist/utils/formatting.js +43 -0
  47. package/dist/utils/houseCalculations.d.ts +10 -13
  48. package/dist/utils/houseCalculations.js +15 -57
  49. package/package.json +1 -1
@@ -1,143 +1,169 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analyzeDispositors = analyzeDispositors;
4
- exports.formatDispositorAnalysis = formatDispositorAnalysis;
5
- const astrology_1 = require("./astrology");
6
- const dignities_1 = require("./dignities");
3
+ exports.calculateDispositors = calculateDispositors;
4
+ const constants_1 = require("../constants");
5
+ const formatting_1 = require("../utils/formatting");
7
6
  /**
8
- * Builds dispositor chains for all planets
9
- * @param planets Array of planet points
10
- * @returns Complete dispositor analysis
7
+ * Calculates the dispositor for a single planet.
8
+ * @param planet The planet to find the dispositor for.
9
+ * @returns The name of the dispositor planet.
11
10
  */
12
- function analyzeDispositors(planets) {
13
- const dispositorMap = new Map();
14
- const disposedByMap = new Map();
15
- // Build the dispositor relationships
16
- planets.forEach((planet) => {
17
- const sign = (0, astrology_1.getDegreeSign)(planet.degree);
18
- const rulers = (0, dignities_1.getSignRulers)(sign);
19
- dispositorMap.set(planet.name, rulers);
20
- rulers.forEach((ruler) => {
21
- if (!disposedByMap.has(ruler)) {
22
- disposedByMap.set(ruler, []);
23
- }
24
- disposedByMap.get(ruler).push(planet.name);
25
- });
26
- });
27
- // Find final dispositors - these are the roots of dispositor trees
28
- const finalDispositors = [];
29
- const planetNames = new Set(planets.map((p) => p.name));
30
- planets.forEach((planet) => {
31
- const rulers = dispositorMap.get(planet.name) || [];
32
- const isRuledBySelf = rulers.includes(planet.name);
33
- const isRuledByPlanetNotInChart = rulers.length > 0 && !rulers.some((ruler) => planetNames.has(ruler));
34
- // A planet is a final dispositor if:
35
- // 1. It rules itself (traditional dignity)
36
- // 2. It's ruled by a planet not in the chart
37
- if (isRuledBySelf || isRuledByPlanetNotInChart) {
38
- finalDispositors.push(planet.name);
39
- }
40
- });
41
- // Build chains
42
- const chains = planets.map((planet) => ({
43
- planet: planet.name,
44
- disposedBy: dispositorMap.get(planet.name) || [],
45
- disposes: disposedByMap.get(planet.name) || [],
46
- }));
47
- return {
48
- chains,
49
- finalDispositors,
50
- };
11
+ function getDispositor(planet) {
12
+ const sign = (0, formatting_1.getSign)(planet.degree);
13
+ const signData = constants_1.ZODIAC_SIGN_DATA.find((s) => s.name === sign);
14
+ return signData ? signData.ruler : 'Unknown';
51
15
  }
52
16
  /**
53
- * Detect cycles in dispositor chains
17
+ * Analyzes the dispositor graph for a single planet, using memoization to avoid re-computation.
18
+ * @param startPlanet - The planet to begin the analysis from.
19
+ * @param dispositorMap - A map of each planet to its direct dispositor.
20
+ * @param cache - A cache of already computed results to avoid redundant work.
21
+ * @returns The analysis result for the starting planet.
54
22
  */
55
- function findDispositorCycles(chains) {
56
- const cycles = [];
57
- const visited = new Set();
58
- const path = new Set();
59
- function dfs(planet, currentPath) {
60
- if (path.has(planet)) {
61
- // Found a cycle
62
- const cycleStart = currentPath.indexOf(planet);
63
- if (cycleStart !== -1) {
64
- const cycle = currentPath.slice(cycleStart);
65
- cycle.push(planet); // Complete the cycle
66
- if (cycle.length > 2) {
67
- cycles.push(cycle);
68
- }
69
- }
70
- return;
23
+ function analyzePlanetChain(startPlanet, dispositorMap, cache) {
24
+ if (cache[startPlanet]) {
25
+ return cache[startPlanet];
26
+ }
27
+ const path = [startPlanet];
28
+ let current = startPlanet;
29
+ // eslint-disable-next-line no-constant-condition
30
+ while (true) {
31
+ const nextDispositor = dispositorMap[current];
32
+ if (!nextDispositor || !dispositorMap.hasOwnProperty(nextDispositor)) {
33
+ // Chain is broken (dispositor not in the chart)
34
+ const result = {
35
+ path: [...path, nextDispositor],
36
+ isCycle: false,
37
+ isFinal: false,
38
+ isBroken: true,
39
+ };
40
+ return (cache[startPlanet] = result);
71
41
  }
72
- if (visited.has(planet))
73
- return;
74
- visited.add(planet);
75
- path.add(planet);
76
- currentPath.push(planet);
77
- const chain = chains.find((c) => c.planet === planet);
78
- if (chain) {
79
- // Follow the dispositor chain (only follow planets in the chart)
80
- const inChartRulers = chain.disposedBy.filter((ruler) => chains.some((c) => c.planet === ruler));
81
- for (const ruler of inChartRulers) {
82
- dfs(ruler, [...currentPath]);
83
- }
42
+ if (nextDispositor === current) {
43
+ // Final dispositor
44
+ const result = {
45
+ path,
46
+ isCycle: false,
47
+ isFinal: true,
48
+ isBroken: false,
49
+ };
50
+ return (cache[startPlanet] = result);
84
51
  }
85
- path.delete(planet);
86
- currentPath.pop();
87
- }
88
- // Start DFS from each planet
89
- chains.forEach((chain) => {
90
- if (!visited.has(chain.planet)) {
91
- dfs(chain.planet, []);
52
+ if (path.includes(nextDispositor)) {
53
+ // Cycle detected
54
+ const cycleStartIndex = path.indexOf(nextDispositor);
55
+ const cyclePath = path.slice(cycleStartIndex);
56
+ const result = {
57
+ path: [...path, nextDispositor],
58
+ isCycle: true,
59
+ isFinal: false,
60
+ isBroken: false,
61
+ };
62
+ // Cache the result for all members of the cycle
63
+ cyclePath.forEach((planet) => {
64
+ cache[planet] = result;
65
+ });
66
+ return result;
92
67
  }
93
- });
94
- return cycles;
68
+ // If we've hit a node that's already been analyzed, we can use its result
69
+ if (cache[nextDispositor]) {
70
+ const nestedResult = cache[nextDispositor];
71
+ const combinedPath = [...path, ...nestedResult.path];
72
+ const result = { ...nestedResult, path: combinedPath };
73
+ return (cache[startPlanet] = result);
74
+ }
75
+ path.push(nextDispositor);
76
+ current = nextDispositor;
77
+ }
95
78
  }
96
79
  /**
97
- * Builds the full dispositor chain for a planet
98
- * @param planet Starting planet
99
- * @param chains All dispositor chains
100
- * @param pathSoFar Array to track the current path (prevents infinite loops)
101
- * @returns Array of planets in the chain (excluding the starting planet)
80
+ * Calculates the full dispositor chain for each planet in the chart.
81
+ * @param planets The list of planets in the chart.
82
+ * @param mode Controls which dispositor chains to include: true (all), false (none), 'finals' (only final dispositors and cycles).
83
+ * @returns A map of each planet to its full dispositor chain string, or a summary in 'finals' mode.
102
84
  */
103
- function buildDispositorChain(planet, chains, pathSoFar = []) {
104
- const chain = chains.find((c) => c.planet === planet);
105
- if (!chain || chain.disposedBy.length === 0) {
106
- return ['(final)'];
85
+ function calculateDispositors(planets, mode = true) {
86
+ if (mode === false) {
87
+ return {};
107
88
  }
108
- // Get the first in-chart ruler
109
- const inChartRulers = chain.disposedBy.filter((ruler) => chains.some((c) => c.planet === ruler));
110
- if (inChartRulers.length === 0) {
111
- return ['(final)'];
112
- }
113
- const ruler = inChartRulers[0];
114
- // If the ruler is the same as the planet (self-ruler), it's final
115
- if (ruler === planet) {
116
- return ['(final)'];
117
- }
118
- // If we've seen this ruler in the current path, we have a cycle
119
- if (pathSoFar.includes(ruler)) {
120
- return ['(cycle)'];
89
+ const dispositorMap = {};
90
+ planets.forEach((p) => {
91
+ dispositorMap[p.name] = getDispositor(p);
92
+ });
93
+ const analysisCache = {};
94
+ planets.forEach((p) => {
95
+ if (!analysisCache[p.name]) {
96
+ analyzePlanetChain(p.name, dispositorMap, analysisCache);
97
+ }
98
+ });
99
+ if (mode === 'finals') {
100
+ const finalDispositors = new Set();
101
+ const cycles = new Map();
102
+ planets.forEach((p) => {
103
+ if (dispositorMap[p.name] === p.name) {
104
+ finalDispositors.add(p.name);
105
+ }
106
+ });
107
+ for (const planetName in analysisCache) {
108
+ const result = analysisCache[planetName];
109
+ if (result.isCycle) {
110
+ const lastPlanetInPath = result.path[result.path.length - 1];
111
+ const cycleStartIndex = result.path.indexOf(lastPlanetInPath);
112
+ const cyclePlanets = result.path.slice(cycleStartIndex, -1);
113
+ const canonicalKey = [...new Set(cyclePlanets)].sort().join('|');
114
+ if (!cycles.has(canonicalKey)) {
115
+ cycles.set(canonicalKey, cyclePlanets);
116
+ }
117
+ }
118
+ }
119
+ const summaryParts = [];
120
+ if (finalDispositors.size > 0) {
121
+ summaryParts.push(`Final dispositors: ${[...finalDispositors].sort().join(', ')}`);
122
+ }
123
+ if (cycles.size > 0) {
124
+ const formattedCycles = [...cycles.values()]
125
+ .map((cycle) => {
126
+ const uniqueCyclePlanets = [...new Set(cycle)];
127
+ const startNode = uniqueCyclePlanets.sort()[0];
128
+ const startIndex = cycle.indexOf(startNode);
129
+ const reordered = [
130
+ ...cycle.slice(startIndex),
131
+ ...cycle.slice(0, startIndex),
132
+ ];
133
+ return [...reordered, startNode].join(' → ');
134
+ })
135
+ .sort();
136
+ summaryParts.push(`Cycles: ${formattedCycles.join(', ')}`);
137
+ }
138
+ if (summaryParts.length === 0) {
139
+ return { summary: 'No final dispositors or cycles found' };
140
+ }
141
+ return { summary: summaryParts.join('; ') };
121
142
  }
122
- // Continue the chain
123
- const newPath = [...pathSoFar, planet];
124
- const nextChain = buildDispositorChain(ruler, chains, newPath);
125
- // If the next chain ends in (final), this whole chain ends in (final)
126
- // If the next chain ends in (cycle), this chain also ends in (cycle)
127
- return [ruler, ...nextChain];
128
- }
129
- /**
130
- * Formats the dispositor analysis for display
131
- * @param analysis The dispositor analysis
132
- * @returns Array of formatted strings
133
- */
134
- function formatDispositorAnalysis(analysis) {
135
- const output = [];
136
- // Build and format chains for each planet
137
- analysis.chains.forEach((chainInfo) => {
138
- const chainRest = buildDispositorChain(chainInfo.planet, analysis.chains);
139
- const chainString = chainRest.join(' → ');
140
- output.push(`${chainInfo.planet} → ${chainString}`);
143
+ // Default mode: return all chains
144
+ const chains = {};
145
+ planets.forEach((p) => {
146
+ const planetName = p.name;
147
+ const path = [planetName];
148
+ let current = planetName;
149
+ // eslint-disable-next-line no-constant-condition
150
+ while (true) {
151
+ const nextDispositor = dispositorMap[current];
152
+ if (!nextDispositor || !dispositorMap.hasOwnProperty(nextDispositor)) {
153
+ chains[planetName] = `${path.join(' ')} ${nextDispositor} (not in chart)`;
154
+ break;
155
+ }
156
+ if (nextDispositor === current) {
157
+ chains[planetName] = `${path.join(' ')} (final)`;
158
+ break;
159
+ }
160
+ if (path.includes(nextDispositor)) {
161
+ chains[planetName] = `${path.join(' → ')} → ${nextDispositor} (cycle)`;
162
+ break;
163
+ }
164
+ path.push(nextDispositor);
165
+ current = nextDispositor;
166
+ }
141
167
  });
142
- return output;
168
+ return chains;
143
169
  }
@@ -0,0 +1,9 @@
1
+ import { AspectData, Settings } from '../types';
2
+ /**
3
+ * Provides a default grouping of aspects into "Tight", "Moderate", and "Wide" categories
4
+ * based on orb thresholds. This is used by the simple chart2txt() function.
5
+ * @param aspects The raw list of aspects to group.
6
+ * @param settings The chart settings, containing aspectStrengthThresholds.
7
+ * @returns A map of category names to aspect data arrays.
8
+ */
9
+ export declare function groupAspects(aspects: AspectData[], settings: Settings): Map<string, AspectData[]>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupAspects = groupAspects;
4
+ /**
5
+ * Provides a default grouping of aspects into "Tight", "Moderate", and "Wide" categories
6
+ * based on orb thresholds. This is used by the simple chart2txt() function.
7
+ * @param aspects The raw list of aspects to group.
8
+ * @param settings The chart settings, containing aspectStrengthThresholds.
9
+ * @returns A map of category names to aspect data arrays.
10
+ */
11
+ function groupAspects(aspects, settings) {
12
+ const grouped = new Map();
13
+ const thresholds = settings.aspectStrengthThresholds;
14
+ const tight = [];
15
+ const moderate = [];
16
+ const wide = [];
17
+ aspects.forEach((aspect) => {
18
+ if (aspect.orb <= thresholds.tight) {
19
+ tight.push(aspect);
20
+ }
21
+ else if (aspect.orb <= thresholds.moderate) {
22
+ moderate.push(aspect);
23
+ }
24
+ else {
25
+ wide.push(aspect);
26
+ }
27
+ });
28
+ // Sort each category by orb tightness
29
+ tight.sort((a, b) => a.orb - b.orb);
30
+ moderate.sort((a, b) => a.orb - b.orb);
31
+ wide.sort((a, b) => a.orb - b.orb);
32
+ if (tight.length > 0) {
33
+ const title = `[TIGHT ASPECTS: orb under ${thresholds.tight.toFixed(1)}°]`;
34
+ grouped.set(title, tight);
35
+ }
36
+ if (moderate.length > 0) {
37
+ const title = `[MODERATE ASPECTS: orb ${thresholds.tight.toFixed(1)}-${thresholds.moderate.toFixed(1)}°]`;
38
+ grouped.set(title, moderate);
39
+ }
40
+ if (wide.length > 0) {
41
+ const title = `[WIDE ASPECTS: orb over ${thresholds.moderate.toFixed(1)}°]`;
42
+ grouped.set(title, wide);
43
+ }
44
+ return grouped;
45
+ }
@@ -1,31 +1,21 @@
1
1
  import { Point } from '../types';
2
- export interface SignDistributions {
3
- elements: Record<string, string[]>;
4
- modalities: Record<string, number>;
5
- polarities: Record<string, number>;
6
- }
7
- /**
8
- * Analyzes the distribution of planets across elements, modalities, and polarities
9
- * @param planets Array of planet points
10
- * @param includeAscendant Optional ascendant degree to include in analysis
11
- * @returns Sign distribution analysis
12
- */
13
- export declare function analyzeSignDistributions(planets: Point[], includeAscendant?: number): SignDistributions;
14
- /**
15
- * Formats element distribution for display
16
- * @param elements Element distribution data
17
- * @returns Array of formatted strings
18
- */
19
- export declare function formatElementDistribution(elements: Record<string, string[]>): string[];
20
- /**
21
- * Formats modality distribution for display
22
- * @param modalities Modality distribution data
23
- * @returns Array of formatted strings
24
- */
25
- export declare function formatModalityDistribution(modalities: Record<string, number>): string[];
26
- /**
27
- * Formats polarity distribution for display
28
- * @param polarities Polarity distribution data
29
- * @returns Array of formatted strings
30
- */
31
- export declare function formatPolarityDistribution(polarities: Record<string, number>): string[];
2
+ export declare function calculateSignDistributions(planets: Point[], ascendant?: number): {
3
+ elements: {
4
+ [key: string]: string[];
5
+ };
6
+ modalities: {
7
+ [key: string]: number;
8
+ };
9
+ polarities: {
10
+ [key: string]: number;
11
+ };
12
+ };
13
+ export declare function formatElementDistribution(elements: {
14
+ [key: string]: string[];
15
+ }): string[];
16
+ export declare function formatModalityDistribution(modalities: {
17
+ [key: string]: number;
18
+ }): string[];
19
+ export declare function formatPolarityDistribution(polarities: {
20
+ [key: string]: number;
21
+ }): string[];
@@ -1,60 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analyzeSignDistributions = analyzeSignDistributions;
3
+ exports.calculateSignDistributions = calculateSignDistributions;
4
4
  exports.formatElementDistribution = formatElementDistribution;
5
5
  exports.formatModalityDistribution = formatModalityDistribution;
6
6
  exports.formatPolarityDistribution = formatPolarityDistribution;
7
- const astrology_1 = require("./astrology");
8
- // Sign classifications
9
- const SIGN_ELEMENTS = {
10
- Aries: 'Fire',
11
- Leo: 'Fire',
12
- Sagittarius: 'Fire',
13
- Taurus: 'Earth',
14
- Virgo: 'Earth',
15
- Capricorn: 'Earth',
16
- Gemini: 'Air',
17
- Libra: 'Air',
18
- Aquarius: 'Air',
19
- Cancer: 'Water',
20
- Scorpio: 'Water',
21
- Pisces: 'Water',
22
- };
23
- const SIGN_MODALITIES = {
24
- Aries: 'Cardinal',
25
- Cancer: 'Cardinal',
26
- Libra: 'Cardinal',
27
- Capricorn: 'Cardinal',
28
- Taurus: 'Fixed',
29
- Leo: 'Fixed',
30
- Scorpio: 'Fixed',
31
- Aquarius: 'Fixed',
32
- Gemini: 'Mutable',
33
- Virgo: 'Mutable',
34
- Sagittarius: 'Mutable',
35
- Pisces: 'Mutable',
36
- };
37
- const SIGN_POLARITIES = {
38
- Aries: 'Masculine',
39
- Gemini: 'Masculine',
40
- Leo: 'Masculine',
41
- Libra: 'Masculine',
42
- Sagittarius: 'Masculine',
43
- Aquarius: 'Masculine',
44
- Taurus: 'Feminine',
45
- Cancer: 'Feminine',
46
- Virgo: 'Feminine',
47
- Scorpio: 'Feminine',
48
- Capricorn: 'Feminine',
49
- Pisces: 'Feminine',
50
- };
51
- /**
52
- * Analyzes the distribution of planets across elements, modalities, and polarities
53
- * @param planets Array of planet points
54
- * @param includeAscendant Optional ascendant degree to include in analysis
55
- * @returns Sign distribution analysis
56
- */
57
- function analyzeSignDistributions(planets, includeAscendant) {
7
+ const formatting_1 = require("../utils/formatting");
8
+ const constants_1 = require("../constants");
9
+ function calculateSignDistributions(planets, ascendant) {
10
+ const points = [...planets];
11
+ if (ascendant !== undefined) {
12
+ points.push({ name: 'Ascendant', degree: ascendant });
13
+ }
58
14
  const elements = {
59
15
  Fire: [],
60
16
  Earth: [],
@@ -66,85 +22,32 @@ function analyzeSignDistributions(planets, includeAscendant) {
66
22
  Fixed: 0,
67
23
  Mutable: 0,
68
24
  };
69
- const polarities = {
70
- Masculine: 0,
71
- Feminine: 0,
72
- };
73
- // Process planets
74
- planets.forEach((planet) => {
75
- const sign = (0, astrology_1.getDegreeSign)(planet.degree);
76
- const element = SIGN_ELEMENTS[sign];
77
- const modality = SIGN_MODALITIES[sign];
78
- const polarity = SIGN_POLARITIES[sign];
79
- if (element) {
80
- elements[element].push(planet.name);
81
- }
82
- if (modality) {
83
- modalities[modality]++;
84
- }
85
- if (polarity) {
86
- polarities[polarity]++;
87
- }
88
- });
89
- // Process ascendant if provided
90
- if (includeAscendant !== undefined) {
91
- const ascSign = (0, astrology_1.getDegreeSign)(includeAscendant);
92
- const ascElement = SIGN_ELEMENTS[ascSign];
93
- const ascModality = SIGN_MODALITIES[ascSign];
94
- const ascPolarity = SIGN_POLARITIES[ascSign];
95
- if (ascElement) {
96
- elements[ascElement].push('Ascendant');
97
- }
98
- if (ascModality) {
99
- modalities[ascModality]++;
100
- }
101
- if (ascPolarity) {
102
- polarities[ascPolarity]++;
25
+ const polarities = { Masculine: 0, Feminine: 0 };
26
+ for (const point of points) {
27
+ const sign = (0, formatting_1.getSign)(point.degree);
28
+ const signInfo = constants_1.ZODIAC_SIGN_DATA.find((s) => s.name === sign);
29
+ if (signInfo) {
30
+ elements[signInfo.element].push(point.name);
31
+ modalities[signInfo.modality]++;
32
+ polarities[signInfo.polarity]++;
103
33
  }
104
34
  }
105
35
  return { elements, modalities, polarities };
106
36
  }
107
- /**
108
- * Formats element distribution for display
109
- * @param elements Element distribution data
110
- * @returns Array of formatted strings
111
- */
112
37
  function formatElementDistribution(elements) {
113
- const output = [];
114
- Object.entries(elements).forEach(([element, planets]) => {
115
- if (planets.length > 0) {
116
- const planetList = planets.join(', ');
117
- output.push(`${element}: ${planets.length} (${planetList})`);
38
+ const parts = Object.entries(elements).map(([element, planets]) => {
39
+ if (planets.length === 0) {
40
+ return `${element}: 0`;
118
41
  }
42
+ return `${element}: ${planets.length} (${planets.join(', ')})`;
119
43
  });
120
- return output;
44
+ return [parts.join(' | ')];
121
45
  }
122
- /**
123
- * Formats modality distribution for display
124
- * @param modalities Modality distribution data
125
- * @returns Array of formatted strings
126
- */
127
46
  function formatModalityDistribution(modalities) {
128
- const output = [];
129
- Object.entries(modalities).forEach(([modality, count]) => {
130
- if (count > 0) {
131
- output.push(`${modality}: ${count}`);
132
- }
133
- });
134
- return output;
47
+ const parts = Object.entries(modalities).map(([modality, count]) => `${modality}: ${count}`);
48
+ return [parts.join(' | ')];
135
49
  }
136
- /**
137
- * Formats polarity distribution for display
138
- * @param polarities Polarity distribution data
139
- * @returns Array of formatted strings
140
- */
141
50
  function formatPolarityDistribution(polarities) {
142
- const output = [];
143
- if (polarities['Masculine'] > 0) {
144
- output.push(`Masculine (Active): ${polarities['Masculine']}`);
145
- }
146
- if (polarities['Feminine'] > 0) {
147
- output.push(`Feminine (Receptive): ${polarities['Feminine']}`);
148
- }
149
- return output;
51
+ const parts = Object.entries(polarities).map(([polarity, count]) => `${polarity}: ${count}`);
52
+ return [parts.join(' | ')];
150
53
  }
@@ -0,0 +1,10 @@
1
+ import { Point, Stellium } from '../types';
2
+ /**
3
+ * Detect Stellium patterns (3+ planets in same sign or adjacent houses)
4
+ * This function requires house information and is specific to single-chart analysis
5
+ */
6
+ export declare function detectStelliums(planets: Point[], houseCusps?: number[], minPlanets?: number): Stellium[];
7
+ /**
8
+ * Format a Stellium pattern for display in compact format
9
+ */
10
+ export declare function formatStellium(pattern: Stellium): string[];