jupyterlab_tabular_data_viewer_extension 1.1.20 → 1.2.8

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
@@ -10,6 +10,10 @@ View and browse Parquet, Excel, CSV, and TSV files directly in JupyterLab. Doubl
10
10
 
11
11
  ![Parquet Viewer](.resources/screenshot.png)
12
12
 
13
+ **Opening files:** Right-click any supported file and select "Tabular Data Viewer" from the "Open With" menu, or simply double-click to open with the default viewer.
14
+
15
+ ![Open With Menu](.resources/screenshot-menu.png)
16
+
13
17
  ## Features
14
18
 
15
19
  **Supported File Formats:**
@@ -36,6 +40,12 @@ View and browse Parquet, Excel, CSV, and TSV files directly in JupyterLab. Doubl
36
40
  - Multiple filters work together to narrow down results
37
41
 
38
42
  **Additional features:**
43
+ - Column statistics modal - Hover over any column header to reveal an info icon, click it to view comprehensive statistics including data type, row counts, null values, unique counts, and type-specific metrics (numeric: min/max/mean/median/std dev/outliers using IQR×1.5; string: most common value/length stats; date: earliest/latest dates). Copy statistics as JSON with one click
44
+
45
+ ![Column Statistics Icon](.resources/screenshot-stats-icon.png)
46
+
47
+ ![Column Statistics Modal](.resources/screenshot-stats.png)
48
+
39
49
  - Right-click context menu on rows to copy data as JSON
40
50
  - Configurable file type support via Settings - Enable/disable Parquet, Excel, or CSV/TSV handling
41
51
  - All features work seamlessly across all supported file formats
@@ -58,10 +68,12 @@ pip uninstall jupyterlab_tabular_data_viewer_extension
58
68
  Configure file type support through JupyterLab Settings:
59
69
 
60
70
  1. Open **Settings → Settings Editor**
61
- 2. Search for "Parquet Viewer Extension"
71
+ 2. Search for "Tabular Data Viewer Extension"
62
72
  3. Configure options:
63
73
  - **Enable Parquet files** - Default: enabled
64
- - **Enable Excel files** - Default: disabled (enable to view .xlsx files)
74
+ - **Enable Excel files** - Default: enabled
75
+ - **Enable CSV files** - Default: enabled
76
+ - **Enable TSV files** - Default: enabled
65
77
 
66
78
  When a file type is disabled, files open with JupyterLab's default handler instead.
67
79
 
