Skip to content

Commit

Permalink
Correctly map Tabulator expanded indexes when paginated, filtered and…
Browse files Browse the repository at this point in the history
… sorted (#7103)
  • Loading branch information
philippjfr authored Aug 8, 2024
1 parent c255c6d commit 01f0fba
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 60 deletions.
21 changes: 14 additions & 7 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export class DataTabulatorView extends HTMLBoxView {
for (const row of this.tabulator.rowManager.getRows()) {
if (row.cells.length > 0) {
const index = row.data._index
const icon = this.model.expanded.indexOf(index) < 0 ? "" : ""
const icon = this.model.expanded.includes(index) ? "" : ""
row.cells[1].element.innerText = icon
}
}
Expand Down Expand Up @@ -741,10 +741,17 @@ export class DataTabulatorView extends HTMLBoxView {
const new_children = await this.build_child_views()
resolve(new_children)
}).then((new_children) => {
for (const r of this.model.expanded) {
const row = this.tabulator.getRow(r)
const rows = this.tabulator.getRows()
const lookup = new Map()
for (const row of rows) {
const index = row._row?.data._index
if (this.model.children.get(index) == null) {
if (index != null) {
lookup.set(index, row)
}
}
for (const index of this.model.expanded) {
const row = lookup.get(index)
if (!this.model.children.has(index)) {
continue
}
const model = this.model.children.get(index)
Expand Down Expand Up @@ -798,10 +805,10 @@ export class DataTabulatorView extends HTMLBoxView {
_update_expand(cell: any): void {
const index = cell._cell.row.data._index
const expanded = [...this.model.expanded]
const exp_index = expanded.indexOf(index)
if (exp_index < 0) {
if (!expanded.includes(index)) {
expanded.push(index)
} else {
const exp_index = expanded.indexOf(index)
const removed = expanded.splice(exp_index, 1)[0]
const model = this.model.children.get(removed)
if (model != null) {
Expand All @@ -812,7 +819,7 @@ export class DataTabulatorView extends HTMLBoxView {
}
}
this.model.expanded = expanded
if (expanded.indexOf(index) < 0) {
if (!expanded.includes(index)) {
return
}
let ready = true
Expand Down
94 changes: 93 additions & 1 deletion panel/tests/widgets/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,99 @@ def test_tabulator_expanded_content(document, comm):
assert row2.text == "&lt;pre&gt;2.0&lt;/pre&gt;"


def test_tabulator_remote_paginated_expanded_content(document, comm):
df = makeMixedDataFrame()

table = Tabulator(
df, expanded=[0, 4], row_content=lambda r: r.A, pagination='remote', page_size=3
)

model = table.get_root(document, comm)

assert len(model.children) == 1
assert 0 in model.children
row0 = model.children[0]
assert row0.text == "&lt;pre&gt;0.0&lt;/pre&gt;"

table.page = 2

assert len(model.children) == 1
assert 1 in model.children
row1 = model.children[1]
assert row1.text == "&lt;pre&gt;4.0&lt;/pre&gt;"


def test_tabulator_remote_sorted_paginated_expanded_content(document, comm):
df = makeMixedDataFrame()

table = Tabulator(
df, expanded=[0, 1], row_content=lambda r: r.A, pagination='remote', page_size=2,
sorters = [{'field': 'A', 'sorter': 'number', 'dir': 'desc'}], page=3
)

model = table.get_root(document, comm)

assert len(model.children) == 1
assert 0 in model.children
row0 = model.children[0]
assert row0.text == "&lt;pre&gt;0.0&lt;/pre&gt;"

table.page = 2

assert len(model.children) == 1
assert 1 in model.children
row1 = model.children[1]
assert row1.text == "&lt;pre&gt;1.0&lt;/pre&gt;"

table.expanded = [0, 1, 2]

assert len(model.children) == 2
assert 0 in model.children
row0 = model.children[0]
assert row0.text == "&lt;pre&gt;2.0&lt;/pre&gt;"


@pytest.mark.parametrize('pagination', ['local', 'remote', None])
def test_tabulator_filtered_expanded_content(document, comm, pagination):
df = makeMixedDataFrame()

table = Tabulator(
df,
expanded=[0, 1, 2, 3],
filters=[{'field': 'B', 'sorter': 'number', 'type': '=', 'value': '1.0'}],
pagination=pagination,
row_content=lambda r: r.A,
)

model = table.get_root(document, comm)

assert len(model.children) == 2

assert 0 in model.children
row0 = model.children[0]
assert row0.text == "&lt;pre&gt;1.0&lt;/pre&gt;"

assert 1 in model.children
row1 = model.children[1]
assert row1.text == "&lt;pre&gt;3.0&lt;/pre&gt;"

model.expanded = [0]
assert table.expanded == [1]

table.filters = [{'field': 'B', 'sorter': 'number', 'type': '=', 'value': '0'}]

assert not model.expanded
assert table.expanded == [1]

table.expanded = [0, 1]

assert len(model.children) == 1

assert 0 in model.children
row0 = model.children[0]
assert row0.text == "&lt;pre&gt;0.0&lt;/pre&gt;"


def test_tabulator_index_column(document, comm):
df = pd.DataFrame({
'int': [1, 2, 3],
Expand Down Expand Up @@ -912,7 +1005,6 @@ def test_tabulator_empty_table(document, comm):

assert table.value.shape == value_df.shape


def test_tabulator_sorters_unnamed_index(document, comm):
df = pd.DataFrame(np.random.rand(10, 4))
assert df.columns.dtype == np.int64
Expand Down
124 changes: 72 additions & 52 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,7 @@ def __init__(self, value=None, **params):
self.style = None
self._computed_styler = None
self._child_panels = {}
self._indexed_children = {}
self._explicit_pagination = 'pagination' in params
self._on_edit_callbacks = []
self._on_click_callbacks = {}
Expand Down Expand Up @@ -1302,6 +1303,11 @@ def _cleanup(self, root: Model | None = None) -> None:
p._cleanup(root)
super()._cleanup(root)

def _process_events(self, events: dict[str, Any]) -> None:
if 'expanded' in events:
self._update_expanded(events.pop('expanded'))
return super()._process_events(events)

def _process_event(self, event) -> None:
if event.event_name == 'selection-change':
if self.pagination == 'remote':
Expand Down Expand Up @@ -1498,27 +1504,50 @@ def _update_style(self, recompute=True):
for ref, (m, _) in self._models.items():
self._apply_update([], msg, m, ref)

def _get_children(self, old={}):
def _get_children(self):
if self.row_content is None or self.value is None:
return {}
return {}, [], []
from ..pane import panel
df = self._processed
if self.pagination == 'remote':
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
df = df.iloc[start:(start+nrows)]
children = {}
for i in (range(len(df)) if self.embed_content else self.expanded):
if i in old:
children[i] = old[i]
else:
children[i] = panel(self.row_content(df.iloc[i]))
return children

def _get_model_children(self, panels, doc, root, parent, comm=None):
indexed_children, children = {}, {}
expanded = []
if self.embed_content:
for i in range(len(df)):
expanded.append(i)
idx = df.index[i]
if idx in self._indexed_children:
child = self._indexed_children[idx]
else:
child = panel(self.row_content(df.iloc[i]))
indexed_children[idx] = children[i] = child
else:
for i in self.expanded:
idx = self.value.index[i]
if idx in self._indexed_children:
child = self._indexed_children[idx]
else:
child = panel(self.row_content(self.value.iloc[i]))
try:
loc = df.index.get_loc(idx)
except KeyError:
continue
expanded.append(loc)
indexed_children[idx] = children[loc] = child
removed = [
child for idx, child in self._indexed_children.items()
if idx not in indexed_children
]
self._indexed_children = indexed_children
return children, removed, expanded

def _get_model_children(self, doc, root, parent, comm=None):
ref = root.ref['id']
models = {}
for i, p in panels.items():
for i, p in self._child_panels.items():
if ref in p._models:
model = p._models[ref][0]
else:
Expand All @@ -1528,35 +1557,20 @@ def _get_model_children(self, panels, doc, root, parent, comm=None):
return models

def _update_children(self, *events):
cleanup, reuse = set(), set()
page_events = ('page', 'page_size', 'pagination')
old_panels = self._child_panels
for event in events:
if event.name == 'expanded' and len(events) == 1:
if self.embed_content:
cleanup = set()
reuse = set(old_panels)
else:
cleanup = set(event.old) - set(event.new)
reuse = set(event.old) & set(event.new)
elif (
(event.name == 'value' and self._indexes_changed(event.old, event.new)) or
(event.name in page_events and not self._updating) or
(self.pagination == 'remote' and event.name == 'sorters')
):
if event.name == 'value' and self._indexes_changed(event.old, event.new):
self.expanded = []
self._indexed_children.clear()
return
self._child_panels = child_panels = self._get_children(
{i: old_panels[i] for i in reuse}
)
elif event.name == 'row_content':
self._indexed_children.clear()
self._child_panels, removed, expanded = self._get_children()
for ref, (m, _) in self._models.items():
root, doc, comm = state._views[ref][1:]
for idx in cleanup:
old_panels[idx]._cleanup(root)
children = self._get_model_children(
child_panels, doc, root, m, comm
)
msg = {'children': children}
for child_panel in removed:
child_panel._cleanup(root)
children = self._get_model_children(doc, root, m, comm)
msg = {'expanded': expanded, 'children': children}
self._apply_update([], msg, m, ref)

@updating
Expand Down Expand Up @@ -1689,32 +1703,39 @@ def _update_column(self, column: str, array: np.ndarray):
with pd.option_context('mode.chained_assignment', None):
self._processed.loc[index, column] = array

def _update_selection(self, indices: list[int] | SelectionEvent):
if isinstance(indices, list):
selected = True
ilocs = []
else: # SelectionEvent
selected = indices.selected
ilocs = [] if indices.flush else self.selection.copy()
indices = indices.indices

def _map_indexes(self, indexes, existing=[], add=True):
if self.pagination == 'remote':
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
else:
start = 0
index = self._processed.iloc[[start+ind for ind in indices]].index
ilocs = list(existing)
index = self._processed.iloc[[start+ind for ind in indexes]].index
for v in index.values:
try:
iloc = self.value.index.get_loc(v)
self._validate_iloc(v, iloc)
except KeyError:
continue
if selected:
if add:
ilocs.append(iloc)
elif iloc in ilocs:
ilocs.remove(iloc)
ilocs = list(dict.fromkeys(ilocs))
return list(dict.fromkeys(ilocs))

def _update_expanded(self, expanded):
self.expanded = self._map_indexes(expanded)

def _update_selection(self, indices: list[int] | SelectionEvent):
if isinstance(indices, list):
selected = True
ilocs = []
else:
selected = indices.selected
ilocs = [] if indices.flush else self.selection.copy()
indices = indices.indices

ilocs = self._map_indexes(indices, ilocs, add=selected)
if isinstance(self.selectable, int) and not isinstance(self.selectable, bool):
ilocs = ilocs[len(ilocs) - self.selectable:]
self.selection = ilocs
Expand Down Expand Up @@ -1765,10 +1786,9 @@ def _get_model(
)
model = super()._get_model(doc, root, parent, comm)
root = root or model
self._child_panels = child_panels = self._get_children()
model.children = self._get_model_children(
child_panels, doc, root, parent, comm
)
self._child_panels, removed, expanded = self._get_children()
model.expanded = expanded
model.children = self._get_model_children(doc, root, parent, 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 01f0fba

Please sign in to comment.