Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address issues with Tabulator embed_content and optimize row expansion #7364

Merged
merged 10 commits into from
Oct 15, 2024
2 changes: 2 additions & 0 deletions panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ class DataTabulator(HTMLBox):

editable = Bool(default=True)

embed_content = Bool(default=False)

expanded = List(Int)

filename = String(default="table.csv")
Expand Down
97 changes: 53 additions & 44 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {undisplay} from "@bokehjs/core/dom"
import {display, undisplay} from "@bokehjs/core/dom"
import {sum} from "@bokehjs/core/util/arrayable"
import {isArray, isBoolean, isString, isNumber} from "@bokehjs/core/util/types"
import {ModelEvent} from "@bokehjs/core/bokeh_events"
Expand All @@ -8,7 +8,6 @@ import type * as p from "@bokehjs/core/properties"
import type {LayoutDOM} from "@bokehjs/models/layouts/layout_dom"
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"
import {TableColumn} from "@bokehjs/models/widgets/tables"
import type {UIElementView} from "@bokehjs/models/ui/ui_element"
import type {Attrs} from "@bokehjs/core/types"

import {debounce} from "debounce"
Expand Down Expand Up @@ -350,6 +349,7 @@ export class DataTabulatorView extends HTMLBoxView {
container: HTMLDivElement | null = null
_tabulator_cell_updating: boolean=false
_updating_page: boolean = false
_updating_expanded: boolean = false
_updating_sort: boolean = false
_selection_updating: boolean = false
_last_selected_row: any = null
Expand Down Expand Up @@ -393,7 +393,7 @@ export class DataTabulatorView extends HTMLBoxView {
this.tabulator.download(ftype, this.model.filename)
})

this.on_change(children, () => this.renderChildren(false))
this.on_change(children, () => this.renderChildren())

this.on_change(expanded, () => {
// The first cell is the cell of the frozen _index column.
Expand All @@ -411,6 +411,12 @@ export class DataTabulatorView extends HTMLBoxView {
row.cells[1].element.innerText = icon
}
}
// If content is embedded, views may not have been
// rendered so if expanded is updated server side
// we have to trigger a render
if (this.model.embed_content && !this._updating_expanded) {
this.renderChildren()
}
})

