Using Panel¶
Panel is a Python library for building interactive dashboards, data apps, and tools entirely in Python — no JavaScript required. It connects widgets to plots, tables, and text with reactive callbacks, and serves the result as a web application.
Always use a pn.viewable.Viewer class to structure apps. This keeps state, layout, and logic organized and avoids flickering from recreated components.
Contents¶
- References — iterative development, Material UI, HoloViews, custom components, Playwright testing, widget mapping, cleanup
- Viewer Class Pattern
- Widgets and Extensions
- Templates and Layouts
- Serving Workflow
- Performance
- Plotting Integration
- Component Gotchas
References¶
Read these for specialized topics. Each is a standalone document you can load with the view tool.
- Iterating on Panel Apps — serve with logging, screenshot with Playwright, review and debug agentic loop
- Mapping Widgets — which Panel/pmui widget to use for each Param type, with
.from_param()patterns - Building Custom Components — building JSComponent, ReactComponent, AnyWidgetComponent, and MaterialUIComponent; CDN selection, event handling, state sync lifecycle
- Applying Material UI —
pmui.Pagetemplate,Container/Gridlayouts, centering, component gotchas - Branding Material UI —
theme_configpalettes, typography, icons, brand assets, chart theming - Interacting with HoloViews — DynamicMap for preserving zoom/pan,
pn.pane.HoloViewsconfig (theme=,linked_axes=), responsive sizing - Using Tabulator —
add_filterwith widgets, checkbox selection, row content, function-based filtering - Using Pytest Playwright —
serve_component/wait_untilutilities, JS↔Python sync tests, complete test patterns for custom components - Reviewing Panel Apps — anti-pattern checklist for code review: flickering, missing hold, watcher gaps, bind vs watch, mutation bugs
Viewer Class Pattern¶
- Recreating panes or layouts inside
@param.dependscauses flickering. Create them once in__init__, bind to reactive content. on_init=Truewatchers fire duringsuper().__init__(). Create any panes they reference before thesuper().__init__(**params)call.- Use
pn.pane.Placeholderwhen the content type varies (string → plot → widget). Swap with.update()or.object =. - Implement
__panel__to return the layout. When served, wrap inpmui.Page(see Material UI); otherwise return the bare component. - Shared UI state: Add a param (
disabled,loading,visible) to a base class and bind widgets to it (e.g.,disabled=self.param.disabled). Set once to update all widgets — useful for form submit, loading states, or toggling visibility. - Organize
__init__: Separate component instantiation from wiring. First create all widgets/panes, then groupon_click,pn.bind, and.watch()calls together. Makes it clear what exists vs. how it's connected. - Method naming:
_on_*for event handlers (_on_click,_on_submit),_update_*for watchers that sync state (_update_view,_update_button_state),_sync_*for bidirectional syncs. - Wizard/pipeline pattern: For multi-step flows, see
examples/wizard.py— Breadcrumbs, Placeholder step swapping, shareddisabledstate,pn.io.hold()batching, andpmui.Page. - KPI dashboard pattern: For metric dashboards, see
examples/dashboard.py—pn.indicators.TrendKPI cards,pmui.Gridresponsive layout, DynamicMap with trigger pattern, Tabulatoradd_filter+ checkbox selection cross-filtering,pn.bind(watch=True)widget wiring,param.DataFrameas single source of truth, andpmui.Page.
import hvplot.pandas # noqa
import panel as pn
import panel_material_ui as pmui
import param
pn.extension(throttled=True)
penguins = hvplot.sampledata.penguins("pandas").dropna()
species_list = sorted(penguins["species"].unique())
# ✅ Static panes, reactive content
class Dashboard(pn.viewable.Viewer):
species = param.ListSelector(default=species_list, objects=species_list)
def __init__(self, **params):
# Create panes before super().__init__ — on_init=True watchers fire during super()
self._species_widget = pmui.CheckButtonGroup.from_param(self.param.species)
self._chart_pane = pn.pane.HoloViews(sizing_mode="stretch_width")
super().__init__(**params)
with pn.config.set(sizing_mode="stretch_width"):
self._sidebar = pmui.Column(self._species_widget)
self._main = pmui.Column(self._summary, self._chart_pane)
def _filtered(self):
return penguins[penguins["species"].isin(self.species)]
@param.depends("species")
def _summary(self):
return f"**{len(self._filtered())}** penguins selected"
@param.depends("species", watch=True, on_init=True)
def _update_chart(self):
self._chart_pane.object = self._filtered().hvplot.scatter(
x="bill_length_mm", y="bill_depth_mm", by="species",
)
def __panel__(self):
if pn.state.served:
return pmui.Page(
title="Penguin Explorer",
sidebar=[self._sidebar],
main=[self._main],
)
return self._main
# ❌ Recreates layout on every change — causes flickering
class BadDashboard(pn.viewable.Viewer):
species = param.ListSelector(default=species_list, objects=species_list)
@param.depends("species")
def view(self):
filtered = penguins[penguins["species"].isin(self.species)]
return pn.Column(
pn.pane.Markdown(f"**{len(filtered)}** penguins selected"),
pn.pane.HoloViews(filtered.hvplot.scatter(x="bill_length_mm", y="bill_depth_mm", by="species")),
)
Widgets and Extensions¶
- Call
pn.extension(throttled=True)with any needed JS extensions ("tabulator","plotly"). Never add"bokeh". .from_param()auto-creates the right widget type from a parameter — syncs value, bounds, and objects. Caveat: some pmui widgets (e.g.pmui.CheckBoxGroup) may not sync changes back to the param. If widgets appear disconnected, create them directly and usepn.bind(fn, widget.param.value, watch=True)to wire updates.- Prefer
pn.bind(self._update, widget1.param.value, widget2.param.value, watch=True)over lambda-based.param.watch()for wiring multiple widgets to a single update method. - Default to
sizing_mode="stretch_width"viapn.config.set.
Templates and Layouts¶
For new apps, use pmui.Page from panel-material-ui (see Material UI). If an existing codebase already uses a different template (e.g. FastListTemplate), keep it rather than migrating.
- Sidebar order: logo → description → widgets → docs.
- Use
FlexBox,GridSpec, orGridBoxfor complex layouts instead of nested Rows/Columns. - Set
min_width/max_width/min_height/max_heightto prevent layout collapse.
Serving Workflow¶
- Keep a dev server running:
panel serve app.py --dev --show. Don't restart after edits. - Don't use
--autoreload(legacy). Don't usepython app.py.
Performance¶
@pn.cachewithttl=andmax_items=for expensive computations.pn.extension(defer_load=True, loading_indicator=True)for heavy components.pn.io.hold()to batch multiple updates into a single redraw.- Async/await for I/O; threads for CPU-intensive work.
@pn.io.profilerto find bottlenecks.- Memory: cap streaming history,
pn.state.clear_caches(), schedule restarts.
Plotting Integration¶
For HoloViews/hvPlot plots in Panel (DynamicMap, streams, responsive sizing), see HoloViews integration. For standalone HoloViews concepts (elements, .opts(), streams, formatters), see the HoloViews skill.
pn.pane.HoloViews(plot, theme="light_minimal")— settheme=on the pane, not globally. Options:"light_minimal","dark_minimal","caliber","night_sky", etc.
Matplotlib¶
- Set
matplotlib.use('agg')BEFORE importing pyplot — required for server-side rendering. - Don't add
'matplotlib'topn.extension()— not a JS extension. - Close figures after rendering:
plt.close(fig).
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
import panel as pn
pn.extension() # no 'matplotlib' needed
Plotly¶
- Add
"plotly"topn.extension("plotly"). - Match template to app theme, use transparent backgrounds:
template = "plotly_dark" if pn.state.theme == "dark" else "plotly_white"
fig.update_layout(
template=template,
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
)
ECharts¶
- Prefer dict config over pyecharts.
- Configs must be JSON-serializable — never use Python functions or lambdas (
SerializationError). - Template strings:
{b}(category),{c}(value),{d}(percentage),{value}(axis). Prefix/suffix:'{value}%'. - Use
replaceMergewhen series count changes dynamically, else old series persist:
chart_pane = pn.pane.ECharts(
self._chart_config,
options={"replaceMerge": ["series"]},
sizing_mode="stretch_width",
height=400,
)
Component Gotchas¶
Tabulator: prefer overpn.pane.DataFramefor displaying DataFrames in apps — sortable, filterable, and paginated. Requirespn.extension("tabulator"). See Using Tabulator foradd_filter, checkbox selection, and row content patterns.Markdown: setdisable_anchors=Trueto avoid flicker on header hover.CheckButtonGroup: useorientation="vertical"in sidebars,button_type="primary",button_style="outline".- Selector widgets with
default=None:RadioBoxGroup/RadioButtonGroupvisually highlight the first option even whenvalue=None. Clicking that option doesn't fire a change event (UI thinks it's already selected), so users can't select the first option. Also,@param.dependsandpn.bindwon't trigger on initial load since the value isNoneand clicking the highlighted option doesn't change it. Always set a real default value for radio widgets, or useSelectif you need an empty state. - Bokeh tools: use
default_tools=["reset"]to strip all default Bokeh toolbar tools except reset, then add specific tools viatools=["hover", "xwheel_zoom"]. Useactive_tools=["xwheel_zoom"]to set which tools are active by default. For cumulative/monotonic curves,hover_mode="vline"gives a better tooltip experience. - Date widgets: convert to
pd.Timestampbefore comparing to DataFrame columns.
start_date, end_date = self.date_range
start_date = pd.Timestamp(start_date)
end_date = pd.Timestamp(end_date)
filtered = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
Lookup¶
Component Reference¶
Look up component docs at https://panel.holoviz.org/reference/{section}/{Component}.html
Sections: panes, widgets, layouts, chat, global, indicators, templates, custom_components
Search¶
Search the web at https://panel.holoviz.org/search.html?q=<topic> for additional information.