package/lib/index.js CHANGED
@@ -34,9 +34,7 @@ const plugin = {
34
34
  autoStart: true,
35
35
  requires: [ISettingRegistry],
36
36
  activate: async (app, settingRegistry) => {
37
- // console.log(
38
- // 'JupyterLab extension jupyterlab_tabular_data_viewer_extension is activated!'
39
- // );
37
+ console.log('JupyterLab extension jupyterlab_tabular_data_viewer_extension is activated!');
40
38
  const { docRegistry, commands, contextMenu } = app;
41
39
  // Track last right-clicked row for context menu
42
40
  let lastContextMenuRow = null;
package/lib/modal.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Widget } from '@lumino/widgets';
2
+ import { IColumnStats } from './request';
3
+ /**
4
+ * Modal dialog widget for displaying column statistics
5
+ */
6
+ export declare class ColumnStatsModal extends Widget {
7
+ private _stats;
8
+ constructor(stats: IColumnStats);
9
+ /**
10
+ * Render the modal content
11
+ */
12
+ private _render;
13
+ /**
14
+ * Format number with thousands separator
15
+ */
16
+ private _formatNumber;
17
+ /**
18
+ * Copy statistics as JSON to clipboard
19
+ */
20
+ private _copyStatsAsJson;
21
+ /**
22
+ * Setup event listeners for closing the modal
23
+ */
24
+ private _setupEventListeners;
25
+ /**
26
+ * Show the modal
27
+ */
28
+ show(): void;
29
+ /**
30
+ * Close and dispose the modal
31
+ */
32
+ close(): void;
33
+ }
package/lib/modal.js ADDED
@@ -0,0 +1,201 @@
1
+ import { Widget } from '@lumino/widgets';
2
+ /**
3
+ * Modal dialog widget for displaying column statistics
4
+ */
5
+ export class ColumnStatsModal extends Widget {
6
+ constructor(stats) {
7
+ super();
8
+ this._stats = stats;
9
+ this.addClass('jp-ColumnStatsModal');
10
+ this._render();
11
+ this._setupEventListeners();
12
+ }
13
+ /**
14
+ * Render the modal content
15
+ */
16
+ _render() {
17
+ const content = document.createElement('div');
18
+ content.className = 'jp-ColumnStatsModal-content';
19
+ // Header with column name and close button
20
+ const header = document.createElement('div');
21
+ header.className = 'jp-ColumnStatsModal-header';
22
+ const title = document.createElement('h3');
23
+ title.textContent = `Column: ${this._stats.column_name}`;
24
+ const closeBtn = document.createElement('button');
25
+ closeBtn.className = 'jp-ColumnStatsModal-close';
26
+ closeBtn.textContent = '×';
27
+ closeBtn.onclick = () => this.close();
28
+ header.appendChild(title);
29
+ header.appendChild(closeBtn);
30
+ content.appendChild(header);
31
+ // Copy JSON button
32
+ const copySection = document.createElement('div');
33
+ copySection.className = 'jp-ColumnStatsModal-copy';
34
+ const copyBtn = document.createElement('button');
35
+ copyBtn.className = 'jp-ColumnStatsModal-copyButton';
36
+ copyBtn.textContent = 'Copy Stats as JSON';
37
+ copyBtn.onclick = () => this._copyStatsAsJson();
38
+ copySection.appendChild(copyBtn);
39
+ content.appendChild(copySection);
40
+ // Data type
41
+ const typeDiv = document.createElement('div');
42
+ typeDiv.className = 'jp-ColumnStatsModal-type';
43
+ typeDiv.textContent = `Type: ${this._stats.data_type}`;
44
+ content.appendChild(typeDiv);
45
+ // Data Summary section
46
+ const summarySection = document.createElement('div');
47
+ summarySection.className = 'jp-ColumnStatsModal-section';
48
+ const summaryTitle = document.createElement('h4');
49
+ summaryTitle.textContent = 'Data Summary';
50
+ summarySection.appendChild(summaryTitle);
51
+ const summaryList = document.createElement('ul');
52
+ summaryList.innerHTML = `
53
+ <li>Total rows: ${this._formatNumber(this._stats.total_rows)}</li>
54
+ <li>Non-null: ${this._formatNumber(this._stats.non_null_count)} (${this._stats.non_null_percentage}%)</li>
55
+ <li>Null: ${this._formatNumber(this._stats.null_count)} (${this._stats.null_percentage}%)</li>
56
+ <li>Unique values: ${this._formatNumber(this._stats.unique_count)} (${this._stats.unique_percentage}%)</li>
57
+ `;
58
+ summarySection.appendChild(summaryList);
59
+ content.appendChild(summarySection);
60
+ // Numeric statistics
61
+ if (this._stats.data_type === 'int' || this._stats.data_type === 'float') {
62
+ const numericSection = document.createElement('div');
63
+ numericSection.className = 'jp-ColumnStatsModal-section';
64
+ const numericTitle = document.createElement('h4');
65
+ numericTitle.textContent = 'Numeric Statistics';
66
+ numericSection.appendChild(numericTitle);
67
+ const numericList = document.createElement('ul');
68
+ const items = [];
69
+ if (this._stats.min_value !== undefined) {
70
+ items.push(`Min: ${this._formatNumber(this._stats.min_value)}`);
71
+ }
72
+ if (this._stats.max_value !== undefined) {
73
+ items.push(`Max: ${this._formatNumber(this._stats.max_value)}`);
74
+ }
75
+ if (this._stats.mean !== undefined) {
76
+ items.push(`Mean: ${this._formatNumber(this._stats.mean)}`);
77
+ }
78
+ if (this._stats.median !== undefined) {
79
+ items.push(`Median: ${this._formatNumber(this._stats.median)}`);
80
+ }
81
+ if (this._stats.std_dev !== undefined) {
82
+ items.push(`Std Dev: ${this._formatNumber(this._stats.std_dev)}`);
83
+ }
84
+ if (this._stats.outlier_count !== undefined) {
85
+ items.push(`Outliers (IQR×1.5): ${this._formatNumber(this._stats.outlier_count)} (${this._stats.outlier_percentage}%)`);
86
+ }
87
+ numericList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
88
+ numericSection.appendChild(numericList);
89
+ content.appendChild(numericSection);
90
+ }
91
+ // String statistics
92
+ if (this._stats.data_type === 'string') {
93
+ const stringSection = document.createElement('div');
94
+ stringSection.className = 'jp-ColumnStatsModal-section';
95
+ const stringTitle = document.createElement('h4');
96
+ stringTitle.textContent = 'String Statistics';
97
+ stringSection.appendChild(stringTitle);
98
+ const stringList = document.createElement('ul');
99
+ const items = [];
100
+ if (this._stats.most_common_value !== undefined) {
101
+ items.push(`Most common: "${this._stats.most_common_value}" (${this._stats.most_common_count})`);
102
+ }
103
+ if (this._stats.min_length !== undefined) {
104
+ items.push(`Min length: ${this._stats.min_length} characters`);
105
+ }
106
+ if (this._stats.max_length !== undefined) {
107
+ items.push(`Max length: ${this._stats.max_length} characters`);
108
+ }
109
+ if (this._stats.avg_length !== undefined) {
110
+ items.push(`Avg length: ${this._formatNumber(this._stats.avg_length)} characters`);
111
+ }
112
+ stringList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
113
+ stringSection.appendChild(stringList);
114
+ content.appendChild(stringSection);
115
+ }
116
+ // Date/datetime statistics
117
+ if (this._stats.data_type === 'date' || this._stats.data_type === 'datetime') {
118
+ const dateSection = document.createElement('div');
119
+ dateSection.className = 'jp-ColumnStatsModal-section';
120
+ const dateTitle = document.createElement('h4');
121
+ dateTitle.textContent = 'Date Range';
122
+ dateSection.appendChild(dateTitle);
123
+ const dateList = document.createElement('ul');
124
+ const items = [];
125
+ if (this._stats.earliest_date) {
126
+ items.push(`Earliest: ${this._stats.earliest_date}`);
127
+ }
128
+ if (this._stats.latest_date) {
129
+ items.push(`Latest: ${this._stats.latest_date}`);
130
+ }
131
+ if (this._stats.date_range_days !== undefined) {
132
+ items.push(`Span: ${this._formatNumber(this._stats.date_range_days)} days`);
133
+ }
134
+ dateList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
135
+ dateSection.appendChild(dateList);
136
+ content.appendChild(dateSection);
137
+ }
138
+ this.node.appendChild(content);
139
+ }
140
+ /**
141
+ * Format number with thousands separator
142
+ */
143
+ _formatNumber(num) {
144
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
145
+ }
146
+ /**
147
+ * Copy statistics as JSON to clipboard
148
+ */
149
+ async _copyStatsAsJson() {
150
+ try {
151
+ const json = JSON.stringify(this._stats, null, 2);
152
+ await navigator.clipboard.writeText(json);
153
+ // Provide visual feedback
154
+ const btn = this.node.querySelector('.jp-ColumnStatsModal-copyButton');
155
+ if (btn) {
156
+ const originalText = btn.textContent;
157
+ btn.textContent = 'Copied!';
158
+ setTimeout(() => {
159
+ btn.textContent = originalText;
160
+ }, 2000);
161
+ }
162
+ }
163
+ catch (error) {
164
+ console.error('Failed to copy stats to clipboard:', error);
165
+ }
166
+ }
167
+ /**
168
+ * Setup event listeners for closing the modal
169
+ */
170
+ _setupEventListeners() {
171
+ // Close on ESC key
172
+ const handleKeydown = (event) => {
173
+ if (event.key === 'Escape') {
174
+ this.close();
175
+ }
176
+ };
177
+ document.addEventListener('keydown', handleKeydown);
178
+ this.disposed.connect(() => {
179
+ document.removeEventListener('keydown', handleKeydown);
180
+ });
181
+ // Close on backdrop click
182
+ this.node.addEventListener('click', (event) => {
183
+ if (event.target === this.node) {
184
+ this.close();
185
+ }
186
+ });
187
+ }
188
+ /**
189
+ * Show the modal
190
+ */
191
+ show() {
192
+ Widget.attach(this, document.body);
193
+ this.node.focus();
194
+ }
195
+ /**
196
+ * Close and dispose the modal
197
+ */
198
+ close() {
199
+ this.dispose();
200
+ }
201
+ }
package/lib/request.d.ts CHANGED
@@ -6,3 +6,42 @@
6
6
  * @returns The response body interpreted as JSON
7
7
  */
8
8
  export declare function requestAPI<T>(endPoint?: string, init?: RequestInit): Promise<T>;
9
+ /**
10
+ * Column statistics interface
11
+ */
12
+ export interface IColumnStats {
13
+ column_name: string;
14
+ data_type: string;
15
+ total_rows: number;
16
+ non_null_count: number;
17
+ non_null_percentage: number;
18
+ null_count: number;
19
+ null_percentage: number;
20
+ unique_count: number;
21
+ unique_percentage: number;
22
+ min_value?: number;
23
+ max_value?: number;
24
+ mean?: number;
25
+ median?: number;
26
+ std_dev?: number;
27
+ outlier_count?: number;
28
+ outlier_percentage?: number;
29
+ outlier_lower_bound?: number;
30
+ outlier_upper_bound?: number;
31
+ most_common_value?: string;
32
+ most_common_count?: number;
33
+ min_length?: number;
34
+ max_length?: number;
35
+ avg_length?: number;
36
+ earliest_date?: string;
37
+ latest_date?: string;
38
+ date_range_days?: number;
39
+ }
40
+ /**
41
+ * Fetch column statistics from the backend
42
+ *
43
+ * @param filePath Path to the data file
44
+ * @param columnName Name of column to analyze
45
+ * @returns Column statistics
46
+ */
47
+ export declare function fetchColumnStats(filePath: string, columnName: string): Promise<IColumnStats>;
package/lib/request.js CHANGED
@@ -33,3 +33,19 @@ export async function requestAPI(endPoint = '', init = {}) {
33
33
  }
34
34
  return data;
35
35
  }
