Panel is a Python library for creating interactive dashboards, apps, and data exploration tools. It works with many plotting libraries (Matplotlib, Bokeh, Plotly) and can turn Jupyter notebooks into standalone applications.
Why Panel?
- Framework Agnostic: Works with any Python plotting library
- Notebook Integration: Build dashboards directly in Jupyter
- Reactive: Automatically updates when inputs change
- Deploy Anywhere: Jupyter, standalone server, or static HTML
- Rich Widgets: Extensive widget library for interactivity
Basic Concepts
Simple Dashboard
import panel as pn
pn.extension()
# Create a simple dashboard
text = pn.pane.Markdown('# My Dashboard')
plot = df.hvplot.line(x='time', y='value')
dashboard = pn.Column(text, plot)
dashboard.servable()
Widgets
# Create interactive widgets
slider = pn.widgets.FloatSlider(name='Threshold', start=0, end=10, value=5)
select = pn.widgets.Select(name='Channel', options=['Ch1', 'Ch2', 'Ch3'])
button = pn.widgets.Button(name='Update', button_type='primary')
# Display widgets
pn.Column(slider, select, button)
Building Interactive Dashboards
Reactive Functions
import panel as pn
import hvplot.pandas
pn.extension()
# Create widgets
channel_select = pn.widgets.Select(
name='Channel',
options=['Ch1', 'Ch2', 'Ch3', 'Ch4']
)
# Reactive function that updates when widget changes
@pn.depends(channel_select.param.value)
def plot_channel(channel):
filtered_df = df[df['channel'] == channel]
return filtered_df.hvplot.line(x='time', y='voltage', title=f'{channel} Voltage')
# Create dashboard
dashboard = pn.Column(
'# LFP Explorer',
channel_select,
plot_channel
)
dashboard.servable()
With Parameters
import param
class DataExplorer(param.Parameterized):
channel = param.Selector(default='Ch1', objects=['Ch1', 'Ch2', 'Ch3', 'Ch4'])
threshold = param.Number(default=5.0, bounds=(0, 10))
smooth = param.Boolean(default=False)
@param.depends('channel', 'threshold', 'smooth')
def view(self):
# Filter data
data = df[df['channel'] == self.channel]
data = data[data['value'] > self.threshold]
if self.smooth:
data['value'] = data['value'].rolling(10).mean()
return data.hvplot.line(x='time', y='value')
explorer = DataExplorer()
pn.Row(explorer.param, explorer.view).servable()
Layout Options
Columns and Rows
# Vertical layout
pn.Column(widget1, plot1, plot2)
# Horizontal layout
pn.Row(widget1, plot1)
# Nested layouts
pn.Column(
'# Dashboard',
pn.Row(widget1, widget2),
pn.Row(plot1, plot2)
)
Tabs
tabs = pn.Tabs(
('Overview', overview_plot),
('Detailed Analysis', detail_plot),
('Statistics', stats_table)
)
Templates
# Use built-in templates
template = pn.template.FastListTemplate(
title='Neural Data Explorer',
sidebar=[channel_select, threshold_slider],
main=[plot1, plot2]
)
template.servable()
Practical Examples
Calcium Imaging ROI Explorer
import panel as pn
import hvplot.pandas
pn.extension()
# Widgets
roi_selector = pn.widgets.IntSlider(name='ROI', start=1, end=100, value=1)
trial_selector = pn.widgets.IntSlider(name='Trial', start=1, end=50, value=1)
@pn.depends(roi_selector.param.value, trial_selector.param.value)
def plot_trace(roi, trial):
# Filter data
trace = traces_df[
(traces_df['roi'] == roi) &
(traces_df['trial'] == trial)
]
return trace.hvplot.line(
x='time', y='dF_F',
xlabel='Time (s)',
ylabel='ΔF/F',
title=f'ROI {roi}, Trial {trial}'
)
# Summary statistics
@pn.depends(roi_selector.param.value)
def show_stats(roi):
roi_data = traces_df[traces_df['roi'] == roi]
stats = {
'Mean Response': roi_data['dF_F'].mean(),
'Peak Response': roi_data['dF_F'].max(),
'Baseline': roi_data['dF_F'].iloc[:100].mean()
}
return pn.pane.DataFrame(pd.DataFrame([stats]))
dashboard = pn.Column(
'# Calcium Imaging Explorer',
pn.Row(roi_selector, trial_selector),
plot_trace,
'## ROI Statistics',
show_stats
)
dashboard.servable()
LFP Analysis Dashboard
# Widgets for filtering
freq_range = pn.widgets.RangeSlider(
name='Frequency Range (Hz)',
start=0, end=100, value=(4, 8), step=1
)
time_range = pn.widgets.RangeSlider(
name='Time Window (s)',
start=0, end=10, value=(0, 10), step=0.1
)
@pn.depends(freq_range.param.value, time_range.param.value)
def filtered_spectrogram(freq, time):
# Filter spectrogram data
filtered = spectrogram_df[
(spectrogram_df['frequency'] >= freq[0]) &
(spectrogram_df['frequency'] <= freq[1]) &
(spectrogram_df['time'] >= time[0]) &
(spectrogram_df['time'] <= time[1])
]
return filtered.hvplot.heatmap(
x='time', y='frequency', C='power',
cmap='viridis',
xlabel='Time (s)',
ylabel='Frequency (Hz)'
)
pn.Column(
'# LFP Spectrogram',
freq_range,
time_range,
filtered_spectrogram
).servable()
Statistical Comparison Tool
# Group comparison dashboard
group1_select = pn.widgets.MultiSelect(
name='Group 1 Subjects',
options=subject_list,
size=10
)
group2_select = pn.widgets.MultiSelect(
name='Group 2 Subjects',
options=subject_list,
size=10
)
metric_select = pn.widgets.Select(
name='Metric',
options=['reaction_time', 'accuracy', 'response_variability']
)
run_button = pn.widgets.Button(name='Run Comparison', button_type='primary')
def run_comparison(event):
from scipy import stats
import pingouin as pg
# Get data for selected groups
g1_data = df[df['subject'].isin(group1_select.value)][metric_select.value]
g2_data = df[df['subject'].isin(group2_select.value)][metric_select.value]
# Run t-test
result = pg.ttest(g1_data, g2_data)
# Create comparison plot
comparison_df = pd.DataFrame({
'Group 1': g1_data,
'Group 2': g2_data
})
plot = comparison_df.hvplot.violin(
ylabel=metric_select.value,
title=f'p = {result["p-val"].values[0]:.4f}'
)
results_pane.object = result
plot_pane.object = plot
run_button.on_click(run_comparison)
results_pane = pn.pane.DataFrame()
plot_pane = pn.pane.HoloViews()
dashboard = pn.Column(
'# Statistical Comparison',
pn.Row(group1_select, group2_select),
metric_select,
run_button,
plot_pane,
results_pane
)
dashboard.servable()
Deployment
Jupyter Notebook
# Display inline in notebook
dashboard.show()
# Or use .servable() at the end of the notebook
dashboard.servable()
Standalone Server
# Save dashboard to file: dashboard.py
# Run server
panel serve dashboard.py --show
Static HTML Export
dashboard.save('dashboard.html', embed=True)
Advanced Features
Caching for Performance
@pn.cache
def expensive_computation(param):
# Expensive operation
result = process_large_dataset(param)
return result
@pn.depends(slider.param.value)
def plot(value):
data = expensive_computation(value) # Cached
return data.hvplot()
Custom JavaScript
# Add custom interactivity
js_code = """
console.log('Value changed to:', value);
"""
widget.jscallback(args={'value': widget}, value=js_code)
Authentication
# Add basic auth
pn.serve(
dashboard,
port=5006,
basic_auth={'username': 'password'}
)
Installation
pixi add panel
# or
conda install -c conda-forge panel
# or
pip install panel
For Jupyter notebook support:
jupyter labextension install @pyviz/jupyterlab_pyviz
Best Practices
- Use
.servable()at the end of notebooks for deployment - Cache expensive computations with
@pn.cache - Organize complex dashboards with tabs and layouts
- Use templates for professional-looking apps
- Test responsiveness with different screen sizes
- Document widget purposes for users
- Consider loading indicators for slow operations
- Use param.Parameterized for complex state management