table.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. from typing import Any, Callable, Dict, List, Literal, Optional, Union
  2. from typing_extensions import Self
  3. from .. import optional_features
  4. from ..element import Element
  5. from ..events import GenericEventArguments, TableSelectionEventArguments, ValueChangeEventArguments, handle_event
  6. from .mixins.filter_element import FilterElement
  7. try:
  8. import pandas as pd
  9. optional_features.register('pandas')
  10. except ImportError:
  11. pass
  12. class Table(FilterElement, component='table.js'):
  13. def __init__(self,
  14. columns: List[Dict],
  15. rows: List[Dict],
  16. row_key: str = 'id',
  17. title: Optional[str] = None,
  18. selection: Optional[Literal['single', 'multiple']] = None,
  19. pagination: Optional[Union[int, dict]] = None,
  20. on_select: Optional[Callable[..., Any]] = None,
  21. on_pagination_change: Optional[Callable[..., Any]] = None,
  22. ) -> None:
  23. """Table
  24. A table based on Quasar's `QTable <https://quasar.dev/vue-components/table>`_ component.
  25. :param columns: list of column objects
  26. :param rows: list of row objects
  27. :param row_key: name of the column containing unique data identifying the row (default: "id")
  28. :param title: title of the table
  29. :param selection: selection type ("single" or "multiple"; default: `None`)
  30. :param pagination: a dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
  31. :param on_select: callback which is invoked when the selection changes
  32. :param on_pagination_change: callback which is invoked when the pagination changes
  33. If selection is 'single' or 'multiple', then a `selected` property is accessible containing the selected rows.
  34. """
  35. super().__init__()
  36. self._props['columns'] = columns
  37. self._props['rows'] = rows
  38. self._props['row-key'] = row_key
  39. self._props['title'] = title
  40. self._props['hide-pagination'] = pagination is None
  41. self._props['pagination'] = pagination if isinstance(pagination, dict) else {'rowsPerPage': pagination or 0}
  42. self._props['selection'] = selection or 'none'
  43. self._props['selected'] = []
  44. self._props['fullscreen'] = False
  45. def handle_selection(e: GenericEventArguments) -> None:
  46. if e.args['added']:
  47. if selection == 'single':
  48. self.selected.clear()
  49. self.selected.extend(e.args['rows'])
  50. else:
  51. self.selected = [row for row in self.selected if row[row_key] not in e.args['keys']]
  52. self.update()
  53. arguments = TableSelectionEventArguments(sender=self, client=self.client, selection=self.selected)
  54. handle_event(on_select, arguments)
  55. self.on('selection', handle_selection, ['added', 'rows', 'keys'])
  56. def handle_pagination_change(e: GenericEventArguments) -> None:
  57. self.pagination = e.args
  58. self.update()
  59. arguments = ValueChangeEventArguments(sender=self, client=self.client, value=self.pagination)
  60. handle_event(on_pagination_change, arguments)
  61. self.on('update:pagination', handle_pagination_change)
  62. @classmethod
  63. def from_pandas(cls,
  64. df: 'pd.DataFrame',
  65. row_key: str = 'id',
  66. title: Optional[str] = None,
  67. selection: Optional[Literal['single', 'multiple']] = None,
  68. pagination: Optional[Union[int, dict]] = None,
  69. on_select: Optional[Callable[..., Any]] = None) -> Self:
  70. """Create a table from a Pandas DataFrame.
  71. Note:
  72. If the DataFrame contains non-serializable columns of type `datetime64[ns]`, `timedelta64[ns]`, `complex128` or `period[M]`,
  73. they will be converted to strings.
  74. To use a different conversion, convert the DataFrame manually before passing it to this method.
  75. See `issue 1698 <https://github.com/zauberzeug/nicegui/issues/1698>`_ for more information.
  76. :param df: Pandas DataFrame
  77. :param row_key: name of the column containing unique data identifying the row (default: "id")
  78. :param title: title of the table
  79. :param selection: selection type ("single" or "multiple"; default: `None`)
  80. :param pagination: a dictionary correlating to a pagination object or number of rows per page (`None` hides the pagination, 0 means "infinite"; default: `None`).
  81. :param on_select: callback which is invoked when the selection changes
  82. :return: table element
  83. """
  84. date_cols = df.columns[df.dtypes == 'datetime64[ns]']
  85. time_cols = df.columns[df.dtypes == 'timedelta64[ns]']
  86. complex_cols = df.columns[df.dtypes == 'complex128']
  87. period_cols = df.columns[df.dtypes == 'period[M]']
  88. if len(date_cols) != 0 or len(time_cols) != 0 or len(complex_cols) != 0 or len(period_cols) != 0:
  89. df = df.copy()
  90. df[date_cols] = df[date_cols].astype(str)
  91. df[time_cols] = df[time_cols].astype(str)
  92. df[complex_cols] = df[complex_cols].astype(str)
  93. df[period_cols] = df[period_cols].astype(str)
  94. return cls(
  95. columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
  96. rows=df.to_dict('records'),
  97. row_key=row_key,
  98. title=title,
  99. selection=selection,
  100. pagination=pagination,
  101. on_select=on_select)
  102. @property
  103. def rows(self) -> List[Dict]:
  104. """List of rows."""
  105. return self._props['rows']
  106. @rows.setter
  107. def rows(self, value: List[Dict]) -> None:
  108. self._props['rows'][:] = value
  109. self.update()
  110. @property
  111. def columns(self) -> List[Dict]:
  112. """List of columns."""
  113. return self._props['columns']
  114. @columns.setter
  115. def columns(self, value: List[Dict]) -> None:
  116. self._props['columns'][:] = value
  117. self.update()
  118. @property
  119. def row_key(self) -> str:
  120. """Name of the column containing unique data identifying the row."""
  121. return self._props['row-key']
  122. @row_key.setter
  123. def row_key(self, value: str) -> None:
  124. self._props['row-key'] = value
  125. self.update()
  126. @property
  127. def selected(self) -> List[Dict]:
  128. """List of selected rows."""
  129. return self._props['selected']
  130. @selected.setter
  131. def selected(self, value: List[Dict]) -> None:
  132. self._props['selected'][:] = value
  133. self.update()
  134. @property
  135. def pagination(self) -> dict:
  136. """Pagination object."""
  137. return self._props['pagination']
  138. @pagination.setter
  139. def pagination(self, value: dict) -> None:
  140. self._props['pagination'] = value
  141. self.update()
  142. @property
  143. def is_fullscreen(self) -> bool:
  144. """Whether the table is in fullscreen mode."""
  145. return self._props['fullscreen']
  146. @is_fullscreen.setter
  147. def is_fullscreen(self, value: bool) -> None:
  148. """Set fullscreen mode."""
  149. self._props['fullscreen'] = value
  150. self.update()
  151. def set_fullscreen(self, value: bool) -> None:
  152. """Set fullscreen mode."""
  153. self.is_fullscreen = value
  154. def toggle_fullscreen(self) -> None:
  155. """Toggle fullscreen mode."""
  156. self.is_fullscreen = not self.is_fullscreen
  157. def add_rows(self, *rows: Dict) -> None:
  158. """Add rows to the table."""
  159. self.rows.extend(rows)
  160. self.update()
  161. def remove_rows(self, *rows: Dict) -> None:
  162. """Remove rows from the table."""
  163. keys = [row[self.row_key] for row in rows]
  164. self.rows[:] = [row for row in self.rows if row[self.row_key] not in keys]
  165. self.selected[:] = [row for row in self.selected if row[self.row_key] not in keys]
  166. self.update()
  167. def update_rows(self, rows: List[Dict], *, clear_selection: bool = True) -> None:
  168. """Update rows in the table.
  169. :param rows: list of rows to update
  170. :param clear_selection: whether to clear the selection (default: True)
  171. """
  172. self.rows[:] = rows
  173. if clear_selection:
  174. self.selected.clear()
  175. self.update()
  176. class row(Element):
  177. def __init__(self) -> None:
  178. """Row Element
  179. This element is based on Quasar's `QTr <https://quasar.dev/vue-components/table#qtr-api>`_ component.
  180. """
  181. super().__init__('q-tr')
  182. class header(Element):
  183. def __init__(self) -> None:
  184. """Header Element
  185. This element is based on Quasar's `QTh <https://quasar.dev/vue-components/table#qth-api>`_ component.
  186. """
  187. super().__init__('q-th')
  188. class cell(Element):
  189. def __init__(self) -> None:
  190. """Cell Element
  191. This element is based on Quasar's `QTd <https://quasar.dev/vue-components/table#qtd-api>`_ component.
  192. """
  193. super().__init__('q-td')