Skip to content

Commit 7c39695

Browse files
committed
feat: add print method for DataFrame display
1 parent 8368a3e commit 7c39695

8 files changed

Lines changed: 298 additions & 0 deletions

File tree

.changeset/short-owls-tap.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'tinyframejs': major
3+
---
4+
5+
# First minimal working version
6+
7+
## WHAT
8+
This is the first stable release of TinyFrameJS with a complete API for DataFrame operations. Major changes include:
9+
- Replacement of the vulnerable xlsx library (v0.18.5) with exceljs (v4.4.0)
10+
- Fixed imports of non-existent modules
11+
- Added proper export of the cloneFrame function
12+
- Fixed bug in sort.js where it called non-existent frame.clone() method
13+
14+
## WHY
15+
These changes were necessary to:
16+
- Address security vulnerabilities in the xlsx dependency
17+
- Fix critical bugs that prevented proper library usage
18+
- Ensure consistent API across the library
19+
- Improve overall stability and reliability
20+
21+
## HOW TO UPDATE
22+
If you were using a previous version:
23+
- Replace any calls to frame.clone() with cloneFrame(frame)
24+
- If you were directly using xlsx functionality, update to the exceljs API
25+
- Review any import statements that might have been relying on the previously non-existent modules

.npmignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,13 @@ benchmarks/
4242

4343
# Dependencies
4444
node_modules/
45+
build/
46+
dist/
47+
.DS_Store
48+
49+
# Documentation
50+
../tinyframejs-docs/
51+
tinyframejs-docs/
52+
docs-site/
53+
docs/
54+
typedoc.json

examples/print-example.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// examples/print-example.js
2+
3+
import { DataFrame } from '../src/core/DataFrame.js';
4+
5+
// Create test data
6+
const data = [
7+
{ name: 'Alice', age: 25, city: 'New York', salary: 75000 },
8+
{ name: 'Bob', age: 30, city: 'Boston', salary: 85000 },
9+
{ name: 'Charlie', age: 35, city: 'Chicago', salary: 95000 },
10+
{ name: 'David', age: 40, city: 'Denver', salary: 105000 },
11+
{ name: 'Eve', age: 45, city: 'El Paso', salary: 115000 },
12+
{ name: 'Frank', age: 50, city: 'Fresno', salary: 125000 },
13+
{ name: 'Grace', age: 55, city: 'Greensboro', salary: 135000 },
14+
{ name: 'Hannah', age: 60, city: 'Houston', salary: 145000 },
15+
{ name: 'Ian', age: 65, city: 'Indianapolis', salary: 155000 },
16+
{ name: 'Julia', age: 70, city: 'Jacksonville', salary: 165000 },
17+
{ name: 'Kevin', age: 75, city: 'Kansas City', salary: 175000 },
18+
{ name: 'Laura', age: 80, city: 'Los Angeles', salary: 185000 },
19+
];
20+
21+
// Create DataFrame
22+
const df = DataFrame.create(data);
23+
24+
// Use the print method to display data in the console
25+
console.log('Standard output (default 10 rows):');
26+
df.print();
27+
28+
console.log('\nOutput with row limit:');
29+
df.print({ maxRows: 5 });
30+
31+
console.log('\nOutput with column limit:');
32+
df.print({ maxCols: 2 });
33+
34+
console.log('\nOutput without indices:');
35+
df.print({ showIndex: false });
36+
37+
console.log('\nFull output:');
38+
df.print({ maxRows: 20, maxCols: 10 });
39+
40+
// Example of using in a method chain (when they are implemented)
41+
// df.sort('age').filter(row => row.salary > 100000).print();

src/methods/display/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { print } from './print.js';

src/methods/display/print.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Formats the DataFrame as a string table for console display.
3+
* @param frame
4+
* @param {Object} options - Display options
5+
* @param {number} [options.maxRows=10] - Maximum number of rows to display
6+
* @param {number} [options.maxCols=Infinity] - Maximum number of columns to display
7+
* @param {boolean} [options.showIndex=true] - Whether to show row indices
8+
* @returns {string} Formatted table string
9+
*/
10+
function formatTable(frame, options = {}) {
11+
const { maxRows = 10, maxCols = Infinity, showIndex = true } = options;
12+
13+
// Convert frame to array of objects for easier processing
14+
const columns = Object.keys(frame.columns);
15+
const rowCount = frame.rowCount;
16+
const columnNames = columns.slice(0, maxCols);
17+
18+
// Create data array with limited rows
19+
const data = [];
20+
for (let i = 0; i < Math.min(rowCount, maxRows); i++) {
21+
const row = {};
22+
for (const name of columnNames) {
23+
row[name] = frame.columns[name][i];
24+
}
25+
data.push(row);
26+
}
27+
28+
// Calculate column widths
29+
const colWidths = {};
30+
columnNames.forEach((col) => {
31+
colWidths[col] = Math.max(
32+
String(col).length,
33+
...data.map((row) => String(row[col] ?? '').length),
34+
);
35+
});
36+
37+
if (showIndex) {
38+
const indexWidth = String(data.length - 1).length;
39+
colWidths['index'] = Math.max(indexWidth, 5); // Minimum width for index column
40+
}
41+
42+
// Build header
43+
let result = '';
44+
if (showIndex) {
45+
result += ' '.repeat(colWidths['index'] + 1) + '| ';
46+
}
47+
48+
columnNames.forEach((col) => {
49+
result += String(col).padEnd(colWidths[col] + 2);
50+
});
51+
result += '\n';
52+
53+
// Add separator
54+
if (showIndex) {
55+
result += '-'.repeat(colWidths['index'] + 1) + '+ ';
56+
}
57+
58+
columnNames.forEach((col) => {
59+
result += '-'.repeat(colWidths[col] + 2);
60+
});
61+
result += '\n';
62+
63+
// Add data rows
64+
data.forEach((row, i) => {
65+
if (showIndex) {
66+
result += String(i).padStart(colWidths['index']) + ' | ';
67+
}
68+
69+
columnNames.forEach((col) => {
70+
result += String(row[col] ?? '').padEnd(colWidths[col] + 2);
71+
});
72+
result += '\n';
73+
});
74+
75+
// Add footer if there are more rows
76+
if (rowCount > maxRows) {
77+
result += `\n... ${rowCount - maxRows} more rows`;
78+
}
79+
80+
// Add footer if there are more columns
81+
if (columns.length > maxCols) {
82+
result += `\n... ${columns.length - maxCols} more columns`;
83+
}
84+
85+
return result;
86+
}
87+
88+
/**
89+
* Prints the DataFrame to the console.
90+
* @param {{ validateColumn(frame, column): void }} deps
91+
* @returns {(frame: TinyFrame, options?: Object) => void}
92+
*/
93+
export const print =
94+
() =>
95+
(frame, options = {}) => {
96+
const formattedTable = formatTable(frame, options);
97+
console.log(formattedTable);
98+
return frame; // Return the frame for method chaining
99+
};

