klio 1.4.8 → 1.5.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.
package/README.md CHANGED
@@ -97,8 +97,11 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
97
97
 
98
98
  - **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
99
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
+ - **Filter CSV data by multiple conditions:** `klio [planet] --csv <file-path> --filter "column1:value1,column2:value2"` (e.g., `--filter "FTR:H,HomeTeam:Liverpool"`)
102
+ - **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"`
100
103
 
101
- The command also returns a Chi-Square.
104
+ - The command also returns a Chi-Square.
102
105
 
103
106
  ### Adding different charts
104
107
 
@@ -109,7 +112,7 @@ The command also returns a Chi-Square.
109
112
  - `--people` - Lists all saved persons
110
113
  - `--delete-person <id>` - Deletes a person
111
114
 
112
- Then, instead using `--i` for the commands from above you can use `--wp <id>` i.e. `--wp john`
115
+ Then, instead using `--i` for the commands from above you can use `--wp <id>` or `--wp john`
113
116
 
114
117
  ### Advanced Features
115
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.8",
3
+ "version": "1.5.0",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -24,6 +24,12 @@ const elements = [
24
24
  'Air', 'Water', 'Fire', 'Earth', 'Air', 'Water'
25
25
  ];
26
26
 
27
+ // Sign types (Cardinal, Fixed, Mutable)
28
+ const signTypes = [
29
+ 'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable',
30
+ 'Cardinal', 'Fixed', 'Mutable', 'Cardinal', 'Fixed', 'Mutable'
31
+ ];
32
+
27
33
  // Decans
28
34
  const decans = [
29
35
  '1st Decan', '2nd Decan', '3rd Decan',
@@ -50,6 +56,7 @@ module.exports = {
50
56
  planets,
51
57
  signs,
52
58
  elements,
59
+ signTypes,
53
60
  decans,
54
61
  dignities
55
62
  };
@@ -3,7 +3,7 @@ const fs = require('fs');
3
3
  const moment = require('moment-timezone');
4
4
  const csvParser = require('csv-parser');
5
5
  const { parse } = require('csv-parse/sync');
6
- const { planets, signs, elements, decans, dignities } = require('./astrologyConstants');
6
+ const { planets, signs, elements, signTypes, decans, dignities } = require('./astrologyConstants');
7
7
  const { loadConfig } = require('../config/configService');
8
8
  const path = require('path');
9
9
 
@@ -454,12 +454,18 @@ function getAstrologicalData(planetName, customDate = null) {
454
454
  };
455
455
  }
456
456
 
457
+
458
+
457
459
  // Function to identify critical planets
