I can click on the grid to select a cell

This commit is contained in:
2026-01-24 12:06:22 +01:00
parent 191ead1c89
commit ba2b6e672a
9 changed files with 1268 additions and 211 deletions

601
docs/DataGrid.md Normal file
View File

@@ -0,0 +1,601 @@
# DataGrid Component
## Introduction
The DataGrid component provides a high-performance tabular data display for your FastHTML application. It renders pandas
DataFrames with interactive features like column resizing, reordering, and filtering, all powered by HTMX for seamless
updates without page reloads.
**Key features:**
- Display tabular data from pandas DataFrames
- Resizable columns with drag handles
- Draggable columns for reordering
- Real-time filtering with search bar
- Virtual scrolling for large datasets (pagination with lazy loading)
- Custom scrollbars for consistent cross-browser appearance
- Optional state persistence per session
**Common use cases:**
- Data exploration and analysis dashboards
- Admin interfaces with tabular data
- Report viewers
- Database table browsers
- CSV/Excel file viewers
## Quick Start
Here's a minimal example showing a data table with a pandas DataFrame:
```python
import pandas as pd
from fasthtml.common import *
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.core.instances import RootInstance
# Create sample data
df = pd.DataFrame({
"Name": ["Alice", "Bob", "Charlie", "Diana"],
"Age": [25, 30, 35, 28],
"City": ["Paris", "London", "Berlin", "Madrid"]
})
# Create root instance and data grid
root = RootInstance(session)
grid = DataGrid(parent=root)
grid.init_from_dataframe(df)
# Render the grid
return grid
```
This creates a complete data grid with:
- A header row with column names ("Name", "Age", "City")
- Data rows displaying the DataFrame content
- A search bar for filtering data
- Resizable column borders (drag to resize)
- Draggable columns (drag headers to reorder)
- Custom scrollbars for horizontal and vertical scrolling
**Note:** The DataGrid automatically detects column types (Text, Number, Bool, Datetime) from the DataFrame dtypes and
applies appropriate formatting.
## Basic Usage
### Visual Structure
The DataGrid component consists of a filter bar, a table with header/body/footer, and custom scrollbars:
```
┌────────────────────────────────────────────────────────────┐
│ Filter Bar │
│ ┌─────────────────────────────────────────────┐ ┌────┐ │
│ │ 🔍 Search... │ │ ✕ │ │
│ └─────────────────────────────────────────────┘ └────┘ │
├────────────────────────────────────────────────────────────┤
│ Header Row ▲ │
│ ┌──────────┬──────────┬──────────┬──────────┐ │ │
│ │ Column 1 │ Column 2 │ Column 3 │ Column 4 │ █ │
│ └──────────┴──────────┴──────────┴──────────┘ █ │
├────────────────────────────────────────────────────────█───┤
│ Body (scrollable) █ │
│ ┌──────────┬──────────┬──────────┬──────────┐ █ │
│ │ Value │ Value │ Value │ Value │ █ │
│ ├──────────┼──────────┼──────────┼──────────┤ │ │
│ │ Value │ Value │ Value │ Value │ │ │
│ ├──────────┼──────────┼──────────┼──────────┤ ▼ │
│ │ Value │ Value │ Value │ Value │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ ◄═══════════════════════════════════════════════════════► │
└────────────────────────────────────────────────────────────┘
```
**Component details:**
| Element | Description |
|------------|-------------------------------------------------------|
| Filter bar | Search input with filter mode toggle and clear button |
| Header row | Column names with resize handles and drag support |
| Body | Scrollable data rows with virtual pagination |
| Scrollbars | Custom vertical and horizontal scrollbars |
### Creating a DataGrid
The DataGrid is a `MultipleInstance`, meaning you can create multiple independent grids in your application. Create it
by providing a parent instance:
```python
grid = DataGrid(parent=root_instance)
# Or with a custom ID
grid = DataGrid(parent=root_instance, _id="my-grid")
# Or with state persistence enabled
grid = DataGrid(parent=root_instance, save_state=True)
```
**Parameters:**
- `parent`: Parent instance (required)
- `_id` (str, optional): Custom identifier for the grid
- `save_state` (bool, optional): Enable state persistence (column widths, order, filters)
### Loading Data
Use the `init_from_dataframe()` method to load data into the grid:
```python
import pandas as pd
# Create a DataFrame
df = pd.DataFrame({
"Product": ["Laptop", "Phone", "Tablet"],
"Price": [999.99, 699.99, 449.99],
"In Stock": [True, False, True]
})
# Load into grid
grid.init_from_dataframe(df)
```
**Column type detection:**
The DataGrid automatically detects column types from pandas dtypes:
| pandas dtype | DataGrid type | Display |
|--------------------|---------------|-------------------------|
| `int64`, `float64` | Number | Right-aligned |
| `bool` | Bool | Checkbox icon |
| `datetime64` | Datetime | Formatted date |
| `object`, others | Text | Left-aligned, truncated |
### Row Index Column
By default, the DataGrid displays a row index column on the left. This can be useful for identifying rows:
```python
# Row index is enabled by default
grid._state.row_index = True
# To disable the row index column
grid._state.row_index = False
grid.init_from_dataframe(df)
```
## Column Features
### Resizing Columns
Users can resize columns by dragging the border between column headers:
- **Drag handle location**: Right edge of each column header
- **Minimum width**: 30 pixels
- **Persistence**: Resized widths are automatically saved when `save_state=True`
The resize interaction:
1. Hover over the right edge of a column header (cursor changes)
2. Click and drag to resize
3. Release to confirm the new width
4. Double-click to reset to default width
**Programmatic width control:**
```python
# Set a specific column width
for col in grid._state.columns:
if col.col_id == "my_column":
col.width = 200 # pixels
break
```
### Moving Columns
Users can reorder columns by dragging column headers:
1. Click and hold a column header
2. Drag to the desired position
3. Release to drop the column
The columns animate smoothly during the move, and other columns shift to accommodate the new position.
**Note:** Column order is persisted when `save_state=True`.
### Column Visibility
Columns can be hidden programmatically:
```python
# Hide a specific column
for col in grid._state.columns:
if col.col_id == "internal_id":
col.visible = False
break
```
Hidden columns are not rendered but remain in the state, allowing them to be shown again later.
## Filtering
### Using the Search Bar
The DataGrid includes a built-in search bar that filters rows in real-time:
```
┌─────────────────────────────────────────────┐ ┌────┐
│ 🔍 Search... │ │ ✕ │
└─────────────────────────────────────────────┘ └────┘
│ │
│ └── Clear button
└── Filter mode icon (click to cycle)
```
**How filtering works:**
1. Type in the search box
2. The grid filters rows where ANY visible column contains the search text
3. Matching text is highlighted in the results
4. Click the ✕ button to clear the filter
### Filter Modes
Click the filter icon to cycle through three modes:
| Mode | Icon | Description |
|------------|------|------------------------------------|
| **Filter** | 🔍 | Hides non-matching rows |
| **Search** | 🔎 | Highlights matches, shows all rows |
| **AI** | 🧠 | AI-powered search (future feature) |
The current mode affects how results are displayed:
- **Filter mode**: Only matching rows are shown
- **Search mode**: All rows shown, matches highlighted
## Advanced Features
### State Persistence
Enable state persistence to save user preferences across sessions:
```python
# Enable state persistence
grid = DataGrid(parent=root, save_state=True)
```
**What gets persisted:**
| State | Description |
|-------------------|---------------------------------|
| Column widths | User-resized column sizes |
| Column order | User-defined column arrangement |
| Column visibility | Which columns are shown/hidden |
| Sort order | Current sort configuration |
| Filter state | Active filters |
### Virtual Scrolling
For large datasets, the DataGrid uses virtual scrolling with lazy loading:
- Only a subset of rows (page) is rendered initially
- As the user scrolls down, more rows are loaded automatically
- Uses Intersection Observer API for efficient scroll detection
- Default page size: configurable via `DATAGRID_PAGE_SIZE`
This allows smooth performance even with thousands of rows.
### Text Size
Customize the text size for the grid body:
```python
# Available sizes: "xs", "sm", "md", "lg"
grid._settings.text_size = "sm" # default
```
### CSS Customization
The DataGrid uses CSS classes that you can customize:
| Class | Element |
|-----------------------------|-------------------------|
| `dt2-table-wrapper` | Root table container |
| `dt2-table` | Table element |
| `dt2-header-container` | Header wrapper |
| `dt2-body-container` | Scrollable body wrapper |
| `dt2-footer-container` | Footer wrapper |
| `dt2-row` | Table row |
| `dt2-cell` | Table cell |
| `dt2-resize-handle` | Column resize handle |
| `dt2-scrollbars-vertical` | Vertical scrollbar |
| `dt2-scrollbars-horizontal` | Horizontal scrollbar |
| `dt2-highlight-1` | Search match highlight |
**Example customization:**
```css
/* Change highlight color */
.dt2-highlight-1 {
background-color: #fef08a;
font-weight: bold;
}
/* Customize row hover */
.dt2-row:hover {
background-color: #f3f4f6;
}
/* Style the scrollbars */
.dt2-scrollbars-vertical,
.dt2-scrollbars-horizontal {
background-color: #3b82f6;
border-radius: 4px;
}
```
## Examples
### Example 1: Simple Data Table
A basic data table displaying product information:
```python
import pandas as pd
from fasthtml.common import *
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.core.instances import RootInstance
# Sample product data
df = pd.DataFrame({
"Product": ["Laptop Pro", "Wireless Mouse", "USB-C Hub", "Monitor 27\"", "Keyboard"],
"Category": ["Computers", "Accessories", "Accessories", "Displays", "Accessories"],
"Price": [1299.99, 49.99, 79.99, 399.99, 129.99],
"In Stock": [True, True, False, True, True],
"Rating": [4.5, 4.2, 4.8, 4.6, 4.3]
})
# Create and configure grid
root = RootInstance(session)
grid = DataGrid(parent=root, _id="products-grid")
grid.init_from_dataframe(df)
# Render
return Div(
H1("Product Catalog"),
grid,
cls="p-4"
)
```
### Example 2: Large Dataset with Filtering
Handling a large dataset with virtual scrolling and filtering:
```python
import pandas as pd
import numpy as np
from fasthtml.common import *
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.core.instances import RootInstance
# Generate large dataset (10,000 rows)
np.random.seed(42)
n_rows = 10000
df = pd.DataFrame({
"ID": range(1, n_rows + 1),
"Name": [f"Item_{i}" for i in range(n_rows)],
"Value": np.random.uniform(10, 1000, n_rows).round(2),
"Category": np.random.choice(["A", "B", "C", "D"], n_rows),
"Active": np.random.choice([True, False], n_rows),
"Created": pd.date_range("2024-01-01", periods=n_rows, freq="h")
})
# Create grid with state persistence
root = RootInstance(session)
grid = DataGrid(parent=root, _id="large-dataset", save_state=True)
grid.init_from_dataframe(df)
return Div(
H1("Large Dataset Explorer"),
P(f"Displaying {n_rows:,} rows with virtual scrolling"),
grid,
cls="p-4",
style="height: 100vh;"
)
```
**Note:** Virtual scrolling loads rows on demand as you scroll, ensuring smooth performance even with 10,000+ rows.
### Example 3: Dashboard with Multiple Grids
An application with multiple data grids in different tabs:
```python
import pandas as pd
from fasthtml.common import *
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.core.instances import RootInstance
# Create data for different views
sales_df = pd.DataFrame({
"Date": pd.date_range("2024-01-01", periods=30, freq="D"),
"Revenue": [1000 + i * 50 for i in range(30)],
"Orders": [10 + i for i in range(30)]
})
customers_df = pd.DataFrame({
"Customer": ["Acme Corp", "Tech Inc", "Global Ltd"],
"Country": ["USA", "UK", "Germany"],
"Total Spent": [15000, 12000, 8500]
})
# Create instances
root = RootInstance(session)
tabs = TabsManager(parent=root, _id="dashboard-tabs")
# Create grids
sales_grid = DataGrid(parent=root, _id="sales-grid")
sales_grid.init_from_dataframe(sales_df)
customers_grid = DataGrid(parent=root, _id="customers-grid")
customers_grid.init_from_dataframe(customers_df)
# Add to tabs
tabs.create_tab("Sales", sales_grid)
tabs.create_tab("Customers", customers_grid)
return Div(
H1("Sales Dashboard"),
tabs,
cls="p-4"
)
```
---
## Developer Reference
This section contains technical details for developers working on the DataGrid component itself.
### State
The DataGrid uses two state objects:
**DatagridState** - Main state for grid data and configuration:
| Name | Type | Description | Default |
|-------------------|---------------------------|----------------------------|---------|
| `sidebar_visible` | bool | Whether sidebar is visible | `False` |
| `row_index` | bool | Show row index column | `True` |
| `columns` | list[DataGridColumnState] | Column definitions | `[]` |
| `rows` | list[DataGridRowState] | Row-specific states | `[]` |
| `sorted` | list | Sort configuration | `[]` |
| `filtered` | dict | Active filters | `{}` |
| `selection` | DatagridSelectionState | Selection state | - |
| `ne_df` | DataFrame | The data (non-persisted) | `None` |
**DatagridSettings** - User preferences:
| Name | Type | Description | Default |
|----------------------|------|--------------------|---------|
| `save_state` | bool | Enable persistence | `False` |
| `header_visible` | bool | Show header row | `True` |
| `filter_all_visible` | bool | Show filter bar | `True` |
| `text_size` | str | Body text size | `"sm"` |
### Column State
Each column is represented by `DataGridColumnState`:
| Name | Type | Description | Default |
|-------------|------------|--------------------|---------|
| `col_id` | str | Column identifier | - |
| `col_index` | int | Index in DataFrame | - |
| `title` | str | Display title | `None` |
| `type` | ColumnType | Data type | `Text` |
| `visible` | bool | Is column visible | `True` |
| `usable` | bool | Is column usable | `True` |
| `width` | int | Width in pixels | `150` |
### Commands
Available commands for programmatic control:
| Name | Description |
|------------------------|---------------------------------------------|
| `get_page(page_index)` | Load a specific page of data (lazy loading) |
| `set_column_width()` | Update column width after resize |
| `move_column()` | Move column to new position |
| `filter()` | Apply current filter to grid |
### Public Methods
| Method | Description |
|-----------------------------------------------|----------------------------------------|
| `init_from_dataframe(df, init_state=True)` | Load data from pandas DataFrame |
| `set_column_width(col_id, width)` | Set column width programmatically |
| `move_column(source_col_id, target_col_id)` | Move column to new position |
| `filter()` | Apply filter and return partial render |
| `render()` | Render the complete grid |
| `render_partial(fragment, redraw_scrollbars)` | Render only part of the grid |
### High Level Hierarchical Structure
```
Div(id="{id}", cls="grid")
├── Div (filter bar)
│ └── DataGridQuery # Filter/search component
├── Div(id="tw_{id}", cls="dt2-table-wrapper")
│ ├── Div(id="t_{id}", cls="dt2-table")
│ │ ├── Div (dt2-header-container)
│ │ │ └── Div(id="th_{id}", cls="dt2-row dt2-header")
│ │ │ ├── Div (dt2-cell) # Column 1 header
│ │ │ ├── Div (dt2-cell) # Column 2 header
│ │ │ └── ...
│ │ ├── Div(id="tb_{id}", cls="dt2-body-container")
│ │ │ └── Div (dt2-body)
│ │ │ ├── Div (dt2-row) # Data row 1
│ │ │ ├── Div (dt2-row) # Data row 2
│ │ │ └── ...
│ │ └── Div (dt2-footer-container)
│ │ └── Div (dt2-row dt2-header) # Footer row
│ └── Div (dt2-scrollbars)
│ ├── Div (dt2-scrollbars-vertical-wrapper)
│ │ └── Div (dt2-scrollbars-vertical)
│ └── Div (dt2-scrollbars-horizontal-wrapper)
│ └── Div (dt2-scrollbars-horizontal)
└── Script # Initialization script
```
### Element IDs
| Pattern | Description |
|-----------------------|-------------------------------------|
| `{id}` | Root grid container |
| `tw_{id}` | Table wrapper (scrollbar container) |
| `t_{id}` | Table element |
| `th_{id}` | Header row |
| `tb_{id}` | Body container |
| `tf_{id}` | Footer row |
| `tsm_{id}` | Selection Manager |
| `tr_{id}-{row_index}` | Individual data row |
### Internal Methods
These methods are used internally for rendering:
| Method | Description |
|---------------------------------------------|----------------------------------------|
| `mk_headers()` | Renders the header row |
| `mk_body()` | Renders the body with first page |
| `mk_body_container()` | Renders the scrollable body container |
| `mk_body_content_page(page_index)` | Renders a specific page of rows |
| `mk_body_cell(col_pos, row_index, col_def)` | Renders a single cell |
| `mk_body_cell_content(...)` | Renders cell content with highlighting |
| `mk_footers()` | Renders the footer row |
| `mk_table()` | Renders the complete table structure |
| `mk_aggregation_cell(...)` | Renders footer aggregation cell |
| `_get_filtered_df()` | Returns filtered and sorted DataFrame |
| `_apply_sort(df)` | Applies sort configuration |
| `_apply_filter(df)` | Applies filter configuration |
### DataGridQuery Component
The filter bar is a separate component (`DataGridQuery`) with its own state:
| State Property | Type | Description | Default |
|----------------|------|-----------------------------------------|------------|
| `filter_type` | str | Current mode ("filter", "search", "ai") | `"filter"` |
| `query` | str | Current search text | `None` |
**Commands:**
| Command | Description |
|------------------------|-----------------------------|
| `change_filter_type()` | Cycle through filter modes |
| `on_filter_changed()` | Handle search input changes |
| `on_cancel_query()` | Clear the search query |