pydatagrid 1.1.1
A powerful data grid library with sorting, filtering, pagination, tree view, and export features
Requires Python: >=3.7

# PyDataGrid

A powerful and flexible Python library for creating interactive data grids with sorting, filtering, pagination, and export capabilities. Perfect for displaying tabular data from Python APIs in a beautiful web interface.

## 📚 Documentation

- **[Quick Start Guide](QUICKSTART.md)** - Get started in 5 minutes
- **[Deployment Guide](DEPLOYMENT.md)** - Docker, PyPI, Angular & Python integration
- **[Examples](examples/)** - Code examples and use cases

## Features

✨ **Core Features:**
- 📊 Display data in a beautiful, responsive table
- 🔄 Sort data by any column (ascending/descending)
- 🔍 Filter data by column values (real-time)
- 📄 Pagination with configurable page size
- ☑️ Row selection (individual, all, cross-page)
- 📥 Export selected or all filtered data to CSV
- 👁️ Show/hide columns dynamically
- 📱 Responsive design for all screen sizes

✨ **Advanced Features:**
- ⚡ High performance with large datasets
- 🎨 Professional UI with hover tooltips
- � Visual filter indicators
- 🌐 REST API for Angular/React integration
- 🐳 Docker-ready
- 📦 PyPI publishable
- 🔧 Easy integration with Flask, Django, FastAPI

## Installation

### From Source

```bash
git clone https://github.com/yourusername/pydatagrid.git
cd pydatagrid
pip install -e .
```

### Using pip (once published)

```bash
pip install pydatagrid
```

## Quick Start

### Basic Usage with Flask

```python
from pydatagrid import create_grid_app

# Your data
data = [
    {'id': 1, 'name': 'John Doe', 'email': 'john@example.com', 'age': 30, 'city': 'New York'},
    {'id': 2, 'name': 'Jane Smith', 'email': 'jane@example.com', 'age': 25, 'city': 'London'},
    {'id': 3, 'name': 'Bob Johnson', 'email': 'bob@example.com', 'age': 35, 'city': 'Paris'},
    # ... more data
]

# Create and run the grid application
app = create_grid_app(data, host='127.0.0.1', port=5000, debug=True)
app.run()
```

Then open your browser and navigate to `http://127.0.0.1:5000/grid/`

### Advanced Usage with Custom Flask App

```python
from flask import Flask
from flask_cors import CORS
from pydatagrid import create_grid_blueprint

app = Flask(__name__)
CORS(app)

# Your data source (can be a function)
def get_data():
    # Fetch data from database, API, or any source
    return [
        {'id': 1, 'name': 'Product A', 'price': 29.99, 'stock': 100},
        {'id': 2, 'name': 'Product B', 'price': 49.99, 'stock': 50},
        # ... more data
    ]

# Create and register the grid blueprint
grid_blueprint = create_grid_blueprint(
    data_source=get_data,
    name='products_grid',
    url_prefix='/products'
)
app.register_blueprint(grid_blueprint)

if __name__ == '__main__':
    app.run(debug=True)
```

Access the grid at `http://127.0.0.1:5000/products/`

### Using DataGrid Class Directly

```python
from pydatagrid import DataGrid

# Create a DataGrid instance
data = [
    {'name': 'Alice', 'score': 95, 'grade': 'A'},
    {'name': 'Bob', 'score': 87, 'grade': 'B'},
    {'name': 'Charlie', 'score': 92, 'grade': 'A'},
]

grid = DataGrid(data)

# Apply sorting
grid.setSorting('score', 'desc')

# Apply filtering
grid.setFilter('grade', 'A')

# Set pagination
grid.setPage(1).setPageSize(50)

# Get processed data
processed_data = grid.getProcessedData()
print(processed_data)

# Export to CSV
csv_data = grid.toCsv()
with open('export.csv', 'w') as f:
    f.write(csv_data)
```

## Integration with Angular

PyDataGrid provides seamless integration for Angular applications. You can use it as a component with property bindings or consume the REST API directly.

### Method 1: Component Integration

This approach uses property bindings and event handlers for a familiar Angular development experience.

#### Step 1: Install and Setup Backend