458
460
  function getCriticalPlanets(customDate = null) {
459
461
  const criticalPlanets = [];
460
462
 
461
- // Critical degrees: 0°, 13°, 26° (cardinal degrees) and 29° (anaretic degree)
462
- const criticalDegrees = [0, 13, 26, 29];
463
+ // Critical degrees by sign type:
464
+ // Cardinal signs (Aries, Cancer, Libra, Capricorn): 0°, 13°, 26°
465
+ // Fixed signs (Taurus, Leo, Scorpio, Aquarius): 8-9°, 21-22°
466
+ // Mutable signs (Gemini, Virgo, Sagittarius, Pisces): 4°, 17°
467
+ // Anaretic degree (all signs): 29°
468
+
463
469
  const orb = 1; // Tolerance of 1 degree
464
470
 
465
471
  // Use the specified date or current time
@@ -502,19 +508,55 @@ function getCriticalPlanets(customDate = null) {
502
508
  const degreeInSign = longitude % 30;
503
509
  const signIndex = Math.floor(longitude / 30);
504
510
  const sign = signs[signIndex];
511
+ const signType = signTypes[signIndex];
512
+
513
+ // Determine critical degrees based on sign type
514
+ let isCritical = false;
515
+ let criticalType = '';
505
516
 
506
- // Check if planet is on a critical degree
507
- const isCritical = criticalDegrees.some(criticalDegree => {
508
- return Math.abs(degreeInSign - criticalDegree) <= orb;
509
- });
510
-
517
+ // First check for Anaretic degree (29°) - applies to all signs
518
+ if (degreeInSign >= 28.8) { // 29° with 0.2° orb for precision
519
+ isCritical = true;
520
+ criticalType = 'Anaretic (29°)';
521
+ } else if (signType === 'Cardinal') {
522
+ // Cardinal: exact degrees 0°, 13°, 26° with orb
523
+ isCritical = [0, 13, 26].some(criticalDegree => {
524
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
525
+ });
526
+ criticalType = 'Cardinal (0°, 13°, 26°)';
527
+ } else if (signType === 'Fixed') {
528
+ // Fixed: ranges 8-9° and 21-22°
529
+ isCritical = (degreeInSign >= 8 && degreeInSign <= 9) || (degreeInSign >= 21 && degreeInSign <= 22);
530
+ criticalType = 'Fixed (8-9°, 21-22°)';
531
+ } else if (signType === 'Mutable') {
532
+ // Mutable: exact degrees 4°, 17° with orb
533
+ isCritical = [4, 17].some(criticalDegree => {
534
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
535
+ });
536
+ criticalType = 'Mutable (4°, 17°)';
537
+ }
538
+
511
539
  if (isCritical) {
540
+ // Simple interpretation based on modality challenges
541
+ let interpretation = '';
542
+
543
+ if (criticalType === 'Cardinal (0°, 13°, 26°)') {
544
+ interpretation = 'Tendency to over-express. May push too hard or initiate prematurely.';
545
+ } else if (criticalType === 'Fixed (8-9°, 21-22°)') {
546
+ interpretation = 'Tendency to under-express. May resist change or hold on too tightly.';
547
+ } else if (criticalType === 'Mutable (4°, 17°)') {
548
+ interpretation = 'Path of ambivalence. May struggle with indecision or adapt excessively.';
549
+ } else if (criticalType === 'Anaretic (29°)') {
550
+ interpretation = 'Poised for change. At the end of a cycle, facing transition.';
551
+ }
552
+
512
553
  criticalPlanets.push({
513
554
  name,
514
555
  sign,
515
556
  degree: degreeInSign.toFixed(2),
516
557
  isCritical: true,
517
- criticalType: degreeInSign >= 28.5 ? 'Anaretic (29°)' : 'Cardinal (0°, 13°, 26°)'
558
+ criticalType: criticalType,
559
+ interpretation: interpretation
518
560
  });
519
561
  }
520
562
  }
@@ -1900,13 +1942,66 @@ function analyzeSignDistributionSignificance(signDistribution) {
1900
1942
  };
1901
1943
  }
1902
1944
 
