klio 1.4.8 → 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 CHANGED
@@ -97,8 +97,10 @@ 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
+ - **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
102
 
101
- The command also returns a Chi-Square.
103
+ - The command also returns a Chi-Square.
102
104
 
103
105
  ### Adding different charts
104
106
 
@@ -109,7 +111,7 @@ The command also returns a Chi-Square.
109
111
  - `--people` - Lists all saved persons
110
112
  - `--delete-person <id>` - Deletes a person
111
113
 
112
- Then, instead using `--i` for the commands from above you can use `--wp <id>` i.e. `--wp john`
114
+ Then, instead using `--i` for the commands from above you can use `--wp <id>` or `--wp john`
113
115
 
114
116
  ### Advanced Features
115
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.8",
3
+ "version": "1.4.9",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -1900,7 +1900,39 @@ function analyzeSignDistributionSignificance(signDistribution) {
1900
1900
  };
1901
1901
  }
1902
1902
 
1903
- function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null) {
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 records) {
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];
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)')
@@ -1495,8 +1507,9 @@ program
1495
1507
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
1496
1508
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
1497
1509
  const analyzeAspects = actualOptions.a || false;
1510
+ const filterCriteria = actualOptions.filter;
1498
1511
 
1499
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
1512
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
1500
1513
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
1501
1514
  if (results.length === 0) {
1502
1515
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -1614,6 +1627,60 @@ program
1614
1627
  // Also show total number of records and house system
1615
1628
  console.log(`\nIn Total: ${results.length}`);
1616
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
+ }
1617
1684
  }
1618
1685
  })
1619
1686
  .catch(error => {
@@ -2509,8 +2576,9 @@ program
2509
2576
  const planet = planetArg ? planetArg.toLowerCase() : 'moon';
2510
2577
  const houseSystem = actualOptions.hs ? actualOptions.hs.toLowerCase() : 'koch';
2511
2578
  const analyzeAspects = actualOptions.a || false;
2579
+ const filterCriteria = actualOptions.filter;
2512
2580
 
2513
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2)
2581
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
2514
2582
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
2515
2583
  if (results.length === 0) {
2516
2584
  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
+ };