klio 1.4.7 → 1.4.9
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.
- package/README.md +18 -4
- package/package.json +1 -1
- package/src/astrology/astrologyService.js +138 -3
- package/src/cli/cli.js +182 -4
- package/src/health/healthAnalysis.js +216 -1
- package/src/utils/chartGenerator.js +129 -0
- package/src/utils/downloadsFolder.js +70 -0
package/README.md
CHANGED
|
@@ -86,17 +86,21 @@ klio --gui --gui-port 8080
|
|
|
86
86
|
### Health Analysis
|
|
87
87
|
It's possible to analyze Apple Health data. This happens on the device and no server is involved like everything from this CLI. It just parses the export XML.
|
|
88
88
|
|
|
89
|
-
- **
|
|
89
|
+
- **Sleep analysis**: `klio [planet] --apple <filepath> --sleep ` - Analyzes Apple Health Export XML file and shows the most frequent aspects during sleep deprivation (Top 10):
|
|
90
90
|
- **Step analysis**: `klio [planet] --apple <filepath> --steps` - Analyzes step patterns of any planet
|
|
91
91
|
- **Stress analysis**: `klio [planet] --apple <filepath> --stress` - Analyzes stress patterns of planet sign stress based on HRV
|
|
92
|
+
- **Late night analysis**: `klio [planet] --apple <filepath> --night` - Analyzes late night sleep patterns (after 2am) and shows the most frequent aspects during these times
|
|
93
|
+
- **All-nighter analysis**: `klio [planet] --apple <filepath> --night-all` - Analyzes all-nighter sleep patterns (starting at 04:00 or 06:00) and shows the most frequent aspects during these times
|
|
92
94
|
|
|
93
95
|
### CSV Analysis
|
|
94
96
|
It's possible to analyze a csv with a column of either ISO date time or unix time stamp.
|
|
95
97
|
|
|
96
98
|
- **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
|
|
97
99
|
- **Show aspect type distribution between two planets:** `klio [planet1] [planet2] --csv <file-path> --a`
|
|
100
|
+
- **Filter CSV data by column value:** `klio [planet] --csv <file-path> --filter "column:value"` (e.g., `--filter "Item:coffee"`)
|
|
101
|
+
- **Creating a bar chart**: Create a bar chart and save the image to the downloads' folder. The image shows the aspect distribution of your csv datetime values: `klio moon sun --csv /home/user/Downloads/coffee.csv --filter "Item:cookie" --a --title "Eaten cookies during sun-moon aspects"`
|
|
98
102
|
|
|
99
|
-
The command also returns a Chi-Square.
|
|
103
|
+
- The command also returns a Chi-Square.
|
|
100
104
|
|
|
101
105
|
### Adding different charts
|
|
102
106
|
|
|
@@ -107,7 +111,7 @@ The command also returns a Chi-Square.
|
|
|
107
111
|
- `--people` - Lists all saved persons
|
|
108
112
|
- `--delete-person <id>` - Deletes a person
|
|
109
113
|
|
|
110
|
-
Then, instead using `--i` for the commands from above you can use `--wp <id>`
|
|
114
|
+
Then, instead using `--i` for the commands from above you can use `--wp <id>` or `--wp john`
|
|
111
115
|
|
|
112
116
|
### Advanced Features
|
|
113
117
|
|
|
@@ -117,11 +121,21 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
|
|
|
117
121
|
- **Aspect figures**: `klio --af` - Shows active aspect figures like T-squares, Grand Trines, etc. Combine with `--wp <id>` or `--i` or `--d <"DD.MM.YYYY HH:MM>` or `--hs <house-system>`
|
|
118
122
|
- **Transits**: `klio --tr` - Shows personal transits based on birth data. Combine with `--wp <id or --i>` and optional `--d <"DD.MM.YYYY HH:MM>`
|
|
119
123
|
- **Transit Houses**: `klio --s --tra --i`
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
120
127
|
### Past and Future Aspects
|
|
121
128
|
|
|
122
129
|
- **Past aspects**: `klio --v <count> [planet1] [aspect-type] [planet2]` - Shows past aspects (Format: --v <count> planet1 aspectType planet2) Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)
|
|
123
130
|
- **Future aspects**: `klio --z <count> [planet1] [aspect-type] [planet2]` - Shows future aspects (Format: --z <count> planet1 aspectType planet2)
|
|
124
131
|
|
|
132
|
+
### Reference
|
|
133
|
+
- **Transit frequency**: `[planet1] [aspect-type] [planet2] --o` - Calculates how often a transit occurs in the sky using astronomical synodic periods
|
|
134
|
+
- **Example**: `klio saturn c neptune --o` - Shows Saturn-Neptune conjunction frequency
|
|
135
|
+
- **Example**: `klio jupiter s pluto --o` - Shows Jupiter-Pluto square frequency
|
|
136
|
+
- **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
|
|
137
|
+
- **Output**: Shows frequency in readable format (e.g., "every 36 years") with approximate
|
|
138
|
+
|
|
125
139
|
### Planet Ingresses
|
|
126
140
|
|
|
127
141
|
- **Planet ingress**: `klio [planet] --in [count]` - Shows when a planet enters a new zodiac sign. Optional count parameter for multiple ingresses (default: 1). Works with any planet (moon, mercury, venus, etc.)
|
|
@@ -132,7 +146,7 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
|
|
|
132
146
|
### Wikidata Integration
|
|
133
147
|
|
|
134
148
|
- **Search for people with specific aspects**: `[planet1] [aspect-type] [planet2] --wiki <occupation> [limit]` - Searches Wikidata for people with specific astrological aspects
|
|
135
|
-
- **Available occupations**: authors, scientists, artists, musicians, politicians, actors, philosophers
|
|
149
|
+
- **Available occupations for now**: authors, scientists, artists, musicians, politicians, actors, philosophers
|
|
136
150
|
- **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
|
|
137
151
|
- **Example**: `klio moon c neptune --wiki authors 50` - Tries to find authors with Moon conjunct Neptune aspect with a limit of 50. Is faster but less common aspects are maybe not found with 50
|
|
138
152
|
- **Example**: `klio saturn s pluto --wiki scientists 100` - Finds scientists with Saturn square Pluto aspect
|
package/package.json
CHANGED
|
@@ -1900,7 +1900,39 @@ function analyzeSignDistributionSignificance(signDistribution) {
|
|
|
1900
1900
|
};
|
|
1901
1901
|
}
|
|
1902
1902
|
|
|
1903
|
-
|
|
1903
|
+
// Helper function to parse filter criteria
|
|
1904
|
+
function parseFilterCriteria(filterString) {
|
|
1905
|
+
if (!filterString) return null;
|
|
1906
|
+
|
|
1907
|
+
const parts = filterString.split(':');
|
|
1908
|
+
if (parts.length !== 2) {
|
|
1909
|
+
console.warn('Invalid filter format. Expected "column:value"');
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
return {
|
|
1914
|
+
column: parts[0].trim(),
|
|
1915
|
+
value: parts[1].trim()
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// Helper function to apply filter to records
|
|
1920
|
+
function applyFilter(records, filterCriteria) {
|
|
1921
|
+
if (!filterCriteria || !records || records.length === 0) return records;
|
|
1922
|
+
|
|
1923
|
+
const parsedCriteria = typeof filterCriteria === 'string'
|
|
1924
|
+
? parseFilterCriteria(filterCriteria)
|
|
1925
|
+
: filterCriteria;
|
|
1926
|
+
|
|
1927
|
+
if (!parsedCriteria) return records;
|
|
1928
|
+
|
|
1929
|
+
return records.filter(record => {
|
|
1930
|
+
const recordValue = record[parsedCriteria.column];
|
|
1931
|
+
return recordValue && recordValue.toString() === parsedCriteria.value;
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
|
|
1904
1936
|
return new Promise(async (resolve, reject) => {
|
|
1905
1937
|
const results = [];
|
|
1906
1938
|
let pendingOperations = 0;
|
|
@@ -1967,8 +1999,11 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
|
|
|
1967
1999
|
skip_empty_lines: true
|
|
1968
2000
|
});
|
|
1969
2001
|
|
|
2002
|
+
// Apply filter if specified
|
|
2003
|
+
const filteredRecords = filterCriteria ? applyFilter(records, filterCriteria) : records;
|
|
2004
|
+
|
|
1970
2005
|
// Process each row
|
|
1971
|
-
for (const data of
|
|
2006
|
+
for (const data of filteredRecords) {
|
|
1972
2007
|
// Look for a column with ISO-Datetime values or Unix-Timestamps
|
|
1973
2008
|
const datetimeColumns = Object.keys(data).filter(key => {
|
|
1974
2009
|
const value = data[key];
|
|
@@ -2107,6 +2142,20 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
|
|
|
2107
2142
|
fs.createReadStream(filePath)
|
|
2108
2143
|
.pipe(csvParser())
|
|
2109
2144
|
.on('data', (data) => {
|
|
2145
|
+
// Apply filter if specified
|
|
2146
|
+
if (filterCriteria) {
|
|
2147
|
+
const parsedCriteria = typeof filterCriteria === 'string'
|
|
2148
|
+
? parseFilterCriteria(filterCriteria)
|
|
2149
|
+
: filterCriteria;
|
|
2150
|
+
|
|
2151
|
+
if (parsedCriteria) {
|
|
2152
|
+
const recordValue = data[parsedCriteria.column];
|
|
2153
|
+
if (!recordValue || recordValue.toString() !== parsedCriteria.value) {
|
|
2154
|
+
return; // Skip this record
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2110
2159
|
// Look for a column with ISO-Datetime values or Unix-Timestamps
|
|
2111
2160
|
const datetimeColumns = Object.keys(data).filter(key => {
|
|
2112
2161
|
const value = data[key];
|
|
@@ -2780,5 +2829,91 @@ module.exports = {
|
|
|
2780
2829
|
showPlanetComboAspects,
|
|
2781
2830
|
calculateNextPlanetIngress,
|
|
2782
2831
|
calculateAstrologicalAngles,
|
|
2783
|
-
longitudeToSignDegree
|
|
2832
|
+
longitudeToSignDegree,
|
|
2833
|
+
calculateTransitFrequency
|
|
2784
2834
|
};
|
|
2835
|
+
|
|
2836
|
+
// Function to calculate how often a transit occurs between two planets
|
|
2837
|
+
function calculateTransitFrequency(planet1, planet2, aspectType) {
|
|
2838
|
+
// Use astronomical synodic period calculation for more accurate results
|
|
2839
|
+
const orbitalPeriods = {
|
|
2840
|
+
'sun': 1, // Not applicable for conjunctions, but included for completeness
|
|
2841
|
+
'moon': 0.0748, // 27.3 days
|
|
2842
|
+
'mercury': 0.2408, // 87.97 days
|
|
2843
|
+
'venus': 0.6152, // 224.7 days
|
|
2844
|
+
'mars': 1.8808, // 686.98 days
|
|
2845
|
+
'jupiter': 11.862, // 11.862 years
|
|
2846
|
+
'saturn': 29.457, // 29.457 years
|
|
2847
|
+
'uranus': 84.016, // 84.016 years
|
|
2848
|
+
'neptune': 164.79, // 164.79 years
|
|
2849
|
+
'pluto': 248.09 // 248.09 years
|
|
2850
|
+
};
|
|
2851
|
+
|
|
2852
|
+
// Get orbital periods for the two planets
|
|
2853
|
+
const period1 = orbitalPeriods[planet1];
|
|
2854
|
+
const period2 = orbitalPeriods[planet2];
|
|
2855
|
+
|
|
2856
|
+
if (!period1 || !period2) {
|
|
2857
|
+
return {
|
|
2858
|
+
frequency: 'Cannot calculate frequency: unknown orbital periods',
|
|
2859
|
+
averageInterval: null,
|
|
2860
|
+
aspects: []
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// Calculate synodic period using the formula: 1/S = |1/P1 - 1/P2|
|
|
2865
|
+
// For conjunctions, we use the absolute difference
|
|
2866
|
+
const synodicPeriod = 1 / Math.abs(1/period1 - 1/period2);
|
|
2867
|
+
|
|
2868
|
+
// Convert to days
|
|
2869
|
+
const averageIntervalDays = synodicPeriod * 365.25;
|
|
2870
|
+
|
|
2871
|
+
// Format the frequency in a more readable way
|
|
2872
|
+
const formattedFrequency = formatFrequency(averageIntervalDays);
|
|
2873
|
+
|
|
2874
|
+
// Also calculate some actual aspects for reference
|
|
2875
|
+
const futureAspects = getFutureAspects(planet1, planet2, aspectType, 5);
|
|
2876
|
+
|
|
2877
|
+
return {
|
|
2878
|
+
frequency: formattedFrequency,
|
|
2879
|
+
frequencyShort: `Approximately every ${synodicPeriod.toFixed(1)} years`,
|
|
2880
|
+
averageInterval: averageIntervalDays,
|
|
2881
|
+
averageIntervalYears: synodicPeriod,
|
|
2882
|
+
intervals: [], // Not applicable for this calculation method
|
|
2883
|
+
aspects: futureAspects,
|
|
2884
|
+
note: `Note: Frequency calculated using astronomical synodic period (1/S = |1/${period1} - 1/${period2}| years)`
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// Helper function to format frequency in a readable way
|
|
2889
|
+
function formatFrequency(days) {
|
|
2890
|
+
if (days < 30) {
|
|
2891
|
+
return `Approximately every ${Math.round(days)} days`;
|
|
2892
|
+
} else if (days < 90) {
|
|
2893
|
+
const months = days / 30;
|
|
2894
|
+
return `Approximately every ${Math.round(months)} months`;
|
|
2895
|
+
} else if (days < 365) {
|
|
2896
|
+
const months = days / 30;
|
|
2897
|
+
return `Approximately every ${Math.round(months)} months`;
|
|
2898
|
+
} else if (days < 730) {
|
|
2899
|
+
const years = days / 365.25;
|
|
2900
|
+
const months = (years - Math.floor(years)) * 12;
|
|
2901
|
+
if (months >= 9) {
|
|
2902
|
+
return `Approximately every ${Math.floor(years) + 1} years`;
|
|
2903
|
+
} else if (months >= 1) {
|
|
2904
|
+
return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
|
|
2905
|
+
} else {
|
|
2906
|
+
return `Approximately every ${Math.floor(years)} years`;
|
|
2907
|
+
}
|
|
2908
|
+
} else {
|
|
2909
|
+
const years = days / 365.25;
|
|
2910
|
+
const months = (years - Math.floor(years)) * 12;
|
|
2911
|
+
if (months >= 9) {
|
|
2912
|
+
return `Approximately every ${Math.floor(years) + 1} years`;
|
|
2913
|
+
} else if (months >= 1) {
|
|
2914
|
+
return `Approximately every ${Math.floor(years)} years, ${Math.round(months)} months`;
|
|
2915
|
+
} else {
|
|
2916
|
+
return `Approximately every ${Math.round(years)} years`;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
package/src/cli/cli.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
2
|
const { planets, signs } = require('../astrology/astrologyConstants');
|
|
3
3
|
const { showRetrogradePlanets } = require('../astrology/retrogradeService');
|
|
4
|
-
const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree } = require('../astrology/astrologyService');
|
|
4
|
+
const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree, calculateTransitFrequency } = require('../astrology/astrologyService');
|
|
5
5
|
const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
|
|
6
6
|
const { parseAppleHealthXML } = require('../health/healthService');
|
|
7
|
-
const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep } = require('../health/healthAnalysis');
|
|
7
|
+
const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep, analyzeLateNightAspects, analyzeAllNighterAspects } = require('../health/healthAnalysis');
|
|
8
8
|
const { analyzeHouseDistribution, filterFilesByHouse } = require('../health/fileAnalysis');
|
|
9
9
|
const { getFileCreationDate, parseDateToComponents } = require('../utils/fileUtils');
|
|
10
10
|
const swisseph = require('swisseph');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
|
|
14
|
+
// Import chart generation and downloads folder utilities
|
|
15
|
+
let chartGenerator = null;
|
|
16
|
+
let downloadsFolder = null;
|
|
17
|
+
try {
|
|
18
|
+
chartGenerator = require('../utils/chartGenerator');
|
|
19
|
+
downloadsFolder = require('../utils/downloadsFolder');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.debug('Chart generation utilities not available');
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
// Wikidata service import
|
|
15
25
|
let wikidataService = null;
|
|
16
26
|
try {
|
|
@@ -232,6 +242,8 @@ program
|
|
|
232
242
|
.option('--sleep', 'Analyzes sleep patterns with moon aspects')
|
|
233
243
|
.option('--steps', 'Analyzes step patterns with moon signs')
|
|
234
244
|
.option('--stress', 'Analyzes stress patterns with moon aspects based on HRV')
|
|
245
|
+
.option('--night', 'Analyzes late night sleep patterns (after 2am) and common aspects')
|
|
246
|
+
.option('--night-all', 'Analyzes all-nighter sleep patterns (04:00 and 06:00 starts)')
|
|
235
247
|
.option('--setup', 'Configures location and birth data for personalized calculations')
|
|
236
248
|
.option('--ai <model>', 'Sets a specific AI model (e.g. "google/gemma-3n-e4b")')
|
|
237
249
|
.option('--system <prompt>', 'Sets a custom system prompt for all AI requests')
|
|
@@ -263,10 +275,13 @@ program
|
|
|
263
275
|
.option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
|
|
264
276
|
.option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
|
|
265
277
|
.option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
|
|
278
|
+
.option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
|
|
279
|
+
.option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
|
|
266
280
|
.option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
|
|
267
281
|
.option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
|
|
268
282
|
.option('--gui', 'Launches the web interface for command history (port 37421)')
|
|
269
283
|
.option('--gui-port <port>', 'Specify custom port for GUI server')
|
|
284
|
+
.option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
|
|
270
285
|
.description('Shows astrological data for a planet')
|
|
271
286
|
.action(async (planetArg, planet2Arg, options) => {
|
|
272
287
|
// If planet2Arg is an object, it contains the options (commander behavior)
|
|
@@ -1492,8 +1507,9 @@ program
|
|
|
1492
1507
|
const planet = planetArg ? planetArg.toLowerCase() : 'moon';
|
|
1493
1508
|
const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
|
|
1494
1509
|
const analyzeAspects = actualOptions.a || false;
|
|
1510
|
+
const filterCriteria = actualOptions.filter;
|
|
1495
1511
|
|
|
1496
|
-
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
|
|
1512
|
+
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
|
|
1497
1513
|
.then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
|
|
1498
1514
|
if (results.length === 0) {
|
|
1499
1515
|
console.log('No valid ISO-Datetime values found in the CSV file.');
|
|
@@ -1611,6 +1627,60 @@ program
|
|
|
1611
1627
|
// Also show total number of records and house system
|
|
1612
1628
|
console.log(`\nIn Total: ${results.length}`);
|
|
1613
1629
|
console.log(`House system: ${houseSystem.charAt(0).toUpperCase() + houseSystem.slice(1)}`);
|
|
1630
|
+
|
|
1631
|
+
// Generate chart image if --title option is provided
|
|
1632
|
+
if (actualOptions.title && chartGenerator && downloadsFolder) {
|
|
1633
|
+
try {
|
|
1634
|
+
// Get the downloads folder path
|
|
1635
|
+
const downloadsPath = downloadsFolder.getValidDownloadsFolder();
|
|
1636
|
+
|
|
1637
|
+
// Create a safe filename from the title
|
|
1638
|
+
const safeTitle = actualOptions.title
|
|
1639
|
+
.replace(/[^a-zA-Z0-9\s\-_.]/g, '')
|
|
1640
|
+
.substring(0, 50)
|
|
1641
|
+
.trim();
|
|
1642
|
+
|
|
1643
|
+
const timestamp = new Date().toISOString()
|
|
1644
|
+
.replace(/[:.]/g, '-')
|
|
1645
|
+
.substring(0, 19);
|
|
1646
|
+
|
|
1647
|
+
const filename = `${timestamp}_${safeTitle}.png`;
|
|
1648
|
+
const outputPath = path.join(downloadsPath, filename);
|
|
1649
|
+
|
|
1650
|
+
console.log(`\n📊 Generating chart image: ${filename}`);
|
|
1651
|
+
console.log(`💾 Saving to: ${outputPath}`);
|
|
1652
|
+
|
|
1653
|
+
// Calculate statistics for the chart
|
|
1654
|
+
let chartStats = null;
|
|
1655
|
+
if (analyzeAspects && aspectStatistics) {
|
|
1656
|
+
const { analyzeAspectDistributionSignificance } = require('../astrology/astrologyService');
|
|
1657
|
+
chartStats = analyzeAspectDistributionSignificance(aspectStatistics);
|
|
1658
|
+
} else if (houseDistribution) {
|
|
1659
|
+
const { analyzeHouseDistributionSignificance } = require('../astrology/astrologyService');
|
|
1660
|
+
chartStats = analyzeHouseDistributionSignificance(houseDistribution);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Generate the chart
|
|
1664
|
+
const chartData = {
|
|
1665
|
+
houseDistribution: houseDistribution || { counts: {}, total: results.length },
|
|
1666
|
+
aspectStatistics: aspectStatistics || { types: [], counts: {}, total: 0 },
|
|
1667
|
+
results: results,
|
|
1668
|
+
statistics: chartStats
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
return chartGenerator.generateBarChart(
|
|
1672
|
+
chartData,
|
|
1673
|
+
actualOptions.title,
|
|
1674
|
+
outputPath,
|
|
1675
|
+
analyzeAspects // Show aspects if --a option was used
|
|
1676
|
+
).then(imagePath => {
|
|
1677
|
+
console.log(`✅ Chart image successfully saved to: ${imagePath}`);
|
|
1678
|
+
console.log(`📁 You can find it in your Downloads folder`);
|
|
1679
|
+
});
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
console.error('❌ Error generating chart image:', error.message);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1614
1684
|
}
|
|
1615
1685
|
})
|
|
1616
1686
|
.catch(error => {
|
|
@@ -1946,6 +2016,44 @@ program
|
|
|
1946
2016
|
};
|
|
1947
2017
|
await handleAIRequest(options, healthDataSummary);
|
|
1948
2018
|
}
|
|
2019
|
+
} else if (options.night) {
|
|
2020
|
+
console.log(`Apple Health late night sleep analysis with ${pLabel} aspects`);
|
|
2021
|
+
console.log('========================================================\n');
|
|
2022
|
+
await analyzeLateNightAspects(planet, healthData);
|
|
2023
|
+
|
|
2024
|
+
// Handle AI request for night analysis
|
|
2025
|
+
if (options.p) {
|
|
2026
|
+
const healthDataSummary = {
|
|
2027
|
+
planet: planet,
|
|
2028
|
+
sign: 'Multiple',
|
|
2029
|
+
degreeInSign: 'Multiple',
|
|
2030
|
+
dignity: `Apple Health late night sleep analysis with ${planet} aspects`,
|
|
2031
|
+
element: 'Multiple',
|
|
2032
|
+
decan: 'Multiple',
|
|
2033
|
+
healthAnalysisType: 'night',
|
|
2034
|
+
planetName: planet
|
|
2035
|
+
};
|
|
2036
|
+
await handleAIRequest(options, healthDataSummary);
|
|
2037
|
+
}
|
|
2038
|
+
} else if (options.nightAll) {
|
|
2039
|
+
console.log(`Apple Health all-nighter sleep analysis with ${pLabel} aspects`);
|
|
2040
|
+
console.log('============================================================\n');
|
|
2041
|
+
await analyzeAllNighterAspects(planet, healthData);
|
|
2042
|
+
|
|
2043
|
+
// Handle AI request for all-nighter analysis
|
|
2044
|
+
if (options.p) {
|
|
2045
|
+
const healthDataSummary = {
|
|
2046
|
+
planet: planet,
|
|
2047
|
+
sign: 'Multiple',
|
|
2048
|
+
degreeInSign: 'Multiple',
|
|
2049
|
+
dignity: `Apple Health all-nighter sleep analysis with ${planet} aspects`,
|
|
2050
|
+
element: 'Multiple',
|
|
2051
|
+
decan: 'Multiple',
|
|
2052
|
+
healthAnalysisType: 'night-all',
|
|
2053
|
+
planetName: planet
|
|
2054
|
+
};
|
|
2055
|
+
await handleAIRequest(options, healthDataSummary);
|
|
2056
|
+
}
|
|
1949
2057
|
} else if (hypothesis === 'stress-moon') {
|
|
1950
2058
|
console.log(`Apple Health stress analysis with ${pLabel} aspects`);
|
|
1951
2059
|
console.log('==========================================\n');
|
|
@@ -2394,13 +2502,83 @@ program
|
|
|
2394
2502
|
return;
|
|
2395
2503
|
}
|
|
2396
2504
|
|
|
2505
|
+
// Show transit frequency if --o option is specified
|
|
2506
|
+
if (options.o) {
|
|
2507
|
+
// We expect a specific format: planet1 aspectType planet2 --o
|
|
2508
|
+
// Example: "saturn k neptun --o" for Saturn conjunction Neptune
|
|
2509
|
+
|
|
2510
|
+
// Parse planet and aspect information from positional arguments
|
|
2511
|
+
// For the format: planet1 aspectType planet2 --o
|
|
2512
|
+
// planetArg = planet1, planet2Arg = aspectType, and we need to get planet2 from excess arguments
|
|
2513
|
+
const planet1 = planetArg ? planetArg.toLowerCase() : null;
|
|
2514
|
+
const aspectType = planet2Arg ? planet2Arg.toLowerCase() : null;
|
|
2515
|
+
|
|
2516
|
+
// Get the third argument (planet2) from excess arguments
|
|
2517
|
+
const excessArgs = program.args;
|
|
2518
|
+
const planet2 = excessArgs.length >= 3 ? excessArgs[2].toLowerCase() : null;
|
|
2519
|
+
|
|
2520
|
+
// Check if we have all required arguments
|
|
2521
|
+
if (!planet1 || !aspectType || !planet2) {
|
|
2522
|
+
console.error('Error: Transit frequency requires two planets and an aspect type.');
|
|
2523
|
+
console.error('Format: planet1 aspectType planet2 --o');
|
|
2524
|
+
console.error('Example: saturn c neptune --o');
|
|
2525
|
+
console.error('Aspect types: c=conjunction, o=opposition, s=square, t=trine, se=sextile, q=quincunx');
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// Check if planets are valid
|
|
2530
|
+
if (!planets[planet1] || !planets[planet2]) {
|
|
2531
|
+
console.error('Error: Invalid planets.');
|
|
2532
|
+
console.error('Available planets:', Object.keys(planets).join(', '));
|
|
2533
|
+
process.exit(1);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// Check if aspect type is valid
|
|
2537
|
+
const targetAngle = getAspectAngle(aspectType);
|
|
2538
|
+
if (targetAngle === null) {
|
|
2539
|
+
console.error('Error: Invalid aspect type.');
|
|
2540
|
+
console.error('Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)');
|
|
2541
|
+
process.exit(1);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// Calculate transit frequency
|
|
2545
|
+
const frequencyData = calculateTransitFrequency(planet1, planet2, aspectType);
|
|
2546
|
+
|
|
2547
|
+
// Show results
|
|
2548
|
+
const aspectTypeFull = getAspectTypeFullName(aspectType);
|
|
2549
|
+
console.log(`Frequency: ${frequencyData.frequency}`);
|
|
2550
|
+
console.log(`(Average interval: ${frequencyData.averageInterval.toFixed(1)} days / ${frequencyData.averageIntervalYears.toFixed(1)} years)`);
|
|
2551
|
+
|
|
2552
|
+
// Show note if available
|
|
2553
|
+
if (frequencyData.note) {
|
|
2554
|
+
console.log(`${frequencyData.note}`);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// Handle AI request for transit frequency
|
|
2558
|
+
if (options.p) {
|
|
2559
|
+
const frequencyDataForAI = {
|
|
2560
|
+
planet: `${planet1}-${planet2}`,
|
|
2561
|
+
sign: 'Multiple',
|
|
2562
|
+
degreeInSign: 'Multiple',
|
|
2563
|
+
dignity: `Transit frequency: ${frequencyData.frequency} for ${aspectTypeFull} between ${planet1} and ${planet2}`,
|
|
2564
|
+
element: 'Multiple',
|
|
2565
|
+
decan: 'Multiple',
|
|
2566
|
+
frequencyData: frequencyData
|
|
2567
|
+
};
|
|
2568
|
+
await handleAIRequest(options, frequencyDataForAI);
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2397
2574
|
// Analyze CSV file if --csv option is provided
|
|
2398
2575
|
if (actualOptions.csv) {
|
|
2399
2576
|
const planet = planetArg ? planetArg.toLowerCase() : 'moon';
|
|
2400
2577
|
const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
|
|
2401
2578
|
const analyzeAspects = actualOptions.a || false;
|
|
2579
|
+
const filterCriteria = actualOptions.filter;
|
|
2402
2580
|
|
|
2403
|
-
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
|
|
2581
|
+
analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
|
|
2404
2582
|
.then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
|
|
2405
2583
|
if (results.length === 0) {
|
|
2406
2584
|
console.log('No valid ISO-Datetime values found in the CSV file.');
|
|
@@ -1,6 +1,219 @@
|
|
|
1
1
|
const { getAstrologicalData, calculatePlanetAspects } = require('../astrology/astrologyService');
|
|
2
2
|
const { extractStepData, extractHRVData, extractSleepData, extractSleepGoal } = require('./healthService');
|
|
3
3
|
|
|
4
|
+
// Function to analyze all-nighter sleep patterns (04:00 and 06:00 starts)
|
|
5
|
+
async function analyzeAllNighterAspects(planetName, healthData) {
|
|
6
|
+
const sleepRecords = extractSleepData(healthData);
|
|
7
|
+
|
|
8
|
+
if (sleepRecords.length === 0) {
|
|
9
|
+
console.log('No sleep data found to analyze.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Filter sleep periods that start at typical all-nighter hours (04:00 and 06:00)
|
|
14
|
+
const allNighterHours = [4, 6];
|
|
15
|
+
const allNighterSleepRecords = sleepRecords.filter(record => {
|
|
16
|
+
const startHour = record.startDate.getHours();
|
|
17
|
+
return allNighterHours.includes(startHour);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(`All-nighter sleep periods (starting at 04:00 or 06:00): ${allNighterSleepRecords.length} of ${sleepRecords.length} total sleep periods`);
|
|
21
|
+
|
|
22
|
+
if (allNighterSleepRecords.length === 0) {
|
|
23
|
+
console.log('No all-nighter sleep periods found (starting at 04:00 or 06:00).');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Analyze planet aspects for each all-nighter sleep period
|
|
28
|
+
const aspectStats = {};
|
|
29
|
+
|
|
30
|
+
for (const night of allNighterSleepRecords) {
|
|
31
|
+
const midSleepTime = new Date(night.startDate.getTime() +
|
|
32
|
+
(night.endDate.getTime() - night.startDate.getTime()) / 2);
|
|
33
|
+
const dateComponents = {
|
|
34
|
+
year: midSleepTime.getFullYear(),
|
|
35
|
+
month: midSleepTime.getMonth() + 1,
|
|
36
|
+
day: midSleepTime.getDate(),
|
|
37
|
+
hour: midSleepTime.getHours(),
|
|
38
|
+
minute: midSleepTime.getMinutes()
|
|
39
|
+
};
|
|
40
|
+
const planetAspects = calculatePlanetAspects(planetName, dateComponents);
|
|
41
|
+
|
|
42
|
+
// Count aspects
|
|
43
|
+
for (const aspect of planetAspects) {
|
|
44
|
+
const aspectKey = `${aspect.type} with ${aspect.planet}`;
|
|
45
|
+
aspectStats[aspectKey] = (aspectStats[aspectKey] || 0) + 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Show statistics (Top 10 planet aspects)
|
|
50
|
+
console.log(`\nMost frequent ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects during all-nighters (Top 10):`);
|
|
51
|
+
console.log('Aspect | Frequency | Percent | Active today');
|
|
52
|
+
console.log('-------|-----------|--------|------------');
|
|
53
|
+
|
|
54
|
+
const totalAllNighters = allNighterSleepRecords.length;
|
|
55
|
+
const sortedAspects = Object.entries(aspectStats)
|
|
56
|
+
.sort((a, b) => b[1] - a[1])
|
|
57
|
+
.slice(0, 10); // Limit to Top 10
|
|
58
|
+
|
|
59
|
+
// Calculate current planet aspects for comparison
|
|
60
|
+
const currentDate = new Date();
|
|
61
|
+
const currentDateComponents = {
|
|
62
|
+
year: currentDate.getFullYear(),
|
|
63
|
+
month: currentDate.getMonth() + 1,
|
|
64
|
+
day: currentDate.getDate(),
|
|
65
|
+
hour: currentDate.getHours(),
|
|
66
|
+
minute: currentDate.getMinutes()
|
|
67
|
+
};
|
|
68
|
+
const currentPlanetAspects = calculatePlanetAspects(planetName, currentDateComponents);
|
|
69
|
+
const currentAspectSet = new Set(currentPlanetAspects.map(a => `${a.type} with ${a.planet}`));
|
|
70
|
+
|
|
71
|
+
for (const [aspect, count] of sortedAspects) {
|
|
72
|
+
const percentage = totalAllNighters > 0 ? ((count / totalAllNighters) * 100).toFixed(1) : 0;
|
|
73
|
+
const isActiveToday = currentAspectSet.has(aspect) ? '✓' : '✗';
|
|
74
|
+
console.log(`${aspect.padEnd(25)} | ${count.toString().padStart(2)} | ${percentage}% | ${isActiveToday}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sortedAspects.length === 0) {
|
|
78
|
+
console.log(`No ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects found in all-nighter sleep periods.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Show all-nighter statistics
|
|
82
|
+
console.log(`\nAll-nighter sleep statistics:`);
|
|
83
|
+
const totalAllNighterDuration = allNighterSleepRecords.reduce((sum, record) => sum + record.duration, 0);
|
|
84
|
+
const avgAllNighterDuration = totalAllNighterDuration / allNighterSleepRecords.length;
|
|
85
|
+
|
|
86
|
+
console.log(`Average all-nighter sleep duration: ${avgAllNighterDuration.toFixed(2)} hours`);
|
|
87
|
+
console.log(`Total all-nighter sleep periods: ${allNighterSleepRecords.length}`);
|
|
88
|
+
|
|
89
|
+
// Show breakdown by all-nighter start hours
|
|
90
|
+
const hourCounts = {};
|
|
91
|
+
allNighterSleepRecords.forEach(record => {
|
|
92
|
+
const hour = record.startDate.getHours();
|
|
93
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`All-nighter start hour breakdown:`);
|
|
97
|
+
allNighterHours.forEach(hour => {
|
|
98
|
+
if (hourCounts[hour]) {
|
|
99
|
+
console.log(` ${hour}:00 - ${hourCounts[hour]} times`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Function to analyze late night sleep patterns (after 2am) and common aspects
|
|
105
|
+
async function analyzeLateNightAspects(planetName, healthData) {
|
|
106
|
+
const sleepRecords = extractSleepData(healthData);
|
|
107
|
+
|
|
108
|
+
if (sleepRecords.length === 0) {
|
|
109
|
+
console.log('No sleep data found to analyze.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Filter sleep periods that start after 2am (went to bed later than 2am)
|
|
114
|
+
const lateNightSleepRecords = sleepRecords.filter(record => {
|
|
115
|
+
const startHour = record.startDate.getHours();
|
|
116
|
+
return startHour >= 2; // Started after 2am
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(`Late night sleep periods (starting after 2am): ${lateNightSleepRecords.length} of ${sleepRecords.length} total sleep periods`);
|
|
120
|
+
|
|
121
|
+
if (lateNightSleepRecords.length === 0) {
|
|
122
|
+
console.log('No late night sleep periods found (starting after 2am).');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Analyze planet aspects for each late night sleep period
|
|
127
|
+
const aspectStats = {};
|
|
128
|
+
|
|
129
|
+
for (const night of lateNightSleepRecords) {
|
|
130
|
+
const midSleepTime = new Date(night.startDate.getTime() +
|
|
131
|
+
(night.endDate.getTime() - night.startDate.getTime()) / 2);
|
|
132
|
+
const dateComponents = {
|
|
133
|
+
year: midSleepTime.getFullYear(),
|
|
134
|
+
month: midSleepTime.getMonth() + 1,
|
|
135
|
+
day: midSleepTime.getDate(),
|
|
136
|
+
hour: midSleepTime.getHours(),
|
|
137
|
+
minute: midSleepTime.getMinutes()
|
|
138
|
+
};
|
|
139
|
+
const planetAspects = calculatePlanetAspects(planetName, dateComponents);
|
|
140
|
+
|
|
141
|
+
// Count aspects
|
|
142
|
+
for (const aspect of planetAspects) {
|
|
143
|
+
const aspectKey = `${aspect.type} with ${aspect.planet}`;
|
|
144
|
+
aspectStats[aspectKey] = (aspectStats[aspectKey] || 0) + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Show statistics (Top 10 planet aspects)
|
|
149
|
+
console.log(`\nMost frequent ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects during late night sleep (Top 10):`);
|
|
150
|
+
console.log('Aspect | Frequency | Percent | Active today');
|
|
151
|
+
console.log('-------|-----------|--------|------------');
|
|
152
|
+
|
|
153
|
+
const totalLateNights = lateNightSleepRecords.length;
|
|
154
|
+
const sortedAspects = Object.entries(aspectStats)
|
|
155
|
+
.sort((a, b) => b[1] - a[1])
|
|
156
|
+
.slice(0, 10); // Limit to Top 10
|
|
157
|
+
|
|
158
|
+
// Calculate current planet aspects for comparison
|
|
159
|
+
const currentDate = new Date();
|
|
160
|
+
const currentDateComponents = {
|
|
161
|
+
year: currentDate.getFullYear(),
|
|
162
|
+
month: currentDate.getMonth() + 1,
|
|
163
|
+
day: currentDate.getDate(),
|
|
164
|
+
hour: currentDate.getHours(),
|
|
165
|
+
minute: currentDate.getMinutes()
|
|
166
|
+
};
|
|
167
|
+
const currentPlanetAspects = calculatePlanetAspects(planetName, currentDateComponents);
|
|
168
|
+
const currentAspectSet = new Set(currentPlanetAspects.map(a => `${a.type} with ${a.planet}`));
|
|
169
|
+
|
|
170
|
+
for (const [aspect, count] of sortedAspects) {
|
|
171
|
+
const percentage = totalLateNights > 0 ? ((count / totalLateNights) * 100).toFixed(1) : 0;
|
|
172
|
+
const isActiveToday = currentAspectSet.has(aspect) ? '✓' : '✗';
|
|
173
|
+
console.log(`${aspect.padEnd(25)} | ${count.toString().padStart(2)} | ${percentage}% | ${isActiveToday}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (sortedAspects.length === 0) {
|
|
177
|
+
console.log(`No ${planetName.charAt(0).toUpperCase() + planetName.slice(1)} aspects found in late night sleep periods.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Show some statistics about the late night sleep patterns
|
|
181
|
+
console.log(`\nLate night sleep statistics:`);
|
|
182
|
+
const totalLateNightDuration = lateNightSleepRecords.reduce((sum, record) => sum + record.duration, 0);
|
|
183
|
+
const avgLateNightDuration = totalLateNightDuration / lateNightSleepRecords.length;
|
|
184
|
+
|
|
185
|
+
console.log(`Average late night sleep duration: ${avgLateNightDuration.toFixed(2)} hours`);
|
|
186
|
+
console.log(`Total late night sleep periods: ${lateNightSleepRecords.length}`);
|
|
187
|
+
|
|
188
|
+
// Show the most common late night sleep start times
|
|
189
|
+
const startHours = lateNightSleepRecords.map(record => record.startDate.getHours());
|
|
190
|
+
const hourCounts = {};
|
|
191
|
+
startHours.forEach(hour => {
|
|
192
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const sortedHours = Object.entries(hourCounts).sort((a, b) => b[1] - a[1]);
|
|
196
|
+
console.log(`Most common late night sleep start hours:`);
|
|
197
|
+
|
|
198
|
+
// Show top 3 most common hours
|
|
199
|
+
sortedHours.slice(0, 3).forEach(([hour, count]) => {
|
|
200
|
+
console.log(` ${hour}:00 - ${count} times`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Check for all-nighter patterns (04:00 and 06:00)
|
|
204
|
+
const allNighterHours = [4, 6];
|
|
205
|
+
const allNighterFound = allNighterHours.some(hour => hourCounts[hour] && hourCounts[hour] > 0);
|
|
206
|
+
|
|
207
|
+
if (allNighterFound) {
|
|
208
|
+
console.log(`\nAll-nighter patterns detected:`);
|
|
209
|
+
allNighterHours.forEach(hour => {
|
|
210
|
+
if (hourCounts[hour] && hourCounts[hour] > 0) {
|
|
211
|
+
console.log(` ${hour}:00 - ${hourCounts[hour]} times (potential all-nighter)`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
4
217
|
// Function to analyze steps by planet sign
|
|
5
218
|
async function analyzeStepsByPlanetSign(planetName, healthData, timeLimitDays = null) {
|
|
6
219
|
let stepRecords = extractStepData(healthData);
|
|
@@ -421,5 +634,7 @@ async function analyzePlanetAspectsForSleep(planetName, healthData, sleepGoal =
|
|
|
421
634
|
module.exports = {
|
|
422
635
|
analyzeStepsByPlanetSign,
|
|
423
636
|
analyzeStressByPlanetAspects,
|
|
424
|
-
analyzePlanetAspectsForSleep
|
|
637
|
+
analyzePlanetAspectsForSleep,
|
|
638
|
+
analyzeLateNightAspects,
|
|
639
|
+
analyzeAllNighterAspects
|
|
425
640
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a bar chart SVG from CSV analysis data
|
|
6
|
+
* @param {Object} data - CSV analysis data
|
|
7
|
+
* @param {string} title - Chart title
|
|
8
|
+
* @param {string} outputPath - Output file path
|
|
9
|
+
* @param {boolean} showAspects - Whether to show aspects or house distribution
|
|
10
|
+
* @returns {Promise<string>} Path to the generated image
|
|
11
|
+
*/
|
|
12
|
+
async function generateBarChart(data, title, outputPath, showAspects = false) {
|
|
13
|
+
// SVG dimensions
|
|
14
|
+
const width = 1200;
|
|
15
|
+
const height = 800;
|
|
16
|
+
const padding = 60;
|
|
17
|
+
const chartWidth = width - 2 * padding;
|
|
18
|
+
const chartHeight = height - 2 * padding;
|
|
19
|
+
const titleHeight = 80;
|
|
20
|
+
const legendHeight = 100;
|
|
21
|
+
|
|
22
|
+
// Colors
|
|
23
|
+
const barColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
|
24
|
+
|
|
25
|
+
// Prepare data for charting
|
|
26
|
+
let chartData, labels, maxValue, dataType;
|
|
27
|
+
|
|
28
|
+
if (showAspects && data.aspectStatistics) {
|
|
29
|
+
// Aspect statistics chart
|
|
30
|
+
dataType = 'Aspects';
|
|
31
|
+
chartData = data.aspectStatistics.types.map((type, index) => ({
|
|
32
|
+
label: type,
|
|
33
|
+
value: data.aspectStatistics.counts[type] || 0,
|
|
34
|
+
color: barColors[index % barColors.length]
|
|
35
|
+
}));
|
|
36
|
+
maxValue = Math.max(...chartData.map(d => d.value), 10);
|
|
37
|
+
} else {
|
|
38
|
+
// House distribution chart (default)
|
|
39
|
+
dataType = 'House Distribution';
|
|
40
|
+
chartData = [];
|
|
41
|
+
|
|
42
|
+
// Ensure houseDistribution exists and has counts
|
|
43
|
+
const houseDist = data.houseDistribution || { counts: {} };
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i <= 12; i++) {
|
|
46
|
+
chartData.push({
|
|
47
|
+
label: `House ${i}`,
|
|
48
|
+
value: houseDist.counts[i] || 0,
|
|
49
|
+
color: barColors[(i-1) % barColors.length]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
maxValue = Math.max(...chartData.map(d => d.value), 10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Calculate bar dimensions
|
|
57
|
+
const barCount = chartData.length;
|
|
58
|
+
const barWidth = chartWidth / barCount / 1.5;
|
|
59
|
+
const maxBarHeight = chartHeight - titleHeight - legendHeight;
|
|
60
|
+
const yScale = maxBarHeight / maxValue;
|
|
61
|
+
|
|
62
|
+
// Generate SVG
|
|
63
|
+
const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
64
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
65
|
+
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
66
|
+
<!-- Background -->
|
|
67
|
+
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
|
68
|
+
|
|
69
|
+
<!-- Title -->
|
|
70
|
+
<text x="${width/2}" y="${padding + titleHeight/2 + 10}" font-family="Arial" font-size="24" font-weight="bold" text-anchor="middle" fill="#333333">${title}</text>
|
|
71
|
+
|
|
72
|
+
<!-- Statistical Subtitle -->
|
|
73
|
+
${data.statistics ? `
|
|
74
|
+
<text x="${width/2}" y="${padding + titleHeight/2 + 40}" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle" fill="#666666">
|
|
75
|
+
χ²(${data.statistics.degreesOfFreedom}) = ${data.statistics.chiSquare}, p ${data.statistics.isSignificant ? '< 0.05' : '≥ 0.05'}, Cramer's V = ${data.statistics.effectSize}
|
|
76
|
+
</text>
|
|
77
|
+
` : ''}
|
|
78
|
+
|
|
79
|
+
<!-- Y-axis grid lines -->
|
|
80
|
+
${Array.from({length: 6}).map((_, i) => {
|
|
81
|
+
const y = height - padding - (i * maxBarHeight / 5);
|
|
82
|
+
return `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" stroke="#e0e0e0" stroke-width="1"/>`;
|
|
83
|
+
}).join('')}
|
|
84
|
+
|
|
85
|
+
<!-- Y-axis labels -->
|
|
86
|
+
${Array.from({length: 6}).map((_, i) => {
|
|
87
|
+
const y = height - padding - (i * maxBarHeight / 5);
|
|
88
|
+
const value = Math.round(maxValue * (i / 5));
|
|
89
|
+
return `<text x="${padding - 10}" y="${y + 4}" font-family="Arial" font-size="14" text-anchor="end" fill="#666666">${value}</text>`;
|
|
90
|
+
}).join('')}
|
|
91
|
+
|
|
92
|
+
<!-- X-axis -->
|
|
93
|
+
<line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="#666666" stroke-width="2"/>
|
|
94
|
+
|
|
95
|
+
<!-- Bars -->
|
|
96
|
+
${chartData.map((d, index) => {
|
|
97
|
+
const barHeight = d.value * yScale;
|
|
98
|
+
const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
|
|
99
|
+
const y = height - padding - barHeight;
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<rect x="${x - barWidth / 2}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${d.color}"/>
|
|
103
|
+
${barHeight > 20 ? `<text x="${x}" y="${y + 20}" font-family="Arial" font-size="14" font-weight="bold" text-anchor="middle" fill="white">${d.value}</text>` : ''}
|
|
104
|
+
`;
|
|
105
|
+
}).join('')}
|
|
106
|
+
|
|
107
|
+
<!-- X-axis labels -->
|
|
108
|
+
${chartData.map((d, index) => {
|
|
109
|
+
const x = padding + (index * (barWidth * 1.5)) + barWidth / 2;
|
|
110
|
+
const y = height - padding + 25;
|
|
111
|
+
return `<text x="${x}" y="${y}" font-family="Arial" font-size="14" text-anchor="middle" fill="#666666">${d.label}</text>`;
|
|
112
|
+
}).join('')}
|
|
113
|
+
</svg>`;
|
|
114
|
+
|
|
115
|
+
// Save the SVG file
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
fs.writeFile(outputPath, svgContent, 'utf8', (error) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
reject(error);
|
|
120
|
+
} else {
|
|
121
|
+
resolve(outputPath);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
generateBarChart
|
|
129
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gets the default downloads folder path for the current operating system
|
|
6
|
+
* @returns {string} Path to the downloads folder
|
|
7
|
+
*/
|
|
8
|
+
function getDownloadsFolder() {
|
|
9
|
+
const homeDir = os.homedir();
|
|
10
|
+
|
|
11
|
+
switch (process.platform) {
|
|
12
|
+
case 'win32':
|
|
13
|
+
// Windows: %USERPROFILE%\Downloads
|
|
14
|
+
return path.join(homeDir, 'Downloads');
|
|
15
|
+
|
|
16
|
+
case 'darwin':
|
|
17
|
+
// macOS: ~/Downloads
|
|
18
|
+
return path.join(homeDir, 'Downloads');
|
|
19
|
+
|
|
20
|
+
case 'linux':
|
|
21
|
+
// Linux: Check common locations
|
|
22
|
+
const xdgDownload = process.env.XDG_DOWNLOAD_DIR;
|
|
23
|
+
if (xdgDownload) {
|
|
24
|
+
return xdgDownload;
|
|
25
|
+
}
|
|
26
|
+
// Fallback to ~/Downloads
|
|
27
|
+
return path.join(homeDir, 'Downloads');
|
|
28
|
+
|
|
29
|
+
default:
|
|
30
|
+
// Fallback for other platforms
|
|
31
|
+
return path.join(homeDir, 'Downloads');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensures the downloads folder exists and is writable
|
|
37
|
+
* @returns {string} Valid downloads folder path
|
|
38
|
+
*/
|
|
39
|
+
function getValidDownloadsFolder() {
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const downloadsFolder = getDownloadsFolder();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Check if folder exists
|
|
45
|
+
if (!fs.existsSync(downloadsFolder)) {
|
|
46
|
+
// Try to create it
|
|
47
|
+
fs.mkdirSync(downloadsFolder, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if folder is writable
|
|
51
|
+
const testFile = path.join(downloadsFolder, '.write_test_' + Date.now());
|
|
52
|
+
try {
|
|
53
|
+
fs.writeFileSync(testFile, 'test');
|
|
54
|
+
fs.unlinkSync(testFile);
|
|
55
|
+
return downloadsFolder;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Fallback to current directory if downloads folder is not writable
|
|
58
|
+
console.warn(`⚠️ Downloads folder not writable: ${downloadsFolder}, using current directory`);
|
|
59
|
+
return process.cwd();
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(`⚠️ Could not access downloads folder: ${error.message}, using current directory`);
|
|
63
|
+
return process.cwd();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getDownloadsFolder,
|
|
69
|
+
getValidDownloadsFolder
|
|
70
|
+
};
|