this.on_change(cell_styles, () => {
Expand Down Expand Up @@ -773,7 +779,7 @@ export class DataTabulatorView extends HTMLBoxView {
frozenRows: (row: any) => {
return (this.model.frozen_rows.length > 0) ? this.model.frozen_rows.includes(row._row.data._index) : false
},
rowFormatter: (row: any) => this._render_row(row),
rowFormatter: (row: any) => this._render_row(row, false),
}
if (this.model.pagination === "remote") {
configuration.ajaxURL = "http://panel.pyviz.org"
Expand Down Expand Up @@ -816,33 +822,23 @@ export class DataTabulatorView extends HTMLBoxView {
return lookup
}

renderChildren(all: boolean = true): void {
new Promise(async (resolve: any) => {
let new_children = await this.build_child_views()
if (all) {
new_children = this.child_views
}
resolve(new_children)
}).then((new_children) => {
renderChildren(): void {
void new Promise(async () => {
await this.build_child_views()
const lookup = this.row_index
for (const index of this.model.expanded) {
const expanded = this.model.expanded
for (const index of expanded) {
const model = this.get_child(index)
const row = lookup.get(index)
const view = model == null ? null : this._child_views.get(model)
if (view != null) {
const render = (new_children as UIElementView[]).includes(view)
this._render_row(row, false, render)
this._render_row(row, index === expanded[expanded.length-1])
}
}
this._update_children()
if (this.tabulator.rowManager.renderer != null) {
this.tabulator.rowManager.adjustTableSize()
}
this.invalidate_layout()
})
}

_render_row(row: any, resize: boolean = true, render: boolean = true): void {
_render_row(row: any, resize: boolean = true): void {
const index = row._row?.data._index
if (!this.model.expanded.includes(index)) {
return
Expand All @@ -852,33 +848,42 @@ export class DataTabulatorView extends HTMLBoxView {
if (view == null) {
return
}
const rowEl = row.getElement()
const style = getComputedStyle(this.tabulator.element.children[1].children[0])
const bg = style.backgroundColor
const neg_margin = rowEl.style.paddingLeft ? `-${rowEl.style.paddingLeft}` : "0"
const prev_child = rowEl.children[rowEl.children.length-1]
let viewEl
if (prev_child != null && prev_child.className == "row-content") {
viewEl = prev_child
if (viewEl.children.length && viewEl.children[0] === view.el) {
return
schedule_when(() => {
const rowEl = row.getElement()
const style = getComputedStyle(this.tabulator.element.children[1].children[0])
const bg = style.backgroundColor
const neg_margin = rowEl.style.paddingLeft ? `-${rowEl.style.paddingLeft}` : "0"
const prev_child = rowEl.children[rowEl.children.length-1]
let viewEl
if (prev_child != null && prev_child.className == "row-content") {
viewEl = prev_child
if (viewEl.children.length && viewEl.children[0] === view.el) {
return
}
} else {
viewEl = div({class: "row-content", style: {background_color: bg, margin_left: neg_margin, max_width: "100%", overflow_x: "hidden"}})
rowEl.appendChild(viewEl)
}
} else {
viewEl = div({class: "row-content", style: {background_color: bg, margin_left: neg_margin, max_width: "100%", overflow_x: "hidden"}})
rowEl.appendChild(viewEl)
}
viewEl.appendChild(view.el)
if (render) {
schedule_when(() => {
display(view.el)
viewEl.appendChild(view.el)
if (view.shadow_el.children.length === 0) {
view.render()
view.after_render()
}, () => this.root.has_finished())
}
if (resize) {
this._update_children()
this.tabulator.rowManager.adjustTableSize()
this.invalidate_layout()
}
if (resize) {
this._update_children()
this.resize_table()
}
}, () => this.root.has_finished())
}

resize_table(): void {
if (this.tabulator.rowManager.renderer != null) {
try {
this.tabulator.rowManager.adjustTableSize()
} catch() {}
}
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
this.invalidate_layout()
}

_expand_render(cell: any): string {
Expand All @@ -903,7 +908,9 @@ export class DataTabulatorView extends HTMLBoxView {
}
}
}
this._updating_expanded = true
this.model.expanded = expanded
this._updating_expanded = false
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
if (!expanded.includes(index)) {
return
}
Expand Down Expand Up @@ -1468,6 +1475,7 @@ export namespace DataTabulator {
configuration: p.Property<any>
download: p.Property<boolean>
editable: p.Property<boolean>
embed_content: p.Property<boolean>
expanded: p.Property<number[]>
filename: p.Property<string>
filters: p.Property<any[]>
Expand Down Expand Up @@ -1513,6 +1521,7 @@ export class DataTabulator extends HTMLBox {
columns: [ List(Ref(TableColumn)), [] ],
download: [ Bool, false ],
editable: [ Bool, true ],
embed_content: [ Bool, false ],
expanded: [ List(Float), [] ],
filename: [ Str, "table.csv" ],
filters: [ List(Any), [] ],
Expand Down
37 changes: 29 additions & 8 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1560,8 +1560,13 @@ def test_tabulator_selection_on_multi_index(page, pagination):
wait_until(lambda: widget.selection == [0, 16], page)


def test_tabulator_row_content(page, df_mixed):
widget = Tabulator(df_mixed, row_content=lambda i: f"{i['str']}-row-content")
@pytest.mark.parametrize('embed_content', [False, True])
def test_tabulator_row_content(page, df_mixed, embed_content):
widget = Tabulator(
df_mixed,
row_content=lambda i: f"{i['str']}-row-content",
embed_content=embed_content
)

serve_component(page, widget)

Expand All @@ -1586,17 +1591,22 @@ def test_tabulator_row_content(page, df_mixed):
closables.first.click()

row_content = page.locator(f'text="{df_mixed.iloc[i]["str"]}-row-content"')
expect(row_content).to_have_count(0)
if embed_content:
expect(row_content).not_to_be_visible()
else:
expect(row_content).to_have_count(0)

expected_expanded.remove(i)
wait_until(lambda: widget.expanded == expected_expanded, page)


def test_tabulator_row_content_expand_from_python_init(page, df_mixed):
@pytest.mark.parametrize('embed_content', [False, True])
def test_tabulator_row_content_expand_from_python_init(page, df_mixed, embed_content):
widget = Tabulator(
df_mixed,
row_content=lambda i: f"{i['str']}-row-content",
expanded = [0, 2],
embed_content=embed_content
)

serve_component(page, widget)
Expand All @@ -1614,8 +1624,13 @@ def test_tabulator_row_content_expand_from_python_init(page, df_mixed):
expect(openables).to_have_count(len(df_mixed) - len(widget.expanded))


def test_tabulator_row_content_expand_from_python_after(page, df_mixed):
widget = Tabulator(df_mixed, row_content=lambda i: f"{i['str']}-row-content")
@pytest.mark.parametrize('embed_content', [False, True])
def test_tabulator_row_content_expand_from_python_after(page, df_mixed, embed_content):
widget = Tabulator(
df_mixed,
row_content=lambda i: f"{i['str']}-row-content",
embed_content=embed_content
)

serve_component(page, widget)

Expand All @@ -1638,8 +1653,14 @@ def test_tabulator_row_content_expand_from_python_after(page, df_mixed):
expect(page.locator('text="►"')).to_have_count(len(df_mixed))


def test_tabulator_row_content_expand_after_filtered(page, df_mixed):
table = Tabulator(df_mixed, row_content=lambda e: f"Hello {e.int}", header_filters=True)
@pytest.mark.parametrize('embed_content', [False, True])
def test_tabulator_row_content_expand_after_filtered(page, df_mixed, embed_content):
table = Tabulator(
df_mixed,
row_content=lambda e: f"Hello {e.int}",
header_filters=True,
embed_content=embed_content
)

serve_component(page, table)

Expand Down
29 changes: 18 additions & 11 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1228,8 +1228,8 @@ class Tabulator(BaseTable):

_rename: ClassVar[Mapping[str, str | None]] = {
'selection': None, 'row_content': None, 'row_height': None,
'text_align': None, 'embed_content': None, 'header_align': None,
'header_filters': None, 'header_tooltips': None, 'styles': 'cell_styles',
'text_align': None, 'header_align': None, 'header_filters': None,
'header_tooltips': None, 'styles': 'cell_styles',
'title_formatters': None, 'sortable': None, 'initial_page_size': None
}

Expand Down Expand Up @@ -1536,17 +1536,22 @@ def _get_children(self):
start = (self.page-1)*nrows
df = df.iloc[start:(start+nrows)]
indexed_children, children = {}, {}
expanded = []
if self.embed_content:
for i in range(len(df)):
expanded.append(i)
indexes = list(range(len(df)))
mapped = self._map_indexes(indexes)
expanded = [
i for i, m in zip(indexes, mapped)
if m in self.expanded
]
for i in indexes:
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:
expanded = []
for i in self.expanded:
idx = self.value.index[i]
if idx in self._indexed_children:
Expand Down Expand Up @@ -1579,7 +1584,8 @@ def _get_model_children(self, doc, root, parent, comm=None):
return models

def _update_children(self, *events):
if all(e.name in ('page', 'page_size', 'pagination', 'sorters') for e in events) and self.pagination != 'remote':
page_event = all(e.name in ('page', 'page_size', 'pagination', 'sorters') for e in events)
if (page_event and self.pagination != 'remote'):
return
for event in events:
if event.name == 'value' and self._indexes_changed(event.old, event.new):
Expand All @@ -1590,11 +1596,12 @@ def _update_children(self, *events):
self._indexed_children.clear()
self._child_panels, removed, expanded = self._get_children()
for ref, (m, _) in self._models.copy().items():
root, doc, comm = state._views[ref][1:]
for child_panel in removed:
child_panel._cleanup(root)
children = self._get_model_children(doc, root, m, comm)
msg = {'expanded': expanded, 'children': children}
msg = {'expanded': expanded}
if not self.embed_content or any(e.name == 'row_content' for e in events):
root, doc, comm = state._views[ref][1:]
for child_panel in removed:
child_panel._cleanup(root)
msg['children'] = self._get_model_children(doc, root, m, comm)
self._apply_update([], msg, m, ref)

@updating
Expand Down
Loading