|
@@ -1,10 +1,11 @@
|
|
|
-from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
|
|
+from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
|
|
|
|
|
|
from typing_extensions import Self
|
|
|
|
|
|
from .. import optional_features
|
|
|
from ..element import Element
|
|
|
from ..events import GenericEventArguments, TableSelectionEventArguments, ValueChangeEventArguments, handle_event
|
|
|
+from ..helpers import warn_once
|
|
|
from .mixins.filter_element import FilterElement
|
|
|
|
|
|
try:
|
|
@@ -17,8 +18,10 @@ except ImportError:
|
|
|
class Table(FilterElement, component='table.js'):
|
|
|
|
|
|
def __init__(self,
|
|
|
- columns: List[Dict],
|
|
|
+ *,
|
|
|
rows: List[Dict],
|
|
|
+ columns: Optional[List[Dict]] = None,
|
|
|
+ column_defaults: Optional[Dict] = None,
|
|
|
row_key: str = 'id',
|
|
|
title: Optional[str] = None,
|
|
|
selection: Optional[Literal['single', 'multiple']] = None,
|
|
@@ -30,8 +33,9 @@ class Table(FilterElement, component='table.js'):
|
|
|
|
|
|
A table based on Quasar's `QTable <https://quasar.dev/vue-components/table>`_ component.
|
|
|
|
|
|
- :param columns: list of column objects
|
|
|
:param rows: list of row objects
|
|
|
+ :param columns: list of column objects (defaults to the columns of the first row)
|
|
|
+ :param column_defaults: optional default column properties
|
|
|
: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`)
|
|
@@ -43,7 +47,13 @@ class Table(FilterElement, component='table.js'):
|
|
|
"""
|
|
|
super().__init__()
|
|
|
|
|
|
- self._props['columns'] = columns
|
|
|
+ if columns is None:
|
|
|
+ first_row = rows[0] if rows else {}
|
|
|
+ columns = [{'name': key, 'label': str(key).upper(), 'field': key, 'sortable': True} for key in first_row]
|
|
|
+
|
|
|
+ self._column_defaults = column_defaults
|
|
|
+ self._use_columns_from_df = False
|
|
|
+ self._props['columns'] = self._normalize_columns(columns)
|
|
|
self._props['rows'] = rows
|
|
|
self._props['row-key'] = row_key
|
|
|
self._props['title'] = title
|
|
@@ -86,9 +96,14 @@ class Table(FilterElement, component='table.js'):
|
|
|
self._pagination_change_handlers.append(callback)
|
|
|
return self
|
|
|
|
|
|
+ def _normalize_columns(self, columns: List[Dict]) -> List[Dict]:
|
|
|
+ return [{**self._column_defaults, **column} for column in columns] if self._column_defaults else columns
|
|
|
+
|
|
|
@classmethod
|
|
|
def from_pandas(cls,
|
|
|
- df: 'pd.DataFrame',
|
|
|
+ df: 'pd.DataFrame', *,
|
|
|
+ columns: Optional[List[Dict]] = None,
|
|
|
+ column_defaults: Optional[Dict] = None,
|
|
|
row_key: str = 'id',
|
|
|
title: Optional[str] = None,
|
|
|
selection: Optional[Literal['single', 'multiple']] = None,
|
|
@@ -103,6 +118,8 @@ class Table(FilterElement, component='table.js'):
|
|
|
See `issue 1698 <https://github.com/zauberzeug/nicegui/issues/1698>`_ for more information.
|
|
|
|
|
|
:param df: Pandas DataFrame
|
|
|
+ :param columns: list of column objects (defaults to the columns of the dataframe)
|
|
|
+ :param column_defaults: optional default column properties
|
|
|
: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`)
|
|
@@ -110,6 +127,49 @@ class Table(FilterElement, component='table.js'):
|
|
|
:param on_select: callback which is invoked when the selection changes
|
|
|
:return: table element
|
|
|
"""
|
|
|
+ rows, columns_from_df = cls._df_to_rows_and_columns(df)
|
|
|
+ table = cls(
|
|
|
+ rows=rows,
|
|
|
+ columns=columns or columns_from_df,
|
|
|
+ column_defaults=column_defaults,
|
|
|
+ row_key=row_key,
|
|
|
+ title=title,
|
|
|
+ selection=selection,
|
|
|
+ pagination=pagination,
|
|
|
+ on_select=on_select,
|
|
|
+ )
|
|
|
+ table._use_columns_from_df = columns is None
|
|
|
+ return table
|
|
|
+
|
|
|
+ def update_from_pandas(self,
|
|
|
+ df: 'pd.DataFrame', *,
|
|
|
+ clear_selection: bool = True,
|
|
|
+ columns: Optional[List[Dict]] = None,
|
|
|
+ column_defaults: Optional[Dict] = None) -> None:
|
|
|
+ """Update the table from a Pandas DataFrame.
|
|
|
+
|
|
|
+ See `from_pandas()` for more information about the conversion of non-serializable columns.
|
|
|
+
|
|
|
+ If `columns` is not provided and the columns had been inferred from a DataFrame,
|
|
|
+ the columns will be updated to match the new DataFrame.
|
|
|
+
|
|
|
+ :param df: Pandas DataFrame
|
|
|
+ :param clear_selection: whether to clear the selection (default: True)
|
|
|
+ :param columns: list of column objects (defaults to the columns of the dataframe)
|
|
|
+ :param column_defaults: optional default column properties
|
|
|
+ """
|
|
|
+ rows, columns_from_df = self._df_to_rows_and_columns(df)
|
|
|
+ self.rows[:] = rows
|
|
|
+ if column_defaults is not None:
|
|
|
+ self._column_defaults = column_defaults
|
|
|
+ if columns or self._use_columns_from_df:
|
|
|
+ self.columns[:] = self._normalize_columns(columns or columns_from_df)
|
|
|
+ if clear_selection:
|
|
|
+ self.selected.clear()
|
|
|
+ self.update()
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _df_to_rows_and_columns(df: 'pd.DataFrame') -> Tuple[List[Dict], List[Dict]]:
|
|
|
def is_special_dtype(dtype):
|
|
|
return (pd.api.types.is_datetime64_any_dtype(dtype) or
|
|
|
pd.api.types.is_timedelta64_dtype(dtype) or
|
|
@@ -125,14 +185,7 @@ class Table(FilterElement, component='table.js'):
|
|
|
'You can convert them to strings using something like '
|
|
|
'`df.columns = ["_".join(col) for col in df.columns.values]`.')
|
|
|
|
|
|
- return cls(
|
|
|
- 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)
|
|
|
+ return df.to_dict('records'), [{'name': col, 'label': col, 'field': col} for col in df.columns]
|
|
|
|
|
|
@property
|
|
|
def rows(self) -> List[Dict]:
|
|
@@ -151,9 +204,19 @@ class Table(FilterElement, component='table.js'):
|
|
|
|
|
|
@columns.setter
|
|
|
def columns(self, value: List[Dict]) -> None:
|
|
|
- self._props['columns'][:] = value
|
|
|
+ self._props['columns'][:] = self._normalize_columns(value)
|
|
|
self.update()
|
|
|
|
|
|
+ @property
|
|
|
+ def column_defaults(self) -> Optional[Dict]:
|
|
|
+ """Default column properties."""
|
|
|
+ return self._column_defaults
|
|
|
+
|
|
|
+ @column_defaults.setter
|
|
|
+ def column_defaults(self, value: Optional[Dict]) -> None:
|
|
|
+ self._column_defaults = value
|
|
|
+ self.columns = self.columns # re-normalize columns
|
|
|
+
|
|
|
@property
|
|
|
def row_key(self) -> str:
|
|
|
"""Name of the column containing unique data identifying the row."""
|
|
@@ -203,18 +266,34 @@ class Table(FilterElement, component='table.js'):
|
|
|
"""Toggle fullscreen mode."""
|
|
|
self.is_fullscreen = not self.is_fullscreen
|
|
|
|
|
|
- def add_rows(self, *rows: Dict) -> None:
|
|
|
+ def add_rows(self, rows: List[Dict], *args: Any) -> None:
|
|
|
"""Add rows to the table."""
|
|
|
+ if isinstance(rows, dict): # DEPRECATED
|
|
|
+ warn_once('Calling add_rows() with variable-length arguments is deprecated. '
|
|
|
+ 'Pass a list instead or use add_row() for a single row.')
|
|
|
+ rows = [rows, *args]
|
|
|
self.rows.extend(rows)
|
|
|
self.update()
|
|
|
|
|
|
- def remove_rows(self, *rows: Dict) -> None:
|
|
|
+ def add_row(self, row: Dict) -> None:
|
|
|
+ """Add a single row to the table."""
|
|
|
+ self.add_rows([row])
|
|
|
+
|
|
|
+ def remove_rows(self, rows: List[Dict], *args: Any) -> None:
|
|
|
"""Remove rows from the table."""
|
|
|
+ if isinstance(rows, dict): # DEPRECATED
|
|
|
+ warn_once('Calling remove_rows() with variable-length arguments is deprecated. '
|
|
|
+ 'Pass a list instead or use remove_row() for a single row.')
|
|
|
+ rows = [rows, *args]
|
|
|
keys = [row[self.row_key] for row in rows]
|
|
|
self.rows[:] = [row for row in self.rows if row[self.row_key] not in keys]
|
|
|
self.selected[:] = [row for row in self.selected if row[self.row_key] not in keys]
|
|
|
self.update()
|
|
|
|
|
|
+ def remove_row(self, row: Dict) -> None:
|
|
|
+ """Remove a single row from the table."""
|
|
|
+ self.remove_rows([row])
|
|
|
+
|
|
|
def update_rows(self, rows: List[Dict], *, clear_selection: bool = True) -> None:
|
|
|
"""Update rows in the table.
|
|
|
|