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 +5 -2
- package/package.json +1 -1
- package/src/astrology/astrologyConstants.js +7 -0
- package/src/astrology/astrologyService.js +315 -125
- package/src/cli/cli.js +125 -7
- package/src/utils/chartGenerator.js +129 -0
- package/src/utils/downloadsFolder.js +70 -0
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>`
|
|
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
|
@@ -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
|
|
462
|
-
|
|
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
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
1972
|
-
// Look for a column with
|
|
1973
|
-
const datetimeColumns =
|
|
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
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
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
|
-
//
|
|
2131
|
-
|
|
2269
|
+
// Single criteria
|
|
2270
|
+
const recordValue = data[parsedCriteria.column];
|
|
2271
|
+
shouldSkip = !recordValue || recordValue.toString() !== parsedCriteria.value;
|
|
2132
2272
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
sign: astroData.sign,
|
|
2178
|
-
degreeInSign: astroData.degreeInSign,
|
|
2179
|
-
house: house
|
|
2180
|
-
};
|
|
2361
|
+
if (analyzeAspects) {
|
|
2362
|
+
result.aspects = aspects;
|
|
2363
|
+
}
|
|
2181
2364
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2365
|
+
results.push(result);
|
|
2366
|
+
checkCompletion();
|
|
2367
|
+
})
|
|
2368
|
+
.catch(error => {
|
|
2369
|
+
console.error('Fehler bei der Hausberechnung:', error);
|
|
2185
2370
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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
|
-
|
|
2218
|
-
|
|
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
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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
|
-
|
|
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 ? '< 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
|
+
};
|