From 47a11ae3702872fa4e66a1acb4da678d628c320a Mon Sep 17 00:00:00 2001 From: Alex K Date: Thu, 29 May 2025 16:36:10 +0200 Subject: [PATCH 1/3] feat: implement DataFrame display methods Add the following display methods for DataFrame: - print: Format and output DataFrame as a table in console - toHTML: Convert DataFrame to HTML table string - display: Display DataFrame in browser environment - renderTo: Render DataFrame to a specified DOM element - toJupyter: Convert DataFrame to Jupyter notebook representation Implement tests for all display methods to ensure proper functionality and integration with the existing framework. Fix test mocking issues to ensure all tests pass correctly. --- src/methods/dataframe/display/display.js | 21 +++ src/methods/dataframe/display/index.js | 11 ++ src/methods/dataframe/display/print.js | 128 ++++++++++++++++++ src/methods/dataframe/display/renderTo.js | 21 +++ src/methods/dataframe/display/toHTML.js | 17 +++ src/methods/dataframe/display/toJupyter.js | 29 ++++ .../methods/dataframe/display/display.test.js | 70 ++++++++++ test/methods/dataframe/display/print.test.js | 48 ++++--- .../dataframe/display/renderTo.test.js | 77 +++++++++++ test/methods/dataframe/display/toHTML.test.js | 69 ++++++++++ .../dataframe/display/toJupyter.test.js | 100 ++++++++++++++ 11 files changed, 572 insertions(+), 19 deletions(-) create mode 100644 src/methods/dataframe/display/display.js create mode 100644 src/methods/dataframe/display/index.js create mode 100644 src/methods/dataframe/display/print.js create mode 100644 src/methods/dataframe/display/renderTo.js create mode 100644 src/methods/dataframe/display/toHTML.js create mode 100644 src/methods/dataframe/display/toJupyter.js create mode 100644 test/methods/dataframe/display/display.test.js create mode 100644 test/methods/dataframe/display/renderTo.test.js create mode 100644 test/methods/dataframe/display/toHTML.test.js create mode 100644 test/methods/dataframe/display/toJupyter.test.js diff --git a/src/methods/dataframe/display/display.js b/src/methods/dataframe/display/display.js new file mode 100644 index 0000000..2fb26a0 --- /dev/null +++ b/src/methods/dataframe/display/display.js @@ -0,0 +1,21 @@ +/** + * Display DataFrame in browser environment + * @returns {Function} Function that takes a frame and displays it in browser + */ +import { display as webDisplay } from '../../../display/web/html.js'; + +/** + * Factory function that returns a display function for DataFrame + * @returns {Function} Function that takes a frame and displays it in browser + */ +export const display = + () => + (frame, options = {}) => { + // Use the existing display function from display/web/html.js + webDisplay(frame, options); + + // Return the frame for method chaining + return frame; + }; + +export default display; diff --git a/src/methods/dataframe/display/index.js b/src/methods/dataframe/display/index.js new file mode 100644 index 0000000..3e1827d --- /dev/null +++ b/src/methods/dataframe/display/index.js @@ -0,0 +1,11 @@ +/** + * Index file for DataFrame display methods + */ +export { print } from './print.js'; +export { toHTML } from './toHTML.js'; +export { display } from './display.js'; +export { renderTo } from './renderTo.js'; +export { toJupyter, registerJupyterDisplay } from './toJupyter.js'; + +// Export the register function as default +export { default } from './register.js'; diff --git a/src/methods/dataframe/display/print.js b/src/methods/dataframe/display/print.js new file mode 100644 index 0000000..24cb6de --- /dev/null +++ b/src/methods/dataframe/display/print.js @@ -0,0 +1,128 @@ +/** + * Print DataFrame as a formatted table in the console + * @returns {Function} Function that takes a frame and prints it to console + */ + +/** + * Factory function that returns a print function for DataFrame + * @returns {Function} Function that takes a frame and prints it to console + */ +export const print = + () => + (frame, maxRows = 10, maxCols = Infinity) => { + // Create a formatted table representation + const table = formatDataFrameTable(frame, maxRows, maxCols); + + // Print the table + console.log(table); + + // Return the frame for method chaining + return frame; + }; + +/** + * Format a DataFrame as a table string + * @param {Object} frame - DataFrame object + * @param {number} maxRows - Maximum number of rows to display + * @param {number} maxCols - Maximum number of columns to display + * @returns {string} - Formatted table string + */ +function formatDataFrameTable(frame, maxRows = 10, maxCols = Infinity) { + // Handle case when frame is undefined or doesn't have expected structure + if (!frame || typeof frame !== 'object') { + return 'Invalid DataFrame'; + } + + // Extract columns and data from the DataFrame + const columns = frame._order || []; + const data = {}; + let rowCount = 0; + + if (frame._columns) { + // Extract data from DataFrame's Series objects + rowCount = + columns.length > 0 && frame._columns[columns[0]] + ? frame._columns[columns[0]].length + : 0; + + for (const col of columns) { + const series = frame._columns[col]; + if (series) { + // Handle Series objects which may have vector property + if (series.vector && series.vector._data) { + data[col] = Array.from(series.vector._data); + } else if (series.toArray) { + data[col] = series.toArray(); + } else if (Array.isArray(series)) { + data[col] = series; + } else { + data[col] = []; + } + } else { + data[col] = []; + } + } + } + + // If no columns or data found, return empty message + if (columns.length === 0) { + return 'Empty DataFrame'; + } + + // Limit columns if needed + const displayColumns = + maxCols < columns.length ? columns.slice(0, maxCols) : columns; + + // Determine rows to display + const displayRows = Math.min(rowCount, maxRows); + + // Create a table representation + let table = 'DataFrame Table:\n'; + + // Add header + table += displayColumns.join(' | ') + '\n'; + table += + displayColumns.map((col) => '-'.repeat(col.length)).join('-+-') + '\n'; + + // Add data rows + for (let i = 0; i < displayRows; i++) { + const rowValues = displayColumns.map((col) => { + const value = + data[col] && i < data[col].length ? data[col][i] : undefined; + return formatValue(value); + }); + table += rowValues.join(' | ') + '\n'; + } + + // Add message about additional rows if needed + if (rowCount > displayRows) { + table += `... ${rowCount - displayRows} more rows ...\n`; + } + + // Add message about additional columns if needed + if (columns.length > displayColumns.length) { + table += `... and ${columns.length - displayColumns.length} more columns ...\n`; + } + + // Add dimensions + table += `[${rowCount} rows x ${columns.length} columns]`; + + return table; +} + +/** + * Format a value for display in the table + * @param {*} value - The value to format + * @returns {string} - Formatted string representation + */ +function formatValue(value) { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'number' && isNaN(value)) return 'NaN'; + if (typeof value === 'object' && value instanceof Date) { + return value.toISOString(); + } + return String(value); +} + +export default print; diff --git a/src/methods/dataframe/display/renderTo.js b/src/methods/dataframe/display/renderTo.js new file mode 100644 index 0000000..23a9ca8 --- /dev/null +++ b/src/methods/dataframe/display/renderTo.js @@ -0,0 +1,21 @@ +/** + * Render DataFrame to a specified DOM element + * @returns {Function} Function that takes a frame and renders it to a DOM element + */ +import { renderTo as webRenderTo } from '../../../display/web/html.js'; + +/** + * Factory function that returns a renderTo function for DataFrame + * @returns {Function} Function that takes a frame and renders it to a DOM element + */ +export const renderTo = + () => + (frame, element, options = {}) => { + // Use the existing renderTo function from display/web/html.js + webRenderTo(frame, element, options); + + // Return the frame for method chaining + return frame; + }; + +export default renderTo; diff --git a/src/methods/dataframe/display/toHTML.js b/src/methods/dataframe/display/toHTML.js new file mode 100644 index 0000000..6435491 --- /dev/null +++ b/src/methods/dataframe/display/toHTML.js @@ -0,0 +1,17 @@ +/** + * Convert DataFrame to HTML table + * @returns {Function} Function that takes a frame and returns HTML string + */ +import { toHTML as webToHTML } from '../../../display/web/html.js'; + +/** + * Factory function that returns a toHTML function for DataFrame + * @returns {Function} Function that takes a frame and returns HTML string + */ +export const toHTML = + () => + (frame, options = {}) => + // Use the existing toHTML function from display/web/html.js + webToHTML(frame, options); + +export default toHTML; diff --git a/src/methods/dataframe/display/toJupyter.js b/src/methods/dataframe/display/toJupyter.js new file mode 100644 index 0000000..93c8e24 --- /dev/null +++ b/src/methods/dataframe/display/toJupyter.js @@ -0,0 +1,29 @@ +/** + * Convert DataFrame to Jupyter notebook compatible representation + * @returns {Function} Function that takes a frame and returns Jupyter display object + */ +import { + toJupyter as jupyterToJupyter, + registerJupyterDisplay as jupyterRegister, +} from '../../../display/web/jupyter.js'; + +/** + * Factory function that returns a toJupyter function for DataFrame + * @returns {Function} Function that takes a frame and returns Jupyter display object + */ +export const toJupyter = + () => + (frame, options = {}) => + // Use the existing toJupyter function from display/web/jupyter.js + jupyterToJupyter(frame, options); + +/** + * Register special display methods for Jupyter notebook + * @param {Class} DataFrame - DataFrame class to extend + */ +export const registerJupyterDisplay = (DataFrame) => { + // Use the existing registerJupyterDisplay function from display/web/jupyter.js + jupyterRegister(DataFrame); +}; + +export default toJupyter; diff --git a/test/methods/dataframe/display/display.test.js b/test/methods/dataframe/display/display.test.js new file mode 100644 index 0000000..2d51e64 --- /dev/null +++ b/test/methods/dataframe/display/display.test.js @@ -0,0 +1,70 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DataFrame } from '../../../../src/core/dataframe/DataFrame.js'; +import { display } from '../../../../src/methods/dataframe/display/display.js'; + +import { + testWithBothStorageTypes, + createDataFrameWithStorage, +} from '../../../utils/storageTestUtils.js'; + +// Mock the module +vi.mock('../../../../src/display/web/html.js', () => ({ + display: vi.fn(), +})); + +// Import the mocked function after mocking +import { display as mockWebDisplay } from '../../../../src/display/web/html.js'; + +describe('DataFrame display method', () => { + // Reset mock before each test + beforeEach(() => { + mockWebDisplay.mockReset(); + }); + + // Run tests with both storage types + testWithBothStorageTypes((storageType) => { + describe(`with ${storageType} storage`, () => { + // Create test data frame with people data for better readability in tests + const testData = [ + { name: 'Alice', age: 25, city: 'New York' }, + { name: 'Bob', age: 30, city: 'Boston' }, + { name: 'Charlie', age: 35, city: 'Chicago' }, + ]; + + // Create DataFrame with the specified storage type + const df = createDataFrameWithStorage(DataFrame, testData, storageType); + + it('should call the web display function with the frame', () => { + // Call display function directly + const displayFn = display(); + displayFn(df); + + // Check that the web display function was called with the frame + expect(mockWebDisplay).toHaveBeenCalledWith(df, expect.any(Object)); + }); + + it('should return the frame for method chaining', () => { + // Call display function and check the return value + const displayFn = display(); + const result = displayFn(df); + + // Check that the function returns the frame + expect(result).toBe(df); + }); + + it('should pass options to the web display function', () => { + // Call display function with options + const displayFn = display(); + const options = { + maxRows: 5, + maxCols: 2, + className: 'custom-table', + }; + displayFn(df, options); + + // Check that the web display function was called with the options + expect(mockWebDisplay).toHaveBeenCalledWith(df, options); + }); + }); + }); +}); diff --git a/test/methods/dataframe/display/print.test.js b/test/methods/dataframe/display/print.test.js index 1f4372b..7b31ba2 100644 --- a/test/methods/dataframe/display/print.test.js +++ b/test/methods/dataframe/display/print.test.js @@ -8,7 +8,7 @@ import { } from '../../../utils/storageTestUtils.js'; // Test data to be used in all tests -const testData = [ +const sampleData = [ { value: 10, category: 'A', mixed: '20' }, { value: 20, category: 'B', mixed: 30 }, { value: 30, category: 'A', mixed: null }, @@ -20,10 +20,7 @@ describe('DataFrame print method', () => { // Run tests with both storage types testWithBothStorageTypes((storageType) => { describe(`with ${storageType} storage`, () => { - // Create DataFrame with the specified storage type - const df = createDataFrameWithStorage(DataFrame, testData, storageType); - - // Create test data frame + // Create test data frame with people data for better readability in tests const testData = [ { name: 'Alice', age: 25, city: 'New York' }, { name: 'Bob', age: 30, city: 'Boston' }, @@ -32,7 +29,8 @@ describe('DataFrame print method', () => { { name: 'Eve', age: 45, city: 'El Paso' }, ]; - // df created above using createDataFrameWithStorage + // Create DataFrame with the specified storage type + const df = createDataFrameWithStorage(DataFrame, testData, storageType); it('should format data as a table string', () => { // Mock console.log to check output @@ -42,7 +40,7 @@ describe('DataFrame print method', () => { // Call print function directly const printFn = print(); - printFn(df._frame); + printFn(df); // Check that console.log was called expect(consoleSpy).toHaveBeenCalled(); @@ -65,17 +63,17 @@ describe('DataFrame print method', () => { }); it('should return the frame for method chaining', () => { - // Mock console.log + // Mock console.log to prevent output const consoleSpy = vi .spyOn(console, 'log') .mockImplementation(() => {}); - // Call print function directly + // Call print function and check the return value const printFn = print(); - const result = printFn(df._frame); + const result = printFn(df); // Check that the function returns the frame - expect(result).toBe(df._frame); + expect(result).toBe(df); // Restore console.log consoleSpy.mockRestore(); @@ -88,7 +86,11 @@ describe('DataFrame print method', () => { value: i * 10, })); - const largeDf = DataFrame.create(largeData); + const largeDf = createDataFrameWithStorage( + DataFrame, + largeData, + storageType, + ); // Mock console.log const consoleSpy = vi @@ -97,7 +99,7 @@ describe('DataFrame print method', () => { // Call print function with row limit const printFn = print(); - printFn(largeDf._frame, 5); + printFn(largeDf, 5); // Get the output const output = consoleSpy.mock.calls[0][0]; @@ -111,11 +113,19 @@ describe('DataFrame print method', () => { it('should respect cols limit', () => { // Create a frame with many columns - const wideData = [ - { col1: 1, col2: 2, col3: 3, col4: 4, col5: 5, col6: 6 }, - ]; - - const wideDf = DataFrame.create(wideData); + const wideData = { + col1: [1, 2, 3], + col2: [4, 5, 6], + col3: [7, 8, 9], + col4: [10, 11, 12], + col5: [13, 14, 15], + }; + + const wideDf = createDataFrameWithStorage( + DataFrame, + wideData, + storageType, + ); // Mock console.log const consoleSpy = vi @@ -124,7 +134,7 @@ describe('DataFrame print method', () => { // Call print function with column limit const printFn = print(); - printFn(wideDf._frame, undefined, 3); + printFn(wideDf, Infinity, 2); // Get the output const output = consoleSpy.mock.calls[0][0]; diff --git a/test/methods/dataframe/display/renderTo.test.js b/test/methods/dataframe/display/renderTo.test.js new file mode 100644 index 0000000..a467089 --- /dev/null +++ b/test/methods/dataframe/display/renderTo.test.js @@ -0,0 +1,77 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DataFrame } from '../../../../src/core/dataframe/DataFrame.js'; +import { renderTo } from '../../../../src/methods/dataframe/display/renderTo.js'; + +import { + testWithBothStorageTypes, + createDataFrameWithStorage, +} from '../../../utils/storageTestUtils.js'; + +// Mock the module +vi.mock('../../../../src/display/web/html.js', () => ({ + renderTo: vi.fn(), +})); + +// Import the mocked function after mocking +import { renderTo as mockWebRenderTo } from '../../../../src/display/web/html.js'; + +describe('DataFrame renderTo method', () => { + // Reset mock before each test + beforeEach(() => { + mockWebRenderTo.mockReset(); + }); + + // Run tests with both storage types + testWithBothStorageTypes((storageType) => { + describe(`with ${storageType} storage`, () => { + // Create test data frame with people data for better readability in tests + const testData = [ + { name: 'Alice', age: 25, city: 'New York' }, + { name: 'Bob', age: 30, city: 'Boston' }, + { name: 'Charlie', age: 35, city: 'Chicago' }, + ]; + + // Create DataFrame with the specified storage type + const df = createDataFrameWithStorage(DataFrame, testData, storageType); + + // Mock DOM element + const mockElement = { id: 'test-element' }; + + it('should call the web renderTo function with the frame and element', () => { + // Call renderTo function directly + const renderToFn = renderTo(); + renderToFn(df, mockElement); + + // Check that the web renderTo function was called with the frame and element + expect(mockWebRenderTo).toHaveBeenCalledWith( + df, + mockElement, + expect.any(Object), + ); + }); + + it('should return the frame for method chaining', () => { + // Call renderTo function and check the return value + const renderToFn = renderTo(); + const result = renderToFn(df, mockElement); + + // Check that the function returns the frame + expect(result).toBe(df); + }); + + it('should pass options to the web renderTo function', () => { + // Call renderTo function with options + const renderToFn = renderTo(); + const options = { + maxRows: 5, + maxCols: 2, + className: 'custom-table', + }; + renderToFn(df, mockElement, options); + + // Check that the web renderTo function was called with the options + expect(mockWebRenderTo).toHaveBeenCalledWith(df, mockElement, options); + }); + }); + }); +}); diff --git a/test/methods/dataframe/display/toHTML.test.js b/test/methods/dataframe/display/toHTML.test.js new file mode 100644 index 0000000..9ecce53 --- /dev/null +++ b/test/methods/dataframe/display/toHTML.test.js @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from 'vitest'; +import { DataFrame } from '../../../../src/core/dataframe/DataFrame.js'; +import { toHTML } from '../../../../src/methods/dataframe/display/toHTML.js'; + +import { + testWithBothStorageTypes, + createDataFrameWithStorage, +} from '../../../utils/storageTestUtils.js'; + +describe('DataFrame toHTML method', () => { + // Run tests with both storage types + testWithBothStorageTypes((storageType) => { + describe(`with ${storageType} storage`, () => { + // Create test data frame with people data for better readability in tests + const testData = [ + { name: 'Alice', age: 25, city: 'New York' }, + { name: 'Bob', age: 30, city: 'Boston' }, + { name: 'Charlie', age: 35, city: 'Chicago' }, + ]; + + // Create DataFrame with the specified storage type + const df = createDataFrameWithStorage(DataFrame, testData, storageType); + + it('should convert DataFrame to HTML string', () => { + // Call toHTML function directly + const toHTMLFn = toHTML(); + const html = toHTMLFn(df); + + // Check that the result is a string + expect(typeof html).toBe('string'); + + // Check that the HTML contains expected elements + expect(html).toContain(''); + + // Check that the HTML contains style information + expect(html).toContain('