36
+ /**
37
+ * Fetch column statistics from the backend
38
+ *
39
+ * @param filePath Path to the data file
40
+ * @param columnName Name of column to analyze
41
+ * @returns Column statistics
42
+ */
43
+ export async function fetchColumnStats(filePath, columnName) {
44
+ return requestAPI('column-stats', {
45
+ method: 'POST',
46
+ body: JSON.stringify({
47
+ path: filePath,
48
+ columnName: columnName
49
+ })
50
+ });
51
+ }
package/lib/widget.d.ts CHANGED
@@ -100,6 +100,10 @@ export declare class TabularDataViewer extends Widget {
100
100
  * Toggle sort on a column
101
101
  */
102
102
  private _toggleSort;
103
+ /**
104
+ * Show column statistics modal
105
+ */
106
+ private _showColumnStats;
103
107
  /**
104
108
  * Update sort indicators in column headers
105
109
  */
package/lib/widget.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Widget } from '@lumino/widgets';
2
- import { requestAPI } from './request';
2
+ import { requestAPI, fetchColumnStats } from './request';
3
+ import { ColumnStatsModal } from './modal';
3
4
  /**
4
5
  * Parquet viewer widget
5
6
  */
@@ -307,6 +308,17 @@ export class TabularDataViewer extends Widget {
307
308
  const nameSpan = document.createElement('div');
308
309
  nameSpan.className = 'jp-TabularDataViewer-columnName';
309
310
  nameSpan.textContent = col.name;
311
+ // Add info icon for column statistics
312
+ const infoIcon = document.createElement('span');
313
+ infoIcon.className = 'jp-TabularDataViewer-infoIcon';
314
+ infoIcon.textContent = '🛈';
315
+ infoIcon.title = 'Show column statistics';
316
+ infoIcon.addEventListener('click', async (e) => {
317
+ e.preventDefault();
318
+ e.stopPropagation(); // Prevent sort from triggering
319
+ await this._showColumnStats(col.name);
320
+ });
321
+ nameSpan.appendChild(infoIcon);
310
322
  const typeSpan = document.createElement('div');
311
323
  typeSpan.className = 'jp-TabularDataViewer-columnType';
312
324
  typeSpan.textContent = this._simplifyType(col.type);
@@ -536,6 +548,22 @@ export class TabularDataViewer extends Widget {
536
548
  this._updateSortIndicators();
537
549
  this._loadData(true);
538
550
  }
551
+ /**
552
+ * Show column statistics modal
553
+ */
554
+ async _showColumnStats(columnName) {
555
+ try {
556
+ // Show loading indicator (we could add a spinner here)
557
+ const stats = await fetchColumnStats(this._filePath, columnName);
558
+ const modal = new ColumnStatsModal(stats);
559
+ modal.show();
560
+ }
561
+ catch (error) {
562
+ console.error('Failed to load column statistics:', error);
563
+ // Could show an error message to user
564
+ alert(`Failed to load statistics for column "${columnName}": ${error}`);
565
+ }
566
+ }
539
567
  /**
540
568
  * Update sort indicators in column headers
541
569
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_tabular_data_viewer_extension",
3
- "version": "1.1.20",
3
+ "version": "1.2.8",
4
4
  "description": "Jupyterlab extension to browse tabular data files (Parquet, Excel, CSV, TSV) with filtering and sorting capabilities",
5
5
  "keywords": [
6
6
  "jupyter",
package/src/index.ts CHANGED
@@ -73,9 +73,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
73
73
  autoStart: true,
74
74
  requires: [ISettingRegistry],
75
75
  activate: async (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
76
- // console.log(
77
- // 'JupyterLab extension jupyterlab_tabular_data_viewer_extension is activated!'
78
- // );
76
+ console.log(
77
+ 'JupyterLab extension jupyterlab_tabular_data_viewer_extension is activated!'
78
+ );
79
79
 
80
80
  const { docRegistry, commands, contextMenu } = app;
81
81
 
package/src/modal.ts ADDED
@@ -0,0 +1,229 @@
1
+ import { Widget } from '@lumino/widgets';
2
+ import { IColumnStats } from './request';
3
+
4
+ /**
5
+ * Modal dialog widget for displaying column statistics
6
+ */
7
+ export class ColumnStatsModal extends Widget {
8
+ private _stats: IColumnStats;
9
+
10
+ constructor(stats: IColumnStats) {
11
+ super();
12
+ this._stats = stats;
13
+ this.addClass('jp-ColumnStatsModal');
14
+ this._render();
15
+ this._setupEventListeners();
16
+ }
17
+
18
+ /**
19
+ * Render the modal content
20
+ */
21
+ private _render(): void {
22
+ const content = document.createElement('div');
23
+ content.className = 'jp-ColumnStatsModal-content';
24
+
25
+ // Header with column name and close button
26
+ const header = document.createElement('div');
27
+ header.className = 'jp-ColumnStatsModal-header';
28
+ const title = document.createElement('h3');
29
+ title.textContent = `Column: ${this._stats.column_name}`;
30
+ const closeBtn = document.createElement('button');
31
+ closeBtn.className = 'jp-ColumnStatsModal-close';
32
+ closeBtn.textContent = '×';
33
+ closeBtn.onclick = () => this.close();
34
+ header.appendChild(title);
35
+ header.appendChild(closeBtn);
36
+ content.appendChild(header);
37
+
38
+ // Copy JSON button
39
+ const copySection = document.createElement('div');
40
+ copySection.className = 'jp-ColumnStatsModal-copy';
41
+ const copyBtn = document.createElement('button');
42
+ copyBtn.className = 'jp-ColumnStatsModal-copyButton';
43
+ copyBtn.textContent = 'Copy Stats as JSON';
44
+ copyBtn.onclick = () => this._copyStatsAsJson();
45
+ copySection.appendChild(copyBtn);
46
+ content.appendChild(copySection);
47
+
48
+ // Data type
49
+ const typeDiv = document.createElement('div');
50
+ typeDiv.className = 'jp-ColumnStatsModal-type';
51
+ typeDiv.textContent = `Type: ${this._stats.data_type}`;
52
+ content.appendChild(typeDiv);
53
+
54
+ // Data Summary section
55
+ const summarySection = document.createElement('div');
56
+ summarySection.className = 'jp-ColumnStatsModal-section';
57
+ const summaryTitle = document.createElement('h4');
58
+ summaryTitle.textContent = 'Data Summary';
59
+ summarySection.appendChild(summaryTitle);
60
+
61
+ const summaryList = document.createElement('ul');
62
+ summaryList.innerHTML = `
63
+ <li>Total rows: ${this._formatNumber(this._stats.total_rows)}</li>
64
+ <li>Non-null: ${this._formatNumber(this._stats.non_null_count)} (${this._stats.non_null_percentage}%)</li>
65
+ <li>Null: ${this._formatNumber(this._stats.null_count)} (${this._stats.null_percentage}%)</li>
66
+ <li>Unique values: ${this._formatNumber(this._stats.unique_count)} (${this._stats.unique_percentage}%)</li>
67
+ `;
68
+ summarySection.appendChild(summaryList);
69
+ content.appendChild(summarySection);
70
+
71
+ // Numeric statistics
72
+ if (this._stats.data_type === 'int' || this._stats.data_type === 'float') {
73
+ const numericSection = document.createElement('div');
74
+ numericSection.className = 'jp-ColumnStatsModal-section';
75
+ const numericTitle = document.createElement('h4');
76
+ numericTitle.textContent = 'Numeric Statistics';
77
+ numericSection.appendChild(numericTitle);
78
+
79
+ const numericList = document.createElement('ul');
80
+ const items: string[] = [];
81
+
82
+ if (this._stats.min_value !== undefined) {
83
+ items.push(`Min: ${this._formatNumber(this._stats.min_value)}`);
84
+ }
85
+ if (this._stats.max_value !== undefined) {
86
+ items.push(`Max: ${this._formatNumber(this._stats.max_value)}`);
87
+ }
88
+ if (this._stats.mean !== undefined) {
89
+ items.push(`Mean: ${this._formatNumber(this._stats.mean)}`);
90
+ }
91
+ if (this._stats.median !== undefined) {
92
+ items.push(`Median: ${this._formatNumber(this._stats.median)}`);
93
+ }
94
+ if (this._stats.std_dev !== undefined) {
95
+ items.push(`Std Dev: ${this._formatNumber(this._stats.std_dev)}`);
96
+ }
97
+ if (this._stats.outlier_count !== undefined) {
98
+ items.push(`Outliers (IQR×1.5): ${this._formatNumber(this._stats.outlier_count)} (${this._stats.outlier_percentage}%)`);
99
+ }
100
+
101
+ numericList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
102
+ numericSection.appendChild(numericList);
103
+ content.appendChild(numericSection);
104
+ }
105
+
106
+ // String statistics
107
+ if (this._stats.data_type === 'string') {
108
+ const stringSection = document.createElement('div');
109
+ stringSection.className = 'jp-ColumnStatsModal-section';
110
+ const stringTitle = document.createElement('h4');
111
+ stringTitle.textContent = 'String Statistics';
112
+ stringSection.appendChild(stringTitle);
113
+
114
+ const stringList = document.createElement('ul');
115
+ const items: string[] = [];
116
+
117
+ if (this._stats.most_common_value !== undefined) {
118
+ items.push(`Most common: "${this._stats.most_common_value}" (${this._stats.most_common_count})`);
119
+ }
120
+ if (this._stats.min_length !== undefined) {
121
+ items.push(`Min length: ${this._stats.min_length} characters`);
122
+ }
123
+ if (this._stats.max_length !== undefined) {
124
+ items.push(`Max length: ${this._stats.max_length} characters`);
125
+ }
126
+ if (this._stats.avg_length !== undefined) {
127
+ items.push(`Avg length: ${this._formatNumber(this._stats.avg_length)} characters`);
128
+ }
129
+
130
+ stringList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
131
+ stringSection.appendChild(stringList);
132
+ content.appendChild(stringSection);
133
+ }
134
+
135
+ // Date/datetime statistics
136
+ if (this._stats.data_type === 'date' || this._stats.data_type === 'datetime') {
137
+ const dateSection = document.createElement('div');
138
+ dateSection.className = 'jp-ColumnStatsModal-section';
139
+ const dateTitle = document.createElement('h4');
140
+ dateTitle.textContent = 'Date Range';
141
+ dateSection.appendChild(dateTitle);
142
+
143
+ const dateList = document.createElement('ul');
144
+ const items: string[] = [];
145
+
146
+ if (this._stats.earliest_date) {
147
+ items.push(`Earliest: ${this._stats.earliest_date}`);
148
+ }
149
+ if (this._stats.latest_date) {
150
+ items.push(`Latest: ${this._stats.latest_date}`);
151
+ }
152
+ if (this._stats.date_range_days !== undefined) {
153
+ items.push(`Span: ${this._formatNumber(this._stats.date_range_days)} days`);
154
+ }
155
+
156
+ dateList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
157
+ dateSection.appendChild(dateList);
158
+ content.appendChild(dateSection);
159
+ }
160
+
161
+ this.node.appendChild(content);
162
+ }
163
+
164
+ /**
165
+ * Format number with thousands separator
166
+ */
167
+ private _formatNumber(num: number): string {
168
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
169
+ }
170
+
171
+ /**
172
+ * Copy statistics as JSON to clipboard
173
+ */
174
+ private async _copyStatsAsJson(): Promise<void> {
175
+ try {
176
+ const json = JSON.stringify(this._stats, null, 2);
177
+ await navigator.clipboard.writeText(json);
178
+ // Provide visual feedback
179
+ const btn = this.node.querySelector('.jp-ColumnStatsModal-copyButton');
180
+ if (btn) {
181
+ const originalText = btn.textContent;
182
+ btn.textContent = 'Copied!';
183
+ setTimeout(() => {
184
+ btn.textContent = originalText;
185
+ }, 2000);
186
+ }
187
+ } catch (error) {
188
+ console.error('Failed to copy stats to clipboard:', error);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Setup event listeners for closing the modal
194
+ */
195
+ private _setupEventListeners(): void {
196
+ // Close on ESC key
197
+ const handleKeydown = (event: KeyboardEvent) => {
198
+ if (event.key === 'Escape') {
199
+ this.close();
200
+ }
201
+ };
202
+ document.addEventListener('keydown', handleKeydown);
203
+ this.disposed.connect(() => {
204
+ document.removeEventListener('keydown', handleKeydown);
205
+ });
206
+
207
+ // Close on backdrop click
208
+ this.node.addEventListener('click', (event: MouseEvent) => {
209
+ if (event.target === this.node) {
210
+ this.close();
211
+ }
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Show the modal
217
+ */
218
+ show(): void {
219
+ Widget.attach(this, document.body);
220
+ this.node.focus();
221
+ }
222
+
223
+ /**
224
+ * Close and dispose the modal
225
+ */
226
+ close(): void {
227
+ this.dispose();
228
+ }
229
+ }
package/src/request.ts CHANGED
@@ -44,3 +44,58 @@ export async function requestAPI<T>(
44
44
 
45
45
  return data;
46
46
  }
47
+
48
+ /**
49
+ * Column statistics interface
50
+ */
51
+ export interface IColumnStats {
52
+ column_name: string;
53
+ data_type: string;
54
+ total_rows: number;
55
+ non_null_count: number;
56
+ non_null_percentage: number;
57
+ null_count: number;
58
+ null_percentage: number;
59
+ unique_count: number;
60
+ unique_percentage: number;
61
+ // Numeric stats
62
+ min_value?: number;
63
+ max_value?: number;
64
+ mean?: number;
65
+ median?: number;
66
+ std_dev?: number;
67
+ outlier_count?: number;
68
+ outlier_percentage?: number;
69
+ outlier_lower_bound?: number;
70
+ outlier_upper_bound?: number;
71
+ // String stats
72
+ most_common_value?: string;
73
+ most_common_count?: number;
74
+ min_length?: number;
75
+ max_length?: number;
76
+ avg_length?: number;
77
+ // Date stats
78
+ earliest_date?: string;
79
+ latest_date?: string;
80
+ date_range_days?: number;
81
+ }
82
+
83
+ /**
84
+ * Fetch column statistics from the backend
85
+ *
86
+ * @param filePath Path to the data file
87
+ * @param columnName Name of column to analyze
88
+ * @returns Column statistics
89
+ */
90
+ export async function fetchColumnStats(
91
+ filePath: string,
92
+ columnName: string
93
+ ): Promise<IColumnStats> {
94
+ return requestAPI<IColumnStats>('column-stats', {
95
+ method: 'POST',
96
+ body: JSON.stringify({
97
+ path: filePath,
98
+ columnName: columnName
99
+ })
100
+ });
101
+ }
package/src/widget.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Widget } from '@lumino/widgets';
2
- import { requestAPI } from './request';
2
+ import { requestAPI, fetchColumnStats } from './request';
3
+ import { ColumnStatsModal } from './modal';
3
4
 
4
5
  /**
5
6
  * Column metadata interface
@@ -346,6 +347,18 @@ export class TabularDataViewer extends Widget {
346
347
  nameSpan.className = 'jp-TabularDataViewer-columnName';
347
348
  nameSpan.textContent = col.name;
348
349
 
350
+ // Add info icon for column statistics
351
+ const infoIcon = document.createElement('span');
352
+ infoIcon.className = 'jp-TabularDataViewer-infoIcon';
353
+ infoIcon.textContent = '🛈';
354
+ infoIcon.title = 'Show column statistics';
355
+ infoIcon.addEventListener('click', async (e: MouseEvent) => {
356
+ e.preventDefault();
357
+ e.stopPropagation(); // Prevent sort from triggering
358
+ await this._showColumnStats(col.name);
359
+ });
360
+ nameSpan.appendChild(infoIcon);
361
+
349
362
  const typeSpan = document.createElement('div');
350
363
  typeSpan.className = 'jp-TabularDataViewer-columnType';
351
364
  typeSpan.textContent = this._simplifyType(col.type);
@@ -666,6 +679,22 @@ export class TabularDataViewer extends Widget {
666
679
  this._loadData(true);
667
680
  }
668
681
 
682
+ /**
683
+ * Show column statistics modal
684
+ */
685
+ private async _showColumnStats(columnName: string): Promise<void> {
686
+ try {
687
+ // Show loading indicator (we could add a spinner here)
688
+ const stats = await fetchColumnStats(this._filePath, columnName);
689
+ const modal = new ColumnStatsModal(stats);
690
+ modal.show();
691
+ } catch (error) {
692
+ console.error('Failed to load column statistics:', error);
693
+ // Could show an error message to user
694
+ alert(`Failed to load statistics for column "${columnName}": ${error}`);
695
+ }
696
+ }
697
+
669
698
  /**
670
699
  * Update sort indicators in column headers
671
700
  */
package/style/base.css CHANGED
@@ -19,7 +19,8 @@
19
19
  }
20
20
 
21
21
  .jp-TabularDataViewer-table {
22
- border-collapse: collapse;
22
+ border-collapse: separate;
23
+ border-spacing: 0;
23
24
  table-layout: fixed;
24
25
  font-family: var(--jp-code-font-family);
25
26
  font-size: var(--jp-code-font-size);
@@ -44,8 +45,10 @@
44
45
  .jp-TabularDataViewer-filterCell {
45
46
  padding: 4px 8px;
46
47
  border-bottom: 1px solid var(--jp-border-color1);
47
- border-right: 1px solid var(--jp-border-color2);
48
+ border-right: 1px solid var(--jp-border-color0);
48
49
  box-sizing: border-box;
50
+ background-color: var(--jp-layout-color2);
51
+ position: relative;
49
52
  }
50
53
 
51
54
  .jp-TabularDataViewer-filterCell:last-child {
@@ -57,7 +60,7 @@
57
60
  padding: 4px 6px;
58
61
  border: 1px solid var(--jp-border-color2);
59
62
  border-radius: 2px;
60
- background-color: var(--jp-input-background);
63
+ background-color: var(--jp-layout-color0);
61
64
  color: var(--jp-ui-font-color1);
62
65
  font-family: var(--jp-ui-font-family);
63
66
  font-size: var(--jp-ui-font-size1);
@@ -85,7 +88,7 @@
85
88
  text-align: left;
86
89
  font-weight: 600;
87
90
  border-bottom: 2px solid var(--jp-border-color1);
88
- border-right: 1px solid var(--jp-border-color2);
91
+ border-right: 1px solid var(--jp-border-color0);
89
92
  background-color: var(--jp-layout-color2);
90
93
  position: relative;
91
94
  min-width: 120px;
@@ -156,6 +159,7 @@
156
159
  .jp-TabularDataViewer-cell {
157
160
  padding: 6px 12px;
158
161
  border-right: 1px solid var(--jp-border-color2);
162
+ border-bottom: 1px solid var(--jp-border-color2);
159
163
  color: var(--jp-ui-font-color1);
160
164
  word-break: break-word;
161
165
  max-width: 400px;
@@ -245,7 +249,7 @@
245
249
  }
246
250
 
247
251
  [data-jp-theme-light='false'] .jp-TabularDataViewer-filterInput {
248
- background-color: var(--jp-input-background);
252
+ background-color: var(--jp-layout-color0);
249
253
  color: var(--jp-ui-font-color0);
250
254
  }
251
255
 
@@ -267,3 +271,147 @@
267
271
  .jp-TabularDataViewer-container::-webkit-scrollbar-thumb:hover {
268
272
  background: var(--jp-border-color1);
269
273
  }
274
+
275
+ /* Column Info Icon */
276
+ .jp-TabularDataViewer-infoIcon {
277
+ margin-left: 6px;
278
+ font-size: 16px;
279
+ color: transparent;
280
+ cursor: pointer;
281
+ transition: color 0.2s ease;
282
+ user-select: none;
283
+ }
284
+
285
+ .jp-TabularDataViewer-headerCell:hover .jp-TabularDataViewer-infoIcon {
286
+ color: var(--jp-brand-color1) !important;
287
+ }
288
+
289
+ .jp-TabularDataViewer-infoIcon:hover {
290
+ color: var(--jp-brand-color1) !important;
291
+ }
292
+
293
+ /* Column Statistics Modal */
294
+ .jp-ColumnStatsModal {
295
+ position: fixed;
296
+ top: 0;
297
+ left: 0;
298
+ width: 100%;
299
+ height: 100%;
300
+ background-color: rgba(0, 0, 0, 0.5);
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ z-index: 10000;
305
+ }
306
+
307
+ .jp-ColumnStatsModal-content {
308
+ background-color: var(--jp-layout-color1);
309
+ border-radius: 8px;
310
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
311
+ max-width: 500px;
312
+ max-height: 80vh;
313
+ overflow-y: auto;
314
+ padding: 20px;
315
+ color: var(--jp-ui-font-color1);
316
+ font-family: var(--jp-ui-font-family);
317
+ }
318
+
319
+ .jp-ColumnStatsModal-header {
320
+ display: flex;
321
+ justify-content: space-between;
322
+ align-items: center;
323
+ margin-bottom: 16px;
324
+ padding-bottom: 12px;
325
+ border-bottom: 2px solid var(--jp-border-color1);
326
+ }
327
+
328
+ .jp-ColumnStatsModal-header h3 {
329
+ margin: 0;
330
+ font-size: 18px;
331
+ font-weight: 600;
332
+ color: var(--jp-ui-font-color1);
333
+ }
334
+
335
+ .jp-ColumnStatsModal-close {
336
+ background: none;
337
+ border: none;
338
+ font-size: 28px;
339
+ line-height: 1;
340
+ cursor: pointer;
341
+ color: var(--jp-ui-font-color2);
342
+ padding: 0;
343
+ width: 32px;
344
+ height: 32px;
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ border-radius: 4px;
349
+ }
350
+
351
+ .jp-ColumnStatsModal-close:hover {
352
+ background-color: var(--jp-layout-color2);
353
+ color: var(--jp-ui-font-color1);
354
+ }
355
+
356
+ .jp-ColumnStatsModal-copy {
357
+ margin-bottom: 16px;
358
+ text-align: center;
359
+ }
360
+
361
+ .jp-ColumnStatsModal-copyButton {
362
+ background-color: var(--jp-brand-color1);
363
+ color: white;
364
+ border: none;
365
+ padding: 8px 16px;
366
+ border-radius: 4px;
367
+ cursor: pointer;
368
+ font-size: 13px;
369
+ font-weight: 500;
370
+ font-family: var(--jp-ui-font-family);
371
+ transition: background-color 0.2s ease;
372
+ }
373
+
374
+ .jp-ColumnStatsModal-copyButton:hover {
375
+ background-color: var(--jp-brand-color2);
376
+ }
377
+
378
+ .jp-ColumnStatsModal-copyButton:active {
379
+ background-color: var(--jp-brand-color3);
380
+ }
381
+
382
+ .jp-ColumnStatsModal-type {
383
+ font-size: 14px;
384
+ color: var(--jp-ui-font-color2);
385
+ margin-bottom: 20px;
386
+ font-style: italic;
387
+ }
388
+
389
+ .jp-ColumnStatsModal-section {
390
+ margin-bottom: 20px;
391
+ }
392
+
393
+ .jp-ColumnStatsModal-section h4 {
394
+ margin: 0 0 8px 0;
395
+ font-size: 15px;
396
+ font-weight: 600;
397
+ color: var(--jp-ui-font-color1);
398
+ }
399
+
400
+ .jp-ColumnStatsModal-section ul {
401
+ list-style: none;
402
+ padding: 0;
403
+ margin: 0;
404
+ }
405
+
406
+ .jp-ColumnStatsModal-section li {
407
+ padding: 4px 0;
408
+ font-size: 14px;
409
+ color: var(--jp-ui-font-color1);
410
+ }
411
+
412
+ .jp-ColumnStatsModal-section li::before {
413
+ content: '• ';
414
+ color: var(--jp-brand-color1);
415
+ font-weight: bold;
416
+ margin-right: 8px;
417
+ }