Skip to content

Commit

Permalink
Add support for automatically determining optimal Tabulator page_size (
Browse files Browse the repository at this point in the history
…#6978)

* Add support for automatically determining optimal Tabulator page_size

* Improvements, tests and docs

* Fix lint

* Apply suggestions from code review
  • Loading branch information
philippjfr authored Aug 1, 2024
1 parent e2afe41 commit 7c90f7b
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 25 deletions.
5 changes: 4 additions & 1 deletion examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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:"
]
},
Expand Down
2 changes: 1 addition & 1 deletion panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class DataTabulator(HTMLBox):

page = Nullable(Int)

page_size = Int()
page_size = Nullable(Int)

max_page = Int()

Expand Down
31 changes: 28 additions & 3 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,31 @@ 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")
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.children.length; i++) {
const row_height = table.children[i].clientHeight
heights.push(row_height)
height += row_height
if (height > 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)
}
Expand Down Expand Up @@ -665,7 +690,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),
Expand Down Expand Up @@ -1351,7 +1376,7 @@ export namespace DataTabulator {
layout: p.Property<typeof TableLayout["__type__"]>
max_page: p.Property<number>
page: p.Property<number>
page_size: p.Property<number>
page_size: p.Property<number | null>
pagination: p.Property<string | null>
select_mode: p.Property<any>
selectable_rows: p.Property<number[] | null>
Expand Down Expand Up @@ -1397,7 +1422,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) ],
Expand Down
23 changes: 23 additions & 0 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3388,6 +3388,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)
Expand Down
45 changes: 25 additions & 20 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,13 +1090,16 @@ 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'])

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="""
Expand Down Expand Up @@ -1168,7 +1171,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)
Expand Down Expand Up @@ -1261,7 +1264,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 self.initial_page_size
event.row = event.row+(self.page-1)*nrows

idx = self._index_mapping.get(event.row, event.row)
Expand Down Expand Up @@ -1349,7 +1352,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 self.initial_page_size
start = (self.page-1)*nrows

page_df = df.iloc[start: start+nrows]
Expand Down Expand Up @@ -1389,8 +1392,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 self.initial_page_size
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 = {}
Expand Down Expand Up @@ -1434,7 +1438,7 @@ def _get_selectable(self):
return None
df = self._processed
if self.pagination == 'remote':
nrows = self.page_size
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)
Expand All @@ -1451,7 +1455,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 self.initial_page_size
start = (self.page-1)*nrows
df = df.iloc[start:(start+nrows)]
children = {}
Expand Down Expand Up @@ -1524,7 +1528,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 self.initial_page_size
max_page = max(length//nrows + bool(length%nrows), 1)
if self.page != max_page:
return
Expand All @@ -1538,7 +1542,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 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:
Expand All @@ -1551,8 +1555,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 self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
filtered = {}
for c, values in patch.items():
Expand Down Expand Up @@ -1597,7 +1601,7 @@ def _update_selectable(self):

def _update_max_page(self):
length = self._length
nrows = self.page_size
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():
Expand All @@ -1621,8 +1625,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 self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
p_range = self._processed.index[start:end]
kwargs['indices'] = [iloc - start for ind, iloc in indices
Expand All @@ -1638,8 +1642,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 self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
index = self._processed.iloc[start:end].index.values
self.value.loc[index, column] = array
Expand All @@ -1659,7 +1663,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 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:
Expand All @@ -1684,7 +1688,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 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'
else:
Expand Down Expand Up @@ -1726,7 +1731,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

Expand Down

0 comments on commit 7c90f7b

Please sign in to comment.