Loading...
Loading...
Best practices for developing tools, dashboards and interactive data apps with HoloViz Panel. Create reactive, component-based UIs with widgets, layouts, templates, and real-time updates. Use when developing interactive data exploration tools, dashboards, data apps, or any interactive Python web application. Supports file uploads, streaming data, multi-page apps, and integration with HoloViews, hvPlot, Pandas, Polars, DuckDB and the rest of the HoloViz and PyData ecosystems.
npx skill4agent add marcskovmadsen/holoviz-mcp panelpanel.plot()pip install panel watchfiles hvplot hvsampledata# DO import panel as pn
import panel as pn
import param
# DO always run pn.extension
# DO remember to add any imports needed by panes, e.g. pn.extension("tabulator", "plotly", ...)
# DON'T add "bokeh" as an extension. It is not needed.
# Do use throttled=True when using slider unless you have a specific reason not to
pn.extension(throttled=True)
# DO organize functions to extract data separately as your app grows. Eventually in a separate data.py file.
# DO use caching to speed up the app, e.g. for expensive data loading or processing that would return the same result given same input arguments.
# DO add a ttl (time to live argument) for expensive data loading that changes over time
@pn.cache(max_items=3)
def extract(n=5):
return "Hello World" + "⭐" * n
text = extract()
text_len = len(text)
# DO organize functions to transform data separately as your app grows. Eventually in a separate transformations.py file
# DO add caching to speed up expensive data transformations
@pn.cache(max_items=3)
def transform(data: str, count: int=5)->str:
count = min(count, len(data))
return data[:count]
# DO organize functions to create plots separately as your app grows. Eventually in a separate plots.py file.
# DO organize custom components and views separately as your app grows. Eventually in separate components.py or views.py file(s).
# DO use param.Parameterized, pn.viewable.Viewer or similar approach to create new components and apps with state and reactivity
class HelloWorld(pn.viewable.Viewer):
# DO define parameters to hold state and drive the reactivity
characters = param.Integer(default=text_len, bounds=(0, text_len), doc="Number of characters to display")
def __init__(self, **params):
super().__init__(**params)
# DO use sizing_mode="stretch_width" for components unless "fixed" or other sizing_mode is specifically needed
with pn.config.set(sizing_mode="stretch_width"):
# DO create widgets using `.from_param` method
self._characters_input = pn.widgets.IntSlider.from_param(self.param.characters, margin=(10,20))
# DO Collect input widgets into horizontal, columnar layout unless other layout is specifically needed
self._inputs = pn.Column(self._characters_input, max_width=300)
# CRITICAL: Create panes ONCE with reactive content
# DON'T recreate panes and layouts in @param.depends methods - causes flickering!
# DO bind reactive methods/functions to panes for smooth updates
self._output_pane = pn.pane.Markdown(
self.model, # Reactive method reference
sizing_mode="stretch_width"
)
# DO collect output components into some layout like Column, Row, FlexBox or Grid depending on use case
self._outputs = pn.Column(self._output_pane)
# DO collect all of your components into a combined layout useful for displaying in notebooks etc.
self._panel = pn.Row(self._inputs, self._outputs)
# DO use caching to speed up bound methods that are expensive to compute or load data and return the same result for a given state of the class.
# DO add a ttl (time to live argument) for expensive data loading that changes over time.
@pn.cache(max_items=3)
# DO prefer .depends over .bind over .rx for reactivity methods on Parameterized classes as it can be typed and documented
# DON'T use `watch=True` or `.watch(...)` methods to update UI components directly.
# DO use `watch=True` or `.watch(...)` for updating the state parameters or triggering side effects like saving files or sending email.
@param.depends("characters")
def model(self):
# CRITICAL: Return ONLY the content, NOT the layout/pane
# The pane was created once in __init__, this just updates its content
return transform(text, self.characters)
# DO use `watch=True` or `.watch(...)` for updating the state parameters or triggering side effects like saving files or sending email.
@param.depends("characters", watch=True)
def _inform_user(self):
print(f"User selected to show {self.characters} characters.")
# DO provide a method for displaying the component in a notebook setting, i.e. without using a Template or other element that cannot be displayed in a notebook setting.
def __panel__(self):
return self._panel
# DO provide a method to create a .servable app
@classmethod
def create_app(cls, **params):
instance = cls(**params)
# DO use a Template or similar page layout for served apps
template = pn.template.FastListTemplate(
# DO provide a title for the app
title="Hello World App",
# DO provide optional image, optional app description, optional navigation menu, input widgets, optional documentation and optional links in the sidebar
# DO provide as list of components or a list of single horizontal layout like Column as the sidebar by default is 300 px wide
sidebar=[instance._inputs],
# DO provide a list of layouts and output components in the main area of the app.
# DO use Grid or FlexBox layouts for complex dashboard layouts instead of combination Rows and Columns.
main=[instance._outputs],
# DO set main_layout=None for modern layout
main_layout=None,
)
return template
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
# DO provide a method to serve the app with `panel serve`
if pn.state.served:
# Mark components to be displayed in the app with .servable()
HelloWorld.create_app().servable()panel serve path_to_this_file.py --show --devpython path_to_this_file.pytests# DO put tests in a separate test file.
# DO always test that the reactivity works as expected
def test_characters_reactivity():
"""
Test characters reactivity.
"""
# DO test the default values of bound
hello_world = HelloWorld()
# DO test the reactivity of bound methods when parameters change
assert hello_world.model() == text[:hello_world.characters]
hello_world.characters = 5
assert hello_world.model() == text[:5]
hello_world.characters = 3
assert hello_world.model() == text[:3]pytest tests/path/to/test_file.pyparam.Parameterizedpn.viewable.Viewer.from_param()@param.depends()@param.depends(..., watch=True)ParameterizedViewable# ❌ AVOID: Updating panes and other components directly. This makes it hard to reason about application flow and state
@param.depends('value', watch=True)
def update_plot(self):
self.output_pane.object = transform(text, self.characters)# ✅ CORRECT: Create panes ONCE in __init__, bind reactive content
class Dashboard(pn.viewable.Viewer):
filter_value = param.String(default="all")
chart = param.Parameter()
def __init__(self, **params):
super().__init__(**params)
# 1. Create static panes with reactive content
self._summary_pane = pn.pane.Markdown(self._summary_text)
self._chart_pane = pn.pane.HoloViews(self.param.chart)
# 2. Create static layout structure
self._layout = pn.Column(
"# Dashboard", # Static title
self._summary_pane, # Reactive content
self._chart_pane, # Reactive content
)
# ✅ Good: Reactive content method
# Will be run multiple times when filter_value updates if multiple panes or reactive functions depend on the _summary_text method
@param.depends("filter_value")
def _summary_text(self):
# Returns string content only, NOT a pane
return f"**Count**: {len(self._get_data())}"
# ✅ Good: Reactive update of chart parameter
# Will be run only one time when filter_value updates - even if multiple panes or reactive functions depend on the chart value
@param.depends("filter_value", watch=True, on_init=True)
def _update_chart(self):
# updates the chart object only, NOT a pane
self.chart = self._get_data().hvplot.bar()
def __panel__(self):
return self._layout
# ❌ WRONG: Recreating layout in @param.depends - causes flickering!
class BadDashboard(pn.viewable.Viewer):
filter_value = param.String(default="all")
@param.depends("filter_value")
def view(self):
# DON'T recreate panes/layouts on every parameter change!
return pn.Column(
"# Dashboard",
pn.pane.Markdown(f"**Count**: {len(self._get_data())}"),
pn.pane.HoloViews(self._get_data().hvplot.bar()),
)__init__@param.dependspn.widgets.IntSliderpn.widgets.Selectpn.widgets.DateRangeSliderpn.widgets.Tabulatorpn.pane.Markdownpn.pane.HTMLpn.pane.HoloViewspn.pane.Plotlypn.pane.Matplotlibpn.pane.EChartspn.Columnpn.Rowpn.Tabspn.Accordionpn.template.FastListTemplatetemplate = pn.template.FastListTemplate(
title="Hello World App",
sidebar=[instance._inputs],
main=[instance._outputs],
main_layout=None,
)sidebarsidebarmain_layout=Nonesizing_mode="stretch_width"with pn.config.set(sizing_mode="stretch_width"):
character_input = pn.widgets...
output_pane = pn.pane....FlexBoxGridSpecGridBoxmin_widthmin_heightmax_widthmax_heightpn.extension# ✅ Good
pn.extension("tabulator", "plotly")# ❌ Bad
pn.extension("bokeh").servable()pn.state.served# ✅ Correct:
if pn.state.served:
main().servable()
# ❌ Incorrect:
if __name__ == "__main__":
main().servable()
# ❌ Don't: Works, but not how we want to serve the app:
if __name__ == "__main__":
main().show()pn.extension(defer_load=True, loading_indicator=True, ...)@pn.cache@pn.io.profilerpn.state.clear_caches()searchpn_searchpn_getpn_paramsref_getskill_getholoviz-mcphvholoviz-mcp searchholoviz-mcp pn searchholoviz-mcp pn getholoviz-mcp pn paramsholoviz-mcp ref getholoviz-mcp skill getTabulatortestspytest tests/path/to/test_file.pypanel serve path_to_file.py --dev --show--show--dev--port {port-number}--autoreloadpython path_to_file.py# ✅ Good: Parameter-driven
widget = pn.widgets.Select.from_param(self.param.model_type, name="Model Type")
# ❌ Avoid: Manual management with links
widget = pn.widgets.Select(options=['A', 'B'], value='A')
widget.link(self, value='model_type') # Hard to reason about# ✅ BEST: Static pane with reactive content (for classes)
class MyComponent(pn.viewable.Viewer):
value = param.Number(default=10)
def __init__(self, **params):
super().__init__(**params)
self._plot_pane = pn.pane.Matplotlib(self._create_plot)
@param.depends('value')
def _create_plot(self):
return create_plot(self.value) # Returns plot only, not pane
# ✅ GOOD: pn.bind for functions
slider = pn.widgets.IntSlider(value=10)
plot_pane = pn.pane.Matplotlib(pn.bind(create_plot, slider))
# ❌ AVOID: Recreating panes and other components directly. This causes flickering.
@param.depends('value')
def view(self):
return pn.pane.Matplotlib(create_plot(self.value)) # DON'T!
# ❌ AVOID: Updating panes and other components directly. This makes it hard to reason about application flow and state
@param.depends('value', watch=True)
def update_plot(self):
self.plot_pane.object = create_plot(self.value)# DO: Create static layout with reactive content
def _get_kpi_card(self):
return pn.pane.HTML(
pn.Column(
"📊 Key Performance Metrics",
self.kpi_value # Reactive reference
),
styles={"padding": "20px", "border": "1px solid #ddd"},
sizing_mode="stretch_width"
)
@param.depends("characters")
def kpi_value(self):
return f"The kpi is {self.characters}"CheckButtonGroupCheckButtonGroup(..., vertical=True)button_type="primary"button_style="outline"Tabulator.disabled=TrueMarkdown.disable_anchors=Truepn.bind(some_func)some_func()hvplotholoviewspanel-holoviews'agg''matplotlib'pn.extension()# ✅ CORRECT
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
import panel as pn
pn.extension() # No 'matplotlib' needed
# ❌ WRONG
pn.extension('matplotlib') # Not a Panel extensionplt.close(fig)themedef create_plot(self) -> go.Figure:
fig = ...
template = "plotly_dark" if pn.state.theme=="dark" else "plotly_white"
fig.update_layout(
template=template, # Change template to align with the theme
paper_bgcolor='rgba(0,0,0,0)', # Change to transparent background to align with the app background
plot_bgcolor='rgba(0,0,0,0)' # Change to transparent background to align with the app background
)
return fig# ❌ WRONG: Lambda functions cause SerializationError
option = {
"tooltip": {
"formatter": lambda params: f"Value: {params['value']}" # DON'T!
},
"xAxis": {
"axisLabel": {
"formatter": lambda value: f"{value}%" # DON'T!
}
},
"series": [{
"animationDelay": lambda idx: idx * 100 # DON'T!
}]
}# ✅ CORRECT: Use ECharts template strings
option = {
"tooltip": {
"formatter": "{b}: {c}" # Template string
},
"xAxis": {
"axisLabel": {
"formatter": "{value}%" # Template string with formatting
}
},
"yAxis": {
"axisLabel": {
"formatter": "${value}" # Dollar sign prefix
}
},
"series": [{
"animationDelay": 100 # Static numeric value
}]
}{a}{b}{c}{d}{value}'{value}%''${value}''{value} units'replaceMerge# ✅ CORRECT: Use replaceMerge to fully replace series on updates
chart_pane = pn.pane.ECharts(
self._chart_config, # Reactive method or parameter
options={"replaceMerge": ["series"]}, # Replace series array instead of merging
sizing_mode="stretch_width",
height=400,
)# ❌ WRONG: Old series remain when filtering reduces series count
chart_pane = pn.pane.ECharts(
self._chart_config,
sizing_mode="stretch_width",
)
# If config changes from 4 series to 2, ECharts merges and keeps all 4!replaceMergeTimestampstart_date, end_date = self.date_range
# DO convert date objects to pandas Timestamp for proper comparison
start_date = pd.Timestamp(start_date)
end_date = pd.Timestamp(end_date)
filtered = filtered[
(filtered['date'] >= start_date) &
(filtered['date'] <= end_date)
]