From 13e87ea7a611bdb1a65e3cc6689fe4e1bf5024a0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 12 Jul 2024 16:01:42 +0200 Subject: [PATCH 1/4] Add support for automatically determining optimal Tabulator page_size --- panel/models/tabulator.py | 2 +- panel/models/tabulator.ts | 21 +++++++++++++++++--- panel/widgets/tables.py | 40 ++++++++++++++++++++------------------- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/panel/models/tabulator.py b/panel/models/tabulator.py index 586c42895e..22eb3a7f7f 100644 --- a/panel/models/tabulator.py +++ b/panel/models/tabulator.py @@ -155,7 +155,7 @@ class DataTabulator(HTMLBox): page = Nullable(Int) - page_size = Int() + page_size = Nullable(Int) max_page = Int() diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 48fdf658eb..ad75693449 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -602,6 +602,21 @@ export class DataTabulatorView extends HTMLBoxView { this.setStyles() if (this.model.pagination) { + if (this.model.page_size == null) { + const table = this.shadow_el.querySelector('.tabulator-table') + const holder = this.shadow_el.querySelector('.tabulator-tableholder') + console.log(table.clientHeight) + let height = 0 + let page_size = null + for (let i = 0; i holder.clientHeight) { + page_size = i+1 + break + } + } + this.model.page_size = page_size + } this.setMaxPage() this.tabulator.setPage(this.model.page) } @@ -664,7 +679,7 @@ export class DataTabulatorView extends HTMLBoxView { layout: this.getLayout(), pagination: this.model.pagination != null, paginationMode: this.model.pagination, - paginationSize: this.model.page_size, + paginationSize: this.model.page_size || 20, paginationInitialPage: 1, groupBy: this.groupBy, rowFormatter: (row: any) => this._render_row(row), @@ -1342,7 +1357,7 @@ export namespace DataTabulator { layout: p.Property max_page: p.Property page: p.Property - page_size: p.Property + page_size: p.Property pagination: p.Property select_mode: p.Property selectable_rows: p.Property @@ -1388,7 +1403,7 @@ export class DataTabulator extends HTMLBox { max_page: [ Float, 0 ], pagination: [ Nullable(Str), null ], page: [ Float, 0 ], - page_size: [ Float, 0 ], + page_size: [ Nullable(Float), null ], select_mode: [ Any, true ], selectable_rows: [ Nullable(List(Float)), null ], source: [ Ref(ColumnDataSource) ], diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index dca0422c00..c1bd0e8a86 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1090,7 +1090,7 @@ class Tabulator(BaseTable): page = param.Integer(default=1, doc=""" Currently selected page (indexed starting at 1), if pagination is enabled.""") - page_size = param.Integer(default=20, bounds=(1, None), doc=""" + page_size = param.Integer(default=None, bounds=(1, None), doc=""" Number of rows to render per page, if pagination is enabled.""") row_content = param.Callable(doc=""" @@ -1255,7 +1255,7 @@ def _process_event(self, event) -> None: event_col = self._renamed_cols.get(event.column, event.column) if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or 20 event.row = event.row+(self.page-1)*nrows idx = self._index_mapping.get(event.row, event.row) @@ -1343,7 +1343,7 @@ def _get_data(self): import pandas as pd df = self._filter_dataframe(self.value) df = self._sort_df(df) - nrows = self.page_size + nrows = self.page_size or 20 start = (self.page-1)*nrows page_df = df.iloc[start: start+nrows] @@ -1383,8 +1383,9 @@ def _get_style_data(self, recompute=True): return {} offset = 1 + len(self.indexes) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content)) if self.pagination == 'remote': - start = (self.page-1)*self.page_size - end = start + self.page_size + page_size = self.page_size or 20 + start = (self.page - 1) * page_size + end = start + page_size # Map column indexes in the data to indexes after frozen_columns are applied column_mapper = {} @@ -1428,7 +1429,7 @@ def _get_selectable(self): return None df = self._processed if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or 20 start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] return self.selectable_rows(df) @@ -1445,7 +1446,7 @@ def _get_children(self, old={}): from ..pane import panel df = self._processed if self.pagination == 'remote': - nrows = self.page_size + nrows = self.page_size or 20 start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] children = {} @@ -1518,7 +1519,7 @@ def _update_children(self, *events): def _stream(self, stream, rollover=None, follow=True): if self.pagination == 'remote': length = self._length - nrows = self.page_size + nrows = self.page_size or 20 max_page = max(length//nrows + bool(length%nrows), 1) if self.page != max_page: return @@ -1532,7 +1533,7 @@ def stream(self, stream_value, rollover=None, reset_index=True, follow=True): self._apply_update([], {'follow': follow}, model, ref) if follow and self.pagination: length = self._length - nrows = self.page_size + nrows = self.page_size or 20 self.page = max(length//nrows + bool(length%nrows), 1) super().stream(stream_value, rollover, reset_index) if follow and self.pagination: @@ -1545,8 +1546,8 @@ def _patch(self, patch): self._update_cds() return if self.pagination == 'remote': - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or 20 + start = (self.page - 1) * nrows end = start+nrows filtered = {} for c, values in patch.items(): @@ -1591,7 +1592,7 @@ def _update_selectable(self): def _update_max_page(self): length = self._length - nrows = self.page_size + nrows = self.page_size or 20 max_page = max(length//nrows + bool(length%nrows), 1) self.param.page.bounds = (1, max_page) for ref, (model, _) in self._models.items(): @@ -1615,8 +1616,8 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): indices.append((ind, iloc)) except KeyError: continue - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or 20 + start = (self.page - 1) * nrows end = start+nrows p_range = self._processed.index[start:end] kwargs['indices'] = [iloc - start for ind, iloc in indices @@ -1632,8 +1633,8 @@ def _update_column(self, column: str, array: np.ndarray): with pd.option_context('mode.chained_assignment', None): self._processed[column] = array return - nrows = self.page_size - start = (self.page-1)*nrows + nrows = self.page_size or 20 + start = (self.page - 1) * nrows end = start+nrows index = self._processed.iloc[start:end].index.values self.value.loc[index, column] = array @@ -1653,7 +1654,7 @@ def _update_selection(self, indices: list[int] | SelectionEvent): ilocs = [] if indices.flush else self.selection.copy() indices = indices.indices - nrows = self.page_size + nrows = self.page_size or 20 start = (self.page-1)*nrows index = self._processed.iloc[[start+ind for ind in indices]].index for v in index.values: @@ -1678,7 +1679,8 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: properties['indexes'] = self.indexes if self.pagination: length = self._length - properties['max_page'] = max(length//self.page_size + bool(length%self.page_size), 1) + page_size = self.page_size or 20 + properties['max_page'] = max(length//page_size + bool(length % page_size), 1) if isinstance(self.selectable, str) and self.selectable.startswith('checkbox'): properties['select_mode'] = 'checkbox' else: @@ -1720,7 +1722,7 @@ def _get_model( model.children = self._get_model_children( child_panels, doc, root, parent, comm ) - self._link_props(model, ['page', 'sorters', 'expanded', 'filters'], doc, root, comm) + self._link_props(model, ['page', 'sorters', 'expanded', 'filters', 'page_size'], doc, root, comm) self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm) return model From 00d93975b437ad55ef21e91fea518ba95b1e9be6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 13 Jul 2024 11:27:59 +0200 Subject: [PATCH 2/4] Improvements, tests and docs --- examples/reference/widgets/Tabulator.ipynb | 5 ++- panel/models/tabulator.ts | 36 ++++++++++++++-------- panel/tests/ui/widgets/test_tabulator.py | 23 ++++++++++++++ panel/widgets/tables.py | 31 ++++++++++--------- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index c061c224ef..403ba414fe 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -49,9 +49,10 @@ "* **`header_filters`** (`boolean`/`dict`): A boolean enabling filters in the column headers or a dictionary providing filter definitions for specific columns.\n", "* **`hidden_columns`** (`list`): List of columns to hide.\n", "* **`hierarchical`** (boolean, default=False): Whether to render multi-indexes as hierarchical index (note hierarchical must be enabled during instantiation and cannot be modified later)\n", + "* **`initial_page_size`** (`int`, `default=20`): If pagination is enabled and `page_size` this determines the initial size of each page before rendering.\n", "* **`layout`** (`str`, `default='fit_data_table'`): Describes the column layout mode with one of the following options `'fit_columns'`, `'fit_data'`, `'fit_data_stretch'`, `'fit_data_fill'`, `'fit_data_table'`. \n", "* **`page`** (`int`, `default=1`): Current page, if pagination is enabled.\n", - "* **`page_size`** (`int`, `default=20`): Number of rows on each page, if pagination is enabled.\n", + "* **`page_size`** (`int | None`, `default=None`): Number of rows on each page, if pagination is enabled. By default the number of rows is automatically determined based on the number of rows that fit on screen. If None the initial amount of data is determined by the `initial_page_size`. \n", "* **`pagination`** (`str`, `default=None`): Set to `'local` or `'remote'` to enable pagination; by default pagination is disabled with the value set to `None`.\n", "* **`row_content`** (`callable`): A function that receives the expanded row (`pandas.Series`) as input and should return a Panel object to render into the expanded region below the row.\n", "* **`selection`** (`list`): The currently selected rows as a list of integer indexes.\n", @@ -822,6 +823,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "Note that the default `page_size` is None, which means it will measure the height of the rows and try to fit the appropriate number of rows into the available space. To override the number of rows sent to the frontend before the measurement has taken place set the `initial_page_size`.\n", + "\n", "Contrary to the `'remote'` option, `'local'` pagination transfers all of the data but still allows to display it on multiple pages:" ] }, diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index ad75693449..68cd15fcc1 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -603,19 +603,29 @@ export class DataTabulatorView extends HTMLBoxView { if (this.model.pagination) { if (this.model.page_size == null) { - const table = this.shadow_el.querySelector('.tabulator-table') - const holder = this.shadow_el.querySelector('.tabulator-tableholder') - console.log(table.clientHeight) - let height = 0 - let page_size = null - for (let i = 0; i holder.clientHeight) { - page_size = i+1 - break - } - } - this.model.page_size = page_size + const table = this.shadow_el.querySelector('.tabulator-table') + const holder = this.shadow_el.querySelector('.tabulator-tableholder') + if (table != null && holder != null) { + const table_height = holder.clientHeight + let height = 0 + let page_size = null + const heights = [] + for (let i = 0; i table_height) { + page_size = i + break + } + } + if (height < table_height) { + page_size = table.children.length + const remaining = table_height - height + page_size += Math.floor(remaining / Math.min(...heights)) + } + this.model.page_size = page_size + } } this.setMaxPage() this.tabulator.setPage(this.model.page) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 09b5b61c8e..19603ef040 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3370,6 +3370,29 @@ def test_tabulator_update_hidden_columns(page): ), page) +def test_tabulator_remote_pagination_auto_page_size_grow(page, df_mixed): + nrows, ncols = df_mixed.shape + widget = Tabulator(df_mixed, pagination='remote', initial_page_size=1, height=200) + + serve_component(page, widget) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + wait_until(lambda: widget.page_size == 4, page) + + +def test_tabulator_remote_pagination_auto_page_size_shrink(page, df_mixed): + nrows, ncols = df_mixed.shape + widget = Tabulator(df_mixed, pagination='remote', initial_page_size=10, height=150) + + serve_component(page, widget) + + expect(page.locator('.tabulator-table')).to_have_count(1) + + wait_until(lambda: widget.page_size == 3, page) + + + class Test_RemotePagination: @pytest.fixture(autouse=True) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index c1bd0e8a86..0ea642f3ae 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1084,6 +1084,9 @@ class Tabulator(BaseTable): 'fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table', 'fit_columns']) + initial_page_size = param.Integer(default=20, bounds=(1, None), doc=""" + Initial page size if page_size is None and therefore automatically set.""") + pagination = param.ObjectSelector(default=None, allow_None=True, objects=['local', 'remote']) @@ -1162,7 +1165,7 @@ class Tabulator(BaseTable): 'selection': None, 'row_content': None, 'row_height': None, 'text_align': None, 'embed_content': None, 'header_align': None, 'header_filters': None, 'styles': 'cell_styles', - 'title_formatters': None, 'sortable': None + 'title_formatters': None, 'sortable': None, 'initial_page_size': None } # Determines the maximum size limits beyond which (local, remote) @@ -1255,7 +1258,7 @@ def _process_event(self, event) -> None: event_col = self._renamed_cols.get(event.column, event.column) if self.pagination == 'remote': - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size event.row = event.row+(self.page-1)*nrows idx = self._index_mapping.get(event.row, event.row) @@ -1343,7 +1346,7 @@ def _get_data(self): import pandas as pd df = self._filter_dataframe(self.value) df = self._sort_df(df) - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows page_df = df.iloc[start: start+nrows] @@ -1383,7 +1386,7 @@ def _get_style_data(self, recompute=True): return {} offset = 1 + len(self.indexes) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content)) if self.pagination == 'remote': - page_size = self.page_size or 20 + page_size = self.page_size or self.initial_page_size start = (self.page - 1) * page_size end = start + page_size @@ -1429,7 +1432,7 @@ def _get_selectable(self): return None df = self._processed if self.pagination == 'remote': - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] return self.selectable_rows(df) @@ -1446,7 +1449,7 @@ def _get_children(self, old={}): from ..pane import panel df = self._processed if self.pagination == 'remote': - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows df = df.iloc[start:(start+nrows)] children = {} @@ -1519,7 +1522,7 @@ def _update_children(self, *events): def _stream(self, stream, rollover=None, follow=True): if self.pagination == 'remote': length = self._length - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) if self.page != max_page: return @@ -1533,7 +1536,7 @@ def stream(self, stream_value, rollover=None, reset_index=True, follow=True): self._apply_update([], {'follow': follow}, model, ref) if follow and self.pagination: length = self._length - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size self.page = max(length//nrows + bool(length%nrows), 1) super().stream(stream_value, rollover, reset_index) if follow and self.pagination: @@ -1546,7 +1549,7 @@ def _patch(self, patch): self._update_cds() return if self.pagination == 'remote': - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page - 1) * nrows end = start+nrows filtered = {} @@ -1592,7 +1595,7 @@ def _update_selectable(self): def _update_max_page(self): length = self._length - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size max_page = max(length//nrows + bool(length%nrows), 1) self.param.page.bounds = (1, max_page) for ref, (model, _) in self._models.items(): @@ -1616,7 +1619,7 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): indices.append((ind, iloc)) except KeyError: continue - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page - 1) * nrows end = start+nrows p_range = self._processed.index[start:end] @@ -1633,7 +1636,7 @@ def _update_column(self, column: str, array: np.ndarray): with pd.option_context('mode.chained_assignment', None): self._processed[column] = array return - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page - 1) * nrows end = start+nrows index = self._processed.iloc[start:end].index.values @@ -1654,7 +1657,7 @@ def _update_selection(self, indices: list[int] | SelectionEvent): ilocs = [] if indices.flush else self.selection.copy() indices = indices.indices - nrows = self.page_size or 20 + nrows = self.page_size or self.initial_page_size start = (self.page-1)*nrows index = self._processed.iloc[[start+ind for ind in indices]].index for v in index.values: @@ -1679,7 +1682,7 @@ def _get_properties(self, doc: Document) -> dict[str, Any]: properties['indexes'] = self.indexes if self.pagination: length = self._length - page_size = self.page_size or 20 + page_size = self.page_size or self.initial_page_size properties['max_page'] = max(length//page_size + bool(length % page_size), 1) if isinstance(self.selectable, str) and self.selectable.startswith('checkbox'): properties['select_mode'] = 'checkbox' From d1f791671088342c0f50ea128c225cc96bc51825 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 13 Jul 2024 11:40:43 +0200 Subject: [PATCH 3/4] Fix lint --- panel/models/tabulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 68cd15fcc1..3ed3900403 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -603,8 +603,8 @@ export class DataTabulatorView extends HTMLBoxView { if (this.model.pagination) { if (this.model.page_size == null) { - const table = this.shadow_el.querySelector('.tabulator-table') - const holder = this.shadow_el.querySelector('.tabulator-tableholder') + const table = this.shadow_el.querySelector(".tabulator-table") + const holder = this.shadow_el.querySelector(".tabulator-tableholder") if (table != null && holder != null) { const table_height = holder.clientHeight let height = 0 From aa4c497abde1cba1248b2a530559006e685810b8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 1 Aug 2024 22:45:29 +0200 Subject: [PATCH 4/4] Apply suggestions from code review --- panel/models/tabulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 3ed3900403..c063b1904c 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -620,7 +620,7 @@ export class DataTabulatorView extends HTMLBoxView { } } if (height < table_height) { - page_size = table.children.length + page_size = table.children.length const remaining = table_height - height page_size += Math.floor(remaining / Math.min(...heights)) }