Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI.table from_pandas method #1983

Merged
merged 4 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion nicegui/elements/table.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from __future__ import annotations

from typing import Any, Callable, Dict, List, Literal, Optional, Union

from .. import optional_features
from ..element import Element
from ..events import GenericEventArguments, TableSelectionEventArguments, handle_event
from .mixins.filter_element import FilterElement

try:
import pandas as pd
optional_features.register('pandas')
except ImportError:
pass


class Table(FilterElement, component='table.js'):

Expand All @@ -25,7 +34,7 @@ def __init__(self,
:param row_key: name of the column containing unique data identifying the row (default: "id")
:param title: title of the table
:param selection: selection type ("single" or "multiple"; default: `None`)
:param pagination: A dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
:param pagination: a dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
:param on_select: callback which is invoked when the selection changes

If selection is 'single' or 'multiple', then a `selected` property is accessible containing the selected rows.
Expand Down Expand Up @@ -54,6 +63,49 @@ def handle_selection(e: GenericEventArguments) -> None:
handle_event(on_select, arguments)
self.on('selection', handle_selection, ['added', 'rows', 'keys'])

@staticmethod
def from_pandas(df: pd.DataFrame,
row_key: str = 'id',
title: Optional[str] = None,
selection: Optional[Literal['single', 'multiple']] = None,
pagination: Optional[Union[int, dict]] = None,
on_select: Optional[Callable[..., Any]] = None) -> Table:
"""Create a table from a Pandas DataFrame.

Note:
If the DataFrame contains non-serializable columns of type `datetime64[ns]`, `timedelta64[ns]`, `complex128` or `period[M]`,
they will be converted to strings.
To use a different conversion, convert the DataFrame manually before passing it to this method.
See `issue 1698 <https://github.com/zauberzeug/nicegui/issues/1698>`_ for more information.

:param df: Pandas DataFrame
:param row_key: name of the column containing unique data identifying the row (default: "id")
:param title: title of the table
:param selection: selection type ("single" or "multiple"; default: `None`)
:param pagination: a dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
:param on_select: callback which is invoked when the selection changes
:return: table element
"""
date_cols = df.columns[df.dtypes == 'datetime64[ns]']
time_cols = df.columns[df.dtypes == 'timedelta64[ns]']
complex_cols = df.columns[df.dtypes == 'complex128']
period_cols = df.columns[df.dtypes == 'period[M]']
if len(date_cols) != 0 or len(time_cols) != 0 or len(complex_cols) != 0 or len(period_cols) != 0:
df = df.copy()
df[date_cols] = df[date_cols].astype(str)
df[time_cols] = df[time_cols].astype(str)
df[complex_cols] = df[complex_cols].astype(str)
df[period_cols] = df[period_cols].astype(str)

return Table(
columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
rows=df.to_dict('records'),
row_key=row_key,
title=title,
selection=selection,
pagination=pagination,
on_select=on_select)

@property
def rows(self) -> List[Dict]:
"""List of rows."""
Expand Down
35 changes: 35 additions & 0 deletions tests/test_table.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import datetime, timedelta
from typing import List

import pandas as pd
from selenium.webdriver.common.by import By

from nicegui import ui
Expand Down Expand Up @@ -158,3 +160,36 @@ def replace_rows():
screen.should_not_contain('Bob')
screen.should_not_contain('Lionel')
screen.should_contain('Carol')


def test_create_from_pandas(screen: Screen):
df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21], 42: 'answer'})
ui.table.from_pandas(df)

screen.open('/')
screen.should_contain('Alice')
screen.should_contain('Bob')
screen.should_contain('18')
screen.should_contain('21')
screen.should_contain('42')
screen.should_contain('answer')


def test_problematic_datatypes(screen: Screen):
df = pd.DataFrame({
'Datetime_col': [datetime(2020, 1, 1)],
'Timedelta_col': [timedelta(days=5)],
'Complex_col': [1 + 2j],
'Period_col': pd.Series([pd.Period('2021-01')]),
})
ui.table.from_pandas(df)

screen.open('/')
screen.should_contain('Datetime_col')
screen.should_contain('Timedelta_col')
screen.should_contain('Complex_col')
screen.should_contain('Period_col')
screen.should_contain('2020-01-01')
screen.should_contain('5 days')
screen.should_contain('(1+2j)')
screen.should_contain('2021-01')
6 changes: 3 additions & 3 deletions website/more_documentation/aggrid_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ def aggrid_with_conditional_cell_formatting():
],
})

@text_demo('Create Grid from Pandas Dataframe', '''
You can create an AG Grid from a Pandas Dataframe using the `from_pandas` method.
This method takes a Pandas Dataframe as input and returns an AG Grid.
@text_demo('Create Grid from Pandas DataFrame', '''
You can create an AG Grid from a Pandas DataFrame using the `from_pandas` method.
This method takes a Pandas DataFrame as input and returns an AG Grid.
''')
def aggrid_from_pandas():
import pandas as pd
Expand Down
10 changes: 4 additions & 6 deletions website/more_documentation/table_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,15 @@ def rename(e: events.GenericEventArguments) -> None:
''')
table.on('rename', rename)

@text_demo('Table from pandas dataframe', '''
Here is a demo of how to create a table from a pandas dataframe.
@text_demo('Table from Pandas DataFrame', '''
You can create a table from a Pandas DataFrame using the `from_pandas` method.
This method takes a Pandas DataFrame as input and returns a table.
''')
def table_from_pandas_demo():
import pandas as pd

df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]})
ui.table(
columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
rows=df.to_dict('records'),
)
ui.table.from_pandas(df).classes('max-h-40')

@text_demo('Adding rows', '''
It's simple to add new rows with the `add_rows(dict)` method.
Expand Down
2 changes: 1 addition & 1 deletion website/static/search_index.json
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@
},
{
"title": "Table: Table from pandas dataframe",
"content": "Here is a demo of how to create a table from a pandas dataframe.",
"content": "You can create a table from a pandas dataframe using the from_pandas method. This method takes a Pandas Dataframe as input and returns a table.",
"url": "/documentation/table#table_from_pandas_dataframe"
},
{
Expand Down