Using Tabulator¶
Tabulator is the preferred widget for displaying DataFrames in Panel apps — sortable, filterable, paginated. Requires pn.extension("tabulator").
Basic Setup¶
self._table = pn.widgets.Tabulator(
value=df,
sizing_mode="stretch_width",
layout="fit_columns",
disabled=True, # prevent cell editing
show_index=False,
theme="materialize",
pagination="remote",
page_size=15,
formatters={
"Revenue": {"type": "money", "symbol": "$", "precision": 0},
"Cost": {"type": "money", "symbol": "$", "precision": 0},
},
)
layout="fit_columns"fills available width. Best option for responsive layouts.disabled=Trueprevents editing — setFalsefor editable tables.- Prefer Tabulator formatters/editors over Bokeh types.
Widget-Based Filtering with add_filter¶
Wire sidebar widgets directly as table filters — Tabulator watches widget values and shows/hides rows automatically. No manual filtering needed.
region_filter = pmui.CheckBoxGroup(value=list(REGIONS), options=REGIONS)
product_filter = pmui.CheckBoxGroup(value=list(PRODUCTS), options=PRODUCTS)
# Must be called AFTER table has data (value is set),
# otherwise raises AttributeError: 'NoneType' has no attribute 'columns'
table.add_filter(region_filter, "Region")
table.add_filter(product_filter, "Product")
Filter behavior depends on the widget value type: a list matches any item in the list, a tuple is interpreted as a range (inclusive), a scalar checks for equality.
Use table.current_view to inspect the currently visible (filtered + sorted) DataFrame.
Function-Based Filtering¶
For complex filtering, pass a bound function:
search = pn.widgets.TextInput(name="Search", value="")
def contains_filter(df, pattern, column):
if not pattern:
return df
return df[df[column].str.contains(pattern, case=False)]
table.add_filter(pn.bind(contains_filter, pattern=search, column="Name"))
Checkbox Selection¶
Use selectable="checkbox" for row selection with checkboxes. Watch the selection parameter for changes and read selected_dataframe for the selected rows.
self._table = pn.widgets.Tabulator(
...,
selectable="checkbox",
)
self._table.param.watch(self._on_selection, "selection")
def _on_selection(self, event):
selected = self._table.selected_dataframe
if selected is not None and not selected.empty:
# Use selected rows to filter charts, update KPIs, etc.
...
Row Content (Expandable Details)¶
Use row_content to render an expandable detail region below each row. Pass a function that receives a pandas.Series (the row) and returns a Panel object.
def row_detail(row):
return pn.Column(
f"**{row['Name']}** — {row['Region']}",
pn.pane.HoloViews(
df[df["Name"] == row["Name"]].hvplot.line(x="date", y="revenue"),
height=200,
),
)
self._table = pn.widgets.Tabulator(
value=df,
row_content=row_detail,
sizing_mode="stretch_width",
)
- The function is called lazily when the user expands a row (clicks the
+icon). - Set
embed_content=Trueto pre-render all row content on load instead of lazily — useful when there are few rows and you want instant expansion, but expensive with many rows.