```python
# server.py
from pydatagrid import create_grid_app
from flask_cors import CORS

# Your data source
data = [
    {'id': 1, 'name': 'John Doe', 'email': 'john@example.com', 'age': 30, 'city': 'New York'},
    {'id': 2, 'name': 'Jane Smith', 'email': 'jane@example.com', 'age': 25, 'city': 'London'},
    {'id': 3, 'name': 'Bob Johnson', 'email': 'bob@example.com', 'age': 35, 'city': 'Paris'},
]

app = create_grid_app(data, host='0.0.0.0', port=5000)
CORS(app)  # Enable CORS for Angular
app.run()
```

#### Step 2: Create PyDataGrid Angular Component

```typescript
// pydatagrid.component.ts
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

export interface ColumnDef {
  field: string;
  headerName?: string;
  sortable?: boolean;
  filter?: boolean;
  width?: number;
}

export interface GridOptions {
  columnDefs: ColumnDef[];
  pagination?: boolean;
  paginationPageSize?: number;
  apiUrl?: string;
  enableSorting?: boolean;
  enableFiltering?: boolean;
  enableExport?: boolean;
}

@Component({
  selector: 'pydatagrid',
  template: `
    <div class="pydatagrid-wrapper">
      <!-- Toolbar -->
      <div class="grid-toolbar" *ngIf="gridOptions.enableExport">
        <button (click)="onExportSelected()" [disabled]="selectedRows.size === 0">
          Export Selected
        </button>
        <button (click)="onExportAll()">Export All</button>
      </div>

      <!-- Grid Table -->
      <div class="grid-container">
        <table class="pydatagrid-table">
          <thead>
            <tr>
              <th class="select-column">
                <input type="checkbox" 
                       (change)="onSelectAll($event.target.checked)"
                       [checked]="isAllSelected()">
              </th>
              <th *ngFor="let col of gridOptions.columnDefs" 
                  [style.width.px]="col.width"
                  [class.sortable]="col.sortable !== false"
                  (click)="col.sortable !== false && onSort(col.field)">
                {{ col.headerName || col.field }}
                <span *ngIf="sortColumn === col.field">
                  {{ sortDirection === 'asc' ? '▲' : '▼' }}
                </span>
              </th>
            </tr>
            <tr *ngIf="gridOptions.enableFiltering !== false">
              <th></th>
              <th *ngFor="let col of gridOptions.columnDefs">
                <input *ngIf="col.filter !== false"
                       type="text" 
                       placeholder="Filter..."
                       (input)="onFilter(col.field, $event.target.value)"
                       class="filter-input">
              </th>
            </tr>
          </thead>
          <tbody>
            <tr *ngFor="let row of rowData; let i = index"
                [class.selected]="selectedRows.has(row.id || i)">
              <td class="select-column">
                <input type="checkbox"
                       [checked]="selectedRows.has(row.id || i)"
                       (change)="onRowSelect(row.id || i, $event.target.checked)">
              </td>
              <td *ngFor="let col of gridOptions.columnDefs">
                {{ row[col.field] }}
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <!-- Pagination -->
      <div class="pagination" *ngIf="gridOptions.pagination">
        <button (click)="onPageChange(1)" [disabled]="currentPage === 1">First</button>
        <button (click)="onPageChange(currentPage - 1)" [disabled]="currentPage === 1">Previous</button>
        <span>Page {{ currentPage }} of {{ totalPages }}</span>
        <button (click)="onPageChange(currentPage + 1)" [disabled]="currentPage === totalPages">Next</button>
        <button (click)="onPageChange(totalPages)" [disabled]="currentPage === totalPages">Last</button>
      </div>
    </div>
  `,
  styles: [`
    .pydatagrid-wrapper {
      width: 100%;
      font-family: Arial, sans-serif;
    }
    .grid-toolbar {
      padding: 10px;
      background: #f5f5f5;
      border: 1px solid #ddd;
      border-bottom: none;
    }
    .grid-toolbar button {
      margin-right: 10px;
      padding: 8px 16px;
      background: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .grid-toolbar button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }
    .grid-container {
      overflow-x: auto;
      border: 1px solid #ddd;
    }
    .pydatagrid-table {
      width: 100%;
      border-collapse: collapse;
    }
    .pydatagrid-table th,
    .pydatagrid-table td {
      padding: 12px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
    .pydatagrid-table th {
      background: #f8f9fa;
      font-weight: 600;
      position: relative;
    }
    .pydatagrid-table th.sortable {
      cursor: pointer;
      user-select: none;
    }
    .pydatagrid-table th.sortable:hover {
      background: #e9ecef;
    }
    .pydatagrid-table tr.selected {
      background: #e3f2fd;
    }
    .pydatagrid-table tr:hover {
      background: #f5f5f5;
    }
    .select-column {
      width: 40px;
      text-align: center !important;
    }
    .filter-input {
      width: 100%;
      padding: 6px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 13px;
    }
    .pagination {
      padding: 10px;
      text-align: center;
      background: #f5f5f5;
      border: 1px solid #ddd;
      border-top: none;
    }
    .pagination button {
      margin: 0 5px;
      padding: 6px 12px;
      background: white;
      border: 1px solid #ddd;
      border-radius: 4px;
      cursor: pointer;
    }
    .pagination button:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  `]
})
export class PyDataGridComponent implements OnInit, OnChanges {
  @Input() gridOptions!: GridOptions;
  @Input() rowData: any[] = [];
  
  selectedRows = new Set<any>();
  currentPage = 1;
  totalPages = 1;
  sortColumn: string = '';
  sortDirection: 'asc' | 'desc' = 'asc';
  filters: { [key: string]: string } = {};
  
  private apiUrl: string;

  constructor(private http: HttpClient) {
    this.apiUrl = 'http://localhost:5000/grid/api';
  }

  ngOnInit() {
    if (this.gridOptions.apiUrl) {
      this.apiUrl = this.gridOptions.apiUrl;
    }
    this.loadData();
  }

  ngOnChanges() {
    if (this.rowData && this.rowData.length > 0) {
      // If rowData is provided directly, use it (no API call)
      return;
    }
    this.loadData();
  }

  loadData() {
    let params = new HttpParams()
      .set('page', this.currentPage.toString())
      .set('page_size', (this.gridOptions.paginationPageSize || 100).toString())
      .set('sort_column', this.sortColumn)
      .set('sort_direction', this.sortDirection);

    Object.entries(this.filters).forEach(([key, value]) => {
      if (value) {
        params = params.set(`filter_${key}`, value);
      }
    });

    this.http.get<any>(`${this.apiUrl}/data`, { params }).subscribe({
      next: (response) => {
        this.rowData = response.data;
        this.totalPages = response.totalPages;
      },
      error: (err) => console.error('Error loading data:', err)
    });
  }

  onSort(field: string) {
    if (this.sortColumn === field) {
      this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
    } else {
      this.sortColumn = field;
      this.sortDirection = 'asc';
    }
    this.loadData();
  }

  onFilter(field: string, value: string) {
    if (value) {
      this.filters[field] = value;
    } else {
      delete this.filters[field];
    }
    this.currentPage = 1;
    this.loadData();
  }

  onPageChange(page: number) {
    this.currentPage = page;
    this.loadData();
  }

  onRowSelect(id: any, checked: boolean) {
    if (checked) {
      this.selectedRows.add(id);
    } else {
      this.selectedRows.delete(id);
    }
  }

  onSelectAll(checked: boolean) {
    if (checked) {
      this.rowData.forEach(row => this.selectedRows.add(row.id || row));
    } else {
      this.selectedRows.clear();
    }
  }

  isAllSelected(): boolean {
    return this.rowData.length > 0 && 
           this.rowData.every(row => this.selectedRows.has(row.id || row));
  }

  onExportSelected() {
    const url = `${this.apiUrl}/export?export_type=selected&selected_rows=${JSON.stringify(Array.from(this.selectedRows))}`;
    window.open(url, '_blank');
  }

  onExportAll() {
    const url = `${this.apiUrl}/export?export_type=all`;
    window.open(url, '_blank');
  }
}
```

#### Step 3: Register Component in Module

```typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { PyDataGridComponent } from './pydatagrid.component';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
    PyDataGridComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

#### Step 4: Use the Component

```typescript
// app.component.ts
import { Component } from '@angular/core';
import { GridOptions, ColumnDef } from './pydatagrid.component';

@Component({
  selector: 'app-root',
  template: `
    <div style="padding: 20px;">
      <h1>My Data Grid</h1>
      
      <!-- Use PyDataGrid component -->
      <pydatagrid 
        [gridOptions]="gridOptions"
        style="height: 600px; width: 100%;">
      </pydatagrid>
    </div>
  `
})
export class AppComponent {
  // Define grid options
  gridOptions: GridOptions = {
    columnDefs: [
      { field: 'id', headerName: 'ID', width: 80 },
      { field: 'name', headerName: 'Full Name', sortable: true },
      { field: 'email', headerName: 'Email Address', sortable: true, filter: true },
      { field: 'age', headerName: 'Age', width: 100, sortable: true },
      { field: 'city', headerName: 'City', filter: true }
    ],
    pagination: true,
    paginationPageSize: 50,
    enableSorting: true,
    enableFiltering: true,
    enableExport: true,
    apiUrl: 'http://localhost:5000/grid/api'  // Optional: customize API URL
  };
}
```

#### Alternative: With Static Data

```typescript
// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <pydatagrid 
      [gridOptions]="gridOptions"
      [rowData]="rowData">
    </pydatagrid>
  `
})
export class AppComponent {
  gridOptions: GridOptions = {
    columnDefs: [
      { field: 'name', headerName: 'Name' },
      { field: 'age', headerName: 'Age' },
      { field: 'city', headerName: 'City' }
    ],
    pagination: true,
    paginationPageSize: 10
  };

  // Provide data directly (no API calls)
  rowData = [
    { name: 'John', age: 30, city: 'New York' },
    { name: 'Jane', age: 25, city: 'London' },
    { name: 'Bob', age: 35, city: 'Paris' }
  ];
}
```

---

### Method 2: iframe Embedding (Quickest)

For rapid prototyping or when you want the full PyDataGrid UI:

```typescript
// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <h1>My Data Grid</h1>
      <iframe 
        src="http://localhost:5000/grid/" 
        width="100%" 
        height="800px"
        frameborder="0"
        style="border: 1px solid #ddd; border-radius: 8px;">
      </iframe>
    </div>
  `,
  styles: [`
    .container { padding: 20px; }
  `]
})
export class AppComponent {}
```

---

### Key Features

✅ **Property-Based Configuration** - Familiar Angular component API  
✅ **Column Definitions** - Define columns with headerName, width, sortable, filter  
✅ **Pagination** - Built-in pagination with page size control  
✅ **Sorting** - Click column headers to sort  
✅ **Filtering** - Per-column filter inputs  
✅ **Row Selection** - Individual and bulk selection  
✅ **Export** - Export selected or all data to CSV  
✅ **REST API** - Python backend handles all data processing  
✅ **TypeScript** - Full TypeScript support with interfaces  

---

### Complete Working Example

See `examples/angular-integration/` for a complete working Angular app with PyDataGrid integration.

```bash
# Run the example
cd examples/angular-integration
npm install
npm start
```

## API Reference

### DataGrid Class

#### Constructor
```python
DataGrid(data, columns=None, column_config=None)
```

#### Methods

- `setVisibleColumns(columns)`: Set which columns to display
- `setPageSize(size)`: Set number of rows per page
- `setPage(page)`: Navigate to specific page
- `setSorting(column, direction)`: Set sorting configuration
- `setFilter(column, value)`: Apply filter to a column
- `clearFilters()`: Remove all filters
- `getProcessedData()`: Get current page data
- `getFilteredData()`: Get all filtered data (no pagination)
- `getTotalRows()`: Get total number of rows
- `getTotalPages()`: Get total number of pages
- `toJson()`: Export current page to JSON
- `toJsonAll()`: Export all data to JSON
- `toCsv(selected_rows)`: Export data to CSV

### Flask Integration

#### create_grid_blueprint()
```python
create_grid_blueprint(data_source, name='datagrid', url_prefix='/grid')
```

Creates a Flask Blueprint with the following routes:
- `/`: Main grid interface
- `/api/data`: Get paginated data
- `/api/export`: Export data as CSV

#### create_grid_app()
```python
create_grid_app(data, host='127.0.0.1', port=5000, debug=True)
```

Creates a complete Flask application with the grid.

## Configuration

### Naming Conventions

The library follows these naming conventions:
- **PascalCase**: Component names, interfaces, and type aliases
- **camelCase**: Variables, functions, and methods
- **_underscore prefix**: Private class members
- **ALL_CAPS**: Constants

### Performance Optimization

- Default page size: 100 rows (configurable)
- Column width adjusts to screen size
- Text truncation with ellipsis for long content
- Tooltip shows up to 200 characters on hover
- Efficient filtering and sorting algorithms

## Browser Support

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)

## Requirements

- Python >= 3.7
- Flask >= 2.0.0
- flask-cors >= 3.0.0

## License

MIT License - see LICENSE file for details

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Support

For issues and questions, please open an issue on GitHub.

## Changelog

### Version 1.0.0
- Initial release
- Core grid functionality
- Sorting and filtering
- Pagination
- Column visibility controls
- Export to CSV
- Flask integration
- Responsive design