1903
- function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null) {
1945
+ // Helper function to parse filter criteria
1946
+ function parseFilterCriteria(filterString) {
1947
+ if (!filterString) return null;
1948
+
1949
+ // Check if multiple filters are provided (comma-separated)
1950
+ const filterConditions = filterString.split(',').map(cond => cond.trim());
1951
+
1952
+ const criteria = [];
1953
+
1954
+ for (const condition of filterConditions) {
1955
+ const parts = condition.split(':');
1956
+ if (parts.length !== 2) {
1957
+ console.warn(`Invalid filter format. Expected "column:value" but got "${condition}"`);
1958
+ return null;
1959
+ }
1960
+
1961
+ criteria.push({
1962
+ column: parts[0].trim(),
1963
+ value: parts[1].trim()
1964
+ });
1965
+ }
1966
+
1967
+ return criteria.length === 1 ? criteria[0] : criteria;
1968
+ }
1969
+
1970
+ // Helper function to apply filter to records
1971
+ function applyFilter(records, filterCriteria) {
1972
+ if (!filterCriteria || !records || records.length === 0) return records;
1973
+
1974
+ const parsedCriteria = typeof filterCriteria === 'string'
1975
+ ? parseFilterCriteria(filterCriteria)
1976
+ : filterCriteria;
1977
+
1978
+ if (!parsedCriteria) return records;
1979
+
1980
+ // Handle both single criteria and multiple criteria
1981
+ if (Array.isArray(parsedCriteria)) {
1982
+ return records.filter(record => {
1983
+ return parsedCriteria.every(criterion => {
1984
+ const recordValue = record[criterion.column];
1985
+ return recordValue && recordValue.toString() === criterion.value;
1986
+ });
1987
+ });
1988
+ } else {
1989
+ // Single criteria (backward compatibility)
1990
+ return records.filter(record => {
1991
+ const recordValue = record[parsedCriteria.column];
1992
+ return recordValue && recordValue.toString() === parsedCriteria.value;
1993
+ });
1994
+ }
1995
+ }
1996
+
1997
+ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
1904
1998
  return new Promise(async (resolve, reject) => {
1905
1999
  const results = [];
1906
2000
  let pendingOperations = 0;
1907
2001
 
1908
2002
  // Helper function to process results
1909
2003
  function processResults() {
2004
+
1910
2005
  // Calculate house distribution
1911
2006
  const houseCounts = {};
1912
2007
  results.forEach(result => {
@@ -1967,17 +2062,38 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1967
2062
  skip_empty_lines: true
1968
2063
  });
1969
2064
 
2065
+ // Apply filter if specified
2066
+ const filteredRecords = filterCriteria ? applyFilter(records, filterCriteria) : records;
2067
+
1970
2068
  // Process each row
1971
- for (const data of records) {
1972
- // Look for a column with ISO-Datetime values or Unix-Timestamps
1973
- const datetimeColumns = Object.keys(data).filter(key => {
2069
+ for (const data of filteredRecords) {
2070
+ // Look for a column with datetime values, prioritizing specific date formats
2071
+ const datetimeColumns = [];
2072
+
2073
+ // First pass: look for YYYY-MM-DD format (most specific)
2074
+ const yyyyMmDdColumns = Object.keys(data).filter(key => {
1974
2075
  const value = data[key];
1975
- // Check for ISO-8601 date
1976
- const isISO = moment(value, moment.ISO_8601, true).isValid();
1977
- // Check for Unix-Timestamp (number with 10 or 13 digits)
1978
- const isUnixTimestamp = /^\d{10,13}$/.test(value);
1979
- return isISO || isUnixTimestamp;
2076
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
2077
+ });
2078
+
2079
+ // Second pass: look for Unix timestamps (10 or 13 digits)
2080
+ const unixTimestampColumns = Object.keys(data).filter(key => {
2081
+ const value = data[key];
2082
+ return /^\d{10,13}$/.test(value);
2083
+ });
2084
+
2085
+ // Third pass: look for ISO-8601 dates (least specific, as it can match many formats)
2086
+ const isoDateColumns = Object.keys(data).filter(key => {
2087
+ const value = data[key];
2088
+ // Only consider it an ISO date if it's not already matched by more specific patterns
2089
+ if (yyyyMmDdColumns.includes(key) || unixTimestampColumns.includes(key)) {
2090
+ return false;
2091
+ }
2092
+ return moment(value, moment.ISO_8601, true).isValid();
1980
2093
  });
2094
+
2095
+ // Prioritize columns: YYYY-MM-DD first, then Unix timestamps, then ISO dates
2096
+ datetimeColumns.push(...yyyyMmDdColumns, ...unixTimestampColumns, ...isoDateColumns);
1981
2097
 
1982
2098
  if (datetimeColumns.length > 0) {
1983
2099
  const datetimeValue = data[datetimeColumns[0]];
@@ -1988,6 +2104,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1988
2104
  // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
1989
2105
  const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
1990
2106
  datetime = moment(timestamp);
2107
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2108
+ // Handle as YYYY-MM-DD date format
2109
+ // Parse as date only, set time to 12:00 (noon) as default
2110
+ datetime = moment(datetimeValue + 'T12:00:00');
1991
2111
  } else {
1992
2112
  // Handle as ISO-8601 date
1993
2113
  datetime = moment(datetimeValue);
@@ -2086,8 +2206,10 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2086
2206
  }
2087
2207
  }
2088
2208
 
2089
- // Check if all operations are completed
2090
- checkCompletion();
2209
+ // Check if all operations are completed (only if no operations were started)
2210
+ if (pendingOperations === 0) {
2211
+ processResults();
2212
+ }
2091
2213
 
2092
2214
  // Helper function to check completion
2093
2215
  function checkCompletion() {
@@ -2104,125 +2226,193 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2104
2226
  }
2105
2227
  } else {
2106
2228
  // Standard file processing for local files
2107
- fs.createReadStream(filePath)
2108
- .pipe(csvParser())
2109
- .on('data', (data) => {
2110
- // Look for a column with ISO-Datetime values or Unix-Timestamps
2111
- const datetimeColumns = Object.keys(data).filter(key => {
2112
- const value = data[key];
2113
- // Check for ISO-8601 date
2114
- const isISO = moment(value, moment.ISO_8601, true).isValid();
2115
- // Check for Unix-Timestamp (number with 10 or 13 digits)
2116
- const isUnixTimestamp = /^\d{10,13}$/.test(value);
2117
- return isISO || isUnixTimestamp;
2118
- });
2119
-
2120
- if (datetimeColumns.length > 0) {
2121
- const datetimeValue = data[datetimeColumns[0]];
2122
- let datetime;
2123
-
2124
- // Check if it's a Unix-Timestamp
2125
- if (/^\d{10,13}$/.test(datetimeValue)) {
2126
- // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2127
- const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2128
- datetime = moment(timestamp);
2229
+ try {
2230
+ // Read the file content first
2231
+ const csvData = fs.readFileSync(filePath, 'utf8');
2232
+
2233
+ // Try to auto-detect delimiter by checking first few lines
2234
+ let delimiter = ',';
2235
+ const firstLines = csvData.split('\n').slice(0, 5).join('\n');
2236
+ const commaCount = (firstLines.match(/,/g) || []).length;
2237
+ const semicolonCount = (firstLines.match(/;/g) || []).length;
2238
+
2239
+ // Use semicolon if it appears more frequently than comma
2240
+ if (semicolonCount > commaCount) {
2241
+ delimiter = ';';
2242
+ }
2243
+
2244
+ // Parse the CSV data with detected delimiter
2245
+ const records = parse(csvData, {
2246
+ columns: true,
2247
+ skip_empty_lines: true,
2248
+ delimiter: delimiter
2249
+ });
2250
+
2251
+ // Process each record
2252
+ for (const data of records) {
2253
+ // Apply filter if specified
2254
+ if (filterCriteria) {
2255
+ const parsedCriteria = typeof filterCriteria === 'string'
2256
+ ? parseFilterCriteria(filterCriteria)
2257
+ : filterCriteria;
2258
+
2259
+ if (parsedCriteria) {
2260
+ let shouldSkip = false;
2261
+
2262
+ if (Array.isArray(parsedCriteria)) {
2263
+ // Multiple criteria - all must match
2264
+ shouldSkip = !parsedCriteria.every(criterion => {
2265
+ const recordValue = data[criterion.column];
2266
+ return recordValue && recordValue.toString() === criterion.value;
2267
+ });
2129
2268
  } else {
2130
- // Handle as ISO-8601 date
2131
- datetime = moment(datetimeValue);
2269
+ // Single criteria
2270
+ const recordValue = data[parsedCriteria.column];
2271
+ shouldSkip = !recordValue || recordValue.toString() !== parsedCriteria.value;
2132
2272
  }
2133
-
2134
- // Convert the date to the format needed for astrological calculations
2135
- const dateComponents = {
2136
- year: datetime.year(),
2137
- month: datetime.month() + 1, // moment months are 0-based
2138
- day: datetime.date(),
2139
- hour: datetime.hour(),
2140
- minute: datetime.minute()
2141
- };
2142
-
2143
- // Calculate the astrological data for the specified planet
2144
- let astroData;
2145
- try {
2146
- astroData = getAstrologicalData(planetName, dateComponents);
2147
- } catch (error) {
2148
- // If error, skip this record (e.g., if date is out of range)
2149
- return;
2273
+
2274
+ if (shouldSkip) {
2275
+ continue; // Skip this record
2150
2276
  }
2277
+ }
2278
+ }
2279
+
2280
+ // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, or Unix-Timestamps
2281
+ const datetimeColumns = Object.keys(data).filter(key => {
2282
+ const value = data[key];
2283
+ // Check for ISO-8601 date
2284
+ const isISO = moment(value, moment.ISO_8601, true).isValid();
2285
+ // Check for YYYY-MM-DD date format
2286
+ const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(value);
2287
+ // Check for Unix-Timestamp (number with 10 or 13 digits)
2288
+ const isUnixTimestamp = /^\d{10,13}$/.test(value);
2289
+ return isISO || isYYYYMMDD || isUnixTimestamp;
2290
+ });
2291
+
2292
+ if (datetimeColumns.length > 0) {
2293
+ const datetimeValue = data[datetimeColumns[0]];
2294
+ let datetime;
2295
+
2296
+ // Check if it's a Unix-Timestamp
2297
+ if (/^\d{10,13}$/.test(datetimeValue)) {
2298
+ // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2299
+ const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2300
+ datetime = moment(timestamp);
2301
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2302
+ // Handle as YYYY-MM-DD date format
2303
+ // Parse as date only, set time to 12:00 (noon) as default
2304
+ datetime = moment(datetimeValue + 'T12:00:00');
2305
+ } else {
2306
+ // Handle as ISO-8601 date
2307
+ datetime = moment(datetimeValue);
2308
+ }
2309
+
2310
+ // Convert the date to the format needed for astrological calculations
2311
+ const dateComponents = {
2312
+ year: datetime.year(),
2313
+ month: datetime.month() + 1, // moment months are 0-based
2314
+ day: datetime.date(),
2315
+ hour: datetime.hour(),
2316
+ minute: datetime.minute()
2317
+ };
2151
2318
 
2152
- // Calculate the houses
2153
- const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2154
- calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2155
- .then(houses => {
2156
- const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
2157
- const house = getPlanetHouse(planetLongitude, houses.house);
2158
-
2159
- // Berechne Aspekte, falls angefordert
2160
- let aspects = [];
2161
- if (analyzeAspects) {
2162
- try {
2163
- aspects = calculatePlanetAspects(planetName, dateComponents, true);
2164
-
2165
- // Filter nach Partner-Planet, falls angegeben
2166
- if (partnerPlanet) {
2167
- aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2168
- }
2169
- } catch (error) {
2170
- // Ignoriere Fehler bei der Aspektberechnung für einzelne Datensätze
2319
+ // Calculate the astrological data for the specified planet
2320
+ let astroData;
2321
+ try {
2322
+ astroData = getAstrologicalData(planetName, dateComponents);
2323
+ } catch (error) {
2324
+ // If error, skip this record (e.g., if date is out of range)
2325
+ continue;
2326
+ }
2327
+
2328
+ // Increase the counter for pending operations
2329
+ pendingOperations++;
2330
+
2331
+ // Calculate the houses
2332
+ const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2333
+ calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2334
+ .then(houses => {
2335
+ const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
2336
+ const house = getPlanetHouse(planetLongitude, houses.house);
2337
+
2338
+ // Berechne Aspekte, falls angefordert
2339
+ let aspects = [];
2340
+ if (analyzeAspects) {
2341
+ try {
2342
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2343
+
2344
+ // Filter nach Partner-Planet, falls angegeben
2345
+ if (partnerPlanet) {
2346
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2171
2347
  }
2348
+ } catch (error) {
2349
+ // Ignoriere Fehler bei der Aspektberechnung für einzelne Datensätze
2172
2350
  }
2351
+ }
2352
+
2353
+ const result = {
2354
+ datetime: datetimeValue,
2355
+ planet: planetName,
2356
+ sign: astroData.sign,
2357
+ degreeInSign: astroData.degreeInSign,
2358
+ house: house
2359
+ };
2173
2360
 
2174
- const result = {
2175
- datetime: datetimeValue,
2176
- planet: planetName,
2177
- sign: astroData.sign,
2178
- degreeInSign: astroData.degreeInSign,
2179
- house: house
2180
- };
2361
+ if (analyzeAspects) {
2362
+ result.aspects = aspects;
2363
+ }
2181
2364
 
2182
- if (analyzeAspects) {
2183
- result.aspects = aspects;
2184
- }
2365
+ results.push(result);
2366
+ checkCompletion();
2367
+ })
2368
+ .catch(error => {
2369
+ console.error('Fehler bei der Hausberechnung:', error);
2185
2370
 
2186
- results.push(result);
2187
- })
2188
- .catch(error => {
2189
- console.error('Fehler bei der Hausberechnung:', error);
2190
-
2191
- // Berechne Aspekte auch bei Hausberechnungsfehler, falls angefordert
2192
- let aspects = [];
2193
- if (analyzeAspects) {
2194
- try {
2195
- aspects = calculatePlanetAspects(planetName, dateComponents, true);
2196
-
2197
- // Filter nach Partner-Planet, falls angegeben
2198
- if (partnerPlanet) {
2199
- aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2200
- }
2201
- } catch (error) {
2202
- console.error('Fehler bei der Aspektberechnung:', error);
2371
+ // Berechne Aspekte auch bei Hausberechnungsfehler, falls angefordert
2372
+ let aspects = [];
2373
+ if (analyzeAspects) {
2374
+ try {
2375
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2376
+
2377
+ // Filter nach Partner-Planet, falls angegeben
2378
+ if (partnerPlanet) {
2379
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2203
2380
  }
2381
+ } catch (error) {
2382
+ console.error('Fehler bei der Aspektberechnung:', error);
2204
2383
  }
2384
+ }
2205
2385
 
2206
- results.push({
2207
- datetime: datetimeValue,
2208
- planet: planetName,
2209
- sign: astroData.sign,
2210
- degreeInSign: astroData.degreeInSign,
2211
- house: 'N/A',
2212
- ...(analyzeAspects ? {aspects} : {})
2213
- });
2386
+ results.push({
2387
+ datetime: datetimeValue,
2388
+ planet: planetName,
2389
+ sign: astroData.sign,
2390
+ degreeInSign: astroData.degreeInSign,
2391
+ house: 'N/A',
2392
+ ...(analyzeAspects ? {aspects} : {})
2214
2393
  });
2215
- }
2216
- })
2217
- .on('end', () => {
2218
- // Verarbeite die Ergebnisse
2394
+ checkCompletion();
2395
+ });
2396
+ }
2397
+ }
2398
+
2399
+ // Helper function to check completion
2400
+ function checkCompletion() {
2401
+ pendingOperations--;
2402
+ if (pendingOperations === 0) {
2219
2403
  processResults();
2220
- })
2221
- .on('error', (error) => {
2222
- console.error('Fehler beim Lesen der CSV-Datei:', error);
2223
- reject(error);
2224
- });
2225
- }
2404
+ }
2405
+ }
2406
+
2407
+ // Check if all operations are completed (only if no operations were started)
2408
+ if (pendingOperations === 0) {
2409
+ processResults();
2410
+ }
2411
+ } catch (error) {
2412
+ console.error('Fehler beim Lesen der CSV-Datei:', error);
2413
+ reject(error);
2414
+ }
2415
+ }
2226
2416
  })
2227
2417
  }
2228
2418
 
package/src/cli/cli.js CHANGED
@@ -11,6 +11,16 @@ 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 {
@@ -265,6 +275,8 @@ program
265
275
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
266
276
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
267
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)')
268
280
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
269
281
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
270
282
  .option('--gui', 'Launches the web interface for command history (port 37421)')
@@ -329,6 +341,54 @@ program
329
341
  }
330
342
  }
331
343
 
344
+ // Use custom date if specified (overrides person data)
345
+ if (options.d) {
346
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
347
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
348
+ const match = options.d.match(dateRegex);
349
+
350
+ if (match) {
351
+ const day = parseInt(match[1], 10);
352
+ const month = parseInt(match[2], 10);
353
+ const year = parseInt(match[3], 10);
354
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
355
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
356
+
357
+ // Check if the date is valid
358
+ const date = new Date(year, month - 1, day, hour, minute);
359
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
360
+ customDate = {
361
+ day: day,
362
+ month: month,
363
+ year: year,
364
+ hour: hour,
365
+ minute: minute
366
+ };
367
+ console.log(`Using custom date: ${day}.${month}.${year} ${hour}:${minute.toString().padStart(2, '0')}`);
368
+ } else {
369
+ console.error('Invalid date:', options.d);
370
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
371
+ process.exit(1);
372
+ }
373
+ } else {
374
+ console.error('Invalid date:', options.d);
375
+ console.error('💡 Please use the format: DD.MM.YYYY or "DD.MM.YYYY HH:MM" (with quotes for date with time)');
376
+ process.exit(1);
377
+ }
378
+ }
379
+
380
+ // If no custom date is specified, use current date
381
+ if (!customDate) {
382
+ const currentTime = getCurrentTimeInTimezone();
383
+ customDate = {
384
+ day: currentTime.day,
385
+ month: currentTime.month,
386
+ year: currentTime.year,
387
+ hour: currentTime.hour,
388
+ minute: currentTime.minute
389
+ };
390
+ }
391
+
332
392
  const criticalPlanets = getCriticalPlanets(customDate);
333
393
 
334
394
  if (criticalPlanets.length === 0) {
@@ -336,18 +396,20 @@ program
336
396
  return;
337
397
  }
338
398
 
339
- console.log('========================================================');
340
- console.log('| Planet | Sign | Degree | Type |');
341
- console.log('========================================================');
399
+ console.log('================================================================================================================');
400
+ console.log('| Planet | Sign | Degree | Type | Interpretation |');
401
+ console.log('================================================================================================================');
342
402
 
343
403
  criticalPlanets.forEach(planet => {
344
404
  const planetName = planet.name.charAt(0).toUpperCase() + planet.name.slice(1);
345
405
  const sign = planet.sign.padEnd(10, ' ');
346
406
  const degree = planet.degree.padEnd(5, ' ');
347
- console.log(`| ${planetName.padEnd(8)} | ${sign} | ${degree} | ${planet.criticalType} |`);
407
+ const criticalType = planet.criticalType.padEnd(20, ' ');
408
+ const interpretation = planet.interpretation.padEnd(46, ' ');
409
+ console.log(`| ${planetName.padEnd(8)} | ${sign} | ${degree} | ${criticalType} | ${interpretation} |`);
348
410
  });
349
411
 
350
- console.log('========================================================');
412
+ console.log('================================================================================================================');
351
413
  if (shouldUseBirthData(options)) {
352
414
  console.log('\nThis analysis is based on your birth chart.');
353
415
  } else {
@@ -1495,8 +1557,9 @@ program
1495
1557
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
1496
1558
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
1497
1559
  const analyzeAspects = actualOptions.a || false;
1560
+ const filterCriteria = actualOptions.filter;
1498
1561
 
1499
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
1562
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
1500
1563
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
1501
1564
  if (results.length === 0) {
1502
1565
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -1614,6 +1677,60 @@ program
1614
1677
  // Also show total number of records and house system
1615
1678
  console.log(`\nIn Total: ${results.length}`);
1616
1679
  console.log(`House system: ${houseSystem.charAt(0).toUpperCase() + houseSystem.slice(1)}`);
1680
+
1681
+ // Generate chart image if --title option is provided
1682
+ if (actualOptions.title && chartGenerator && downloadsFolder) {
1683
+ try {
1684
+ // Get the downloads folder path
1685
+ const downloadsPath = downloadsFolder.getValidDownloadsFolder();
1686
+
1687
+ // Create a safe filename from the title
1688
+ const safeTitle = actualOptions.title
1689
+ .replace(/[^a-zA-Z0-9\s\-_.]/g, '')
1690
+ .substring(0, 50)
1691
+ .trim();
1692
+
1693
+ const timestamp = new Date().toISOString()
1694
+ .replace(/[:.]/g, '-')
1695
+ .substring(0, 19);
1696
+
1697
+ const filename = `${timestamp}_${safeTitle}.png`;
1698
+ const outputPath = path.join(downloadsPath, filename);
1699
+
1700
+ console.log(`\n📊 Generating chart image: ${filename}`);
1701
+ console.log(`💾 Saving to: ${outputPath}`);
1702
+
1703
+ // Calculate statistics for the chart
1704
+ let chartStats = null;
1705
+ if (analyzeAspects && aspectStatistics) {
1706
+ const { analyzeAspectDistributionSignificance } = require('../astrology/astrologyService');
1707
+ chartStats = analyzeAspectDistributionSignificance(aspectStatistics);
1708
+ } else if (houseDistribution) {
1709
+ const { analyzeHouseDistributionSignificance } = require('../astrology/astrologyService');
1710
+ chartStats = analyzeHouseDistributionSignificance(houseDistribution);
1711
+ }
1712
+
1713
+ // Generate the chart
1714
+ const chartData = {
1715
+ houseDistribution: houseDistribution || { counts: {}, total: results.length },
1716
+ aspectStatistics: aspectStatistics || { types: [], counts: {}, total: 0 },
1717
+ results: results,
1718
+ statistics: chartStats
1719
+ };
1720
+
1721
+ return chartGenerator.generateBarChart(
1722
+ chartData,
1723
+ actualOptions.title,
1724
+ outputPath,
1725
+ analyzeAspects // Show aspects if --a option was used
1726
+ ).then(imagePath => {
1727
+ console.log(`✅ Chart image successfully saved to: ${imagePath}`);
1728
+ console.log(`📁 You can find it in your Downloads folder`);
1729
+ });
1730
+ } catch (error) {
1731
+ console.error('❌ Error generating chart image:', error.message);
1732
+ }
1733
+ }
1617
1734
  }
1618
1735
  })
1619
1736
  .catch(error => {
@@ -2509,8 +2626,9 @@ program
2509
2626
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
2510
2627
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
2511
2628
  const analyzeAspects = actualOptions.a || false;
2629
+ const filterCriteria = actualOptions.filter;
2512
2630
 
2513
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
2631
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
2514
2632
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
2515
2633
  if (results.length === 0) {
2516
2634
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -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 ? '&lt; 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
+ };