src/methods/raw.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { count } from './aggregation/count.js';
44
export { mean } from './aggregation/mean.js';
55
export { sort } from './aggregation/sort.js';
66
export { first } from './aggregation/first.js';
7+
export { print } from './display/print.js';

test/methods/display/print.test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { DataFrame } from '../../../src/core/DataFrame.js';
3+
import { print } from '../../../src/methods/display/print.js';
4+
5+
describe('DataFrame print method', () => {
6+
// Create test data frame
7+
const testData = [
8+
{ name: 'Alice', age: 25, city: 'New York' },
9+
{ name: 'Bob', age: 30, city: 'Boston' },
10+
{ name: 'Charlie', age: 35, city: 'Chicago' },
11+
{ name: 'David', age: 40, city: 'Denver' },
12+
{ name: 'Eve', age: 45, city: 'El Paso' },
13+
];
14+
15+
const df = DataFrame.create(testData);
16+
17+
it('should format data as a table string', () => {
18+
// Mock console.log to check output
19+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
20+
21+
// Call print function directly
22+
const printFn = print();
23+
printFn(df._frame);
24+
25+
// Check that console.log was called
26+
expect(consoleSpy).toHaveBeenCalled();
27+
28+
// Get the argument passed to console.log
29+
const output = consoleSpy.mock.calls[0][0];
30+
31+
// Check that the output contains column headers
32+
expect(output).toContain('name');
33+
expect(output).toContain('age');
34+
expect(output).toContain('city');
35+
36+
// Check that the output contains data
37+
expect(output).toContain('Alice');
38+
expect(output).toContain('25');
39+
expect(output).toContain('New York');
40+
41+
// Restore console.log
42+
consoleSpy.mockRestore();
43+
});
44+
45+
it('should return the frame for method chaining', () => {
46+
// Mock console.log
47+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
48+
49+
// Call print function directly
50+
const printFn = print();
51+
const result = printFn(df._frame);
52+
53+
// Check that the function returns the frame
54+
expect(result).toBe(df._frame);
55+
56+
// Restore console.log
57+
consoleSpy.mockRestore();
58+
});
59+
60+
it('should respect maxRows option', () => {
61+
// Create a frame with many rows
62+
const largeData = Array.from({ length: 20 }, (_, i) => ({
63+
id: i,
64+
value: i * 10,
65+
}));
66+
67+
const largeDf = DataFrame.create(largeData);
68+
69+
// Mock console.log
70+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
71+
72+
// Call print function with row limit
73+
const printFn = print();
74+
printFn(largeDf._frame, { maxRows: 5 });
75+
76+
// Get the output
77+
const output = consoleSpy.mock.calls[0][0];
78+
79+
// Check that the output contains message about additional rows
80+
expect(output).toContain('more rows');
81+
82+
// Restore console.log
83+
consoleSpy.mockRestore();
84+
});
85+
86+
it('should respect maxCols option', () => {
87+
// Create a frame with many columns
88+
const wideData = [{ col1: 1, col2: 2, col3: 3, col4: 4, col5: 5, col6: 6 }];
89+
90+
const wideDf = DataFrame.create(wideData);
91+
92+
// Mock console.log
93+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
94+
95+
// Call print function with column limit
96+
const printFn = print();
97+
printFn(wideDf._frame, { maxCols: 3 });
98+
99+
// Get the output
100+
const output = consoleSpy.mock.calls[0][0];
101+
102+
// Check that the output contains message about additional columns
103+
expect(output).toContain('more columns');
104+
105+
// Restore console.log
106+
consoleSpy.mockRestore();
107+
});
108+
});

tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"checkJs": false,
5+
"target": "ES2020",
6+
"module": "ESNext",
7+
"moduleResolution": "Node",
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"noEmit": true
11+
},
12+
"include": ["src/**/*"]
13+
}

0 commit comments

Comments
 (0)