The powerful data exploration & web app framework for Python.
—
Classes for extending Panel with custom functionality using Python, JavaScript, React, or ESM modules. Custom components enable developers to create specialized widgets and visualizations that integrate seamlessly with Panel's component system.
Create custom components using pure Python with reactive parameters and custom rendering logic.
class PyComponent:
"""
Python-based custom component for creating widgets with pure Python.
Parameters:
- _template: HTML template string for component rendering
- _stylesheets: List of CSS stylesheets to include
- _extension_name: Name for the component extension
- **params: Component parameters that become reactive
"""Create custom components using JavaScript for client-side interactivity and custom behavior.
class JSComponent:
"""
JavaScript-based component for client-side custom widgets.
Parameters:
- _esm: ECMAScript module code or file path
- _template: HTML template for the component
- _stylesheets: CSS stylesheets to include
- _importmap: Import map for module dependencies
- **params: Component parameters
"""Create custom components using React for building complex interactive interfaces.
class ReactComponent:
"""
React-based component for building React-powered widgets.
Parameters:
- _template: React JSX template
- _stylesheets: CSS stylesheets for styling
- _importmap: Import map for React dependencies
- **params: Component parameters passed as React props
"""Create components from ECMAScript modules for modern JavaScript functionality.
class ReactiveESM:
"""
ESM module-based component with reactive parameter binding.
Parameters:
- _esm: ECMAScript module source code or file path
- _template: HTML template for component
- _stylesheets: CSS stylesheets
- _importmap: Module import map
- **params: Reactive parameters
"""Create components compatible with the AnyWidget specification for cross-framework compatibility.
class AnyWidgetComponent:
"""
AnyWidget-compatible component for cross-framework integration.
Parameters:
- _esm: JavaScript module implementing AnyWidget interface
- _css: CSS styles for the component
- **params: Widget parameters
"""import panel as pn
import param
class CounterComponent(pn.custom.PyComponent):
value = param.Integer(default=0)
step = param.Integer(default=1)
_template = """
<div class="counter">
<button id="decrement" onclick="${script('decrement')}">-</button>
<span class="value">${value}</span>
<button id="increment" onclick="${script('increment')}">+</button>
</div>
"""
_stylesheets = ["""
.counter {
display: flex;
align-items: center;
gap: 10px;
font-family: sans-serif;
}
.counter button {
padding: 5px 10px;
font-size: 16px;
border: 1px solid #ccc;
background: #f8f9fa;
cursor: pointer;
}
.counter .value {
font-size: 18px;
font-weight: bold;
min-width: 40px;
text-align: center;
}
"""]
def _handle_increment(self, event):
self.value += self.step
def _handle_decrement(self, event):
self.value -= self.step
# Use the custom component
counter = CounterComponent(value=10, step=2)class ChartComponent(pn.custom.JSComponent):
data = param.List(default=[])
chart_type = param.Selector(default='line', objects=['line', 'bar', 'pie'])
_esm = """
export function render({ model, el }) {
// Create chart using Chart.js or D3
const canvas = document.createElement('canvas');
el.appendChild(canvas);
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, {
type: model.chart_type,
data: {
datasets: [{
data: model.data,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
}
}
});
// Update chart when parameters change
model.on('change:data', () => {
chart.data.datasets[0].data = model.data;
chart.update();
});
model.on('change:chart_type', () => {
chart.config.type = model.chart_type;
chart.update();
});
}
"""
_importmap = {
"imports": {
"chart.js": "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/+esm"
}
}
# Use the chart component
chart = ChartComponent(
data=[10, 20, 30, 25, 15],
chart_type='bar'
)class ReactButton(pn.custom.ReactComponent):
clicks = param.Integer(default=0)
label = param.String(default="Click me")
variant = param.Selector(default='primary', objects=['primary', 'secondary', 'success', 'danger'])
_template = """
export default function ReactButton({ model }) {
const [clicks, setClicks] = React.useState(model.clicks);
const handleClick = () => {
const newClicks = clicks + 1;
setClicks(newClicks);
model.clicks = newClicks;
};
return (
<button
className={`btn btn-${model.variant}`}
onClick={handleClick}
style={{
padding: '8px 16px',
fontSize: '16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: getVariantColor(model.variant)
}}
>
{model.label} ({clicks})
</button>
);
}
function getVariantColor(variant) {
const colors = {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545'
};
return colors[variant] || colors.primary;
}
"""
_importmap = {
"imports": {
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18"
}
}
# Use the React component
react_btn = ReactButton(label="React Button", variant='success')import param
import pandas as pd
class DataExplorer(pn.custom.PyComponent):
data = param.DataFrame()
selected_columns = param.List(default=[])
chart_type = param.Selector(default='scatter', objects=['scatter', 'line', 'bar', 'histogram'])
_template = """
<div class="data-explorer">
<div class="controls">
<h3>Data Explorer</h3>
<div class="column-selector">
<label>Select Columns:</label>
<select multiple size="5" onchange="${script('update_columns')}">
{% for col in data.columns %}
<option value="{{ col }}"
{% if col in selected_columns %}selected{% endif %}>
{{ col }}
</option>
{% endfor %}
</select>
</div>
<div class="chart-type">
<label>Chart Type:</label>
<select onchange="${script('update_chart_type')}">
<option value="scatter" {% if chart_type == 'scatter' %}selected{% endif %}>Scatter</option>
<option value="line" {% if chart_type == 'line' %}selected{% endif %}>Line</option>
<option value="bar" {% if chart_type == 'bar' %}selected{% endif %}>Bar</option>
<option value="histogram" {% if chart_type == 'histogram' %}selected{% endif %}>Histogram</option>
</select>
</div>
</div>
<div class="preview">
{% if selected_columns %}
<h4>Selected Data Preview:</h4>
<div class="data-preview">
{{ data[selected_columns].head().to_html() }}
</div>
{% endif %}
</div>
</div>
"""
_stylesheets = ["""
.data-explorer {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.controls {
display: flex;
gap: 20px;
align-items: flex-start;
}
.column-selector select, .chart-type select {
padding: 5px;
min-width: 150px;
}
.data-preview {
max-height: 300px;
overflow: auto;
background: white;
padding: 10px;
border-radius: 4px;
}
"""]
def _handle_update_columns(self, event):
# Extract selected columns from event
selected = event.target.selectedValues
self.selected_columns = list(selected)
def _handle_update_chart_type(self, event):
self.chart_type = event.target.value
@param.depends('selected_columns', 'chart_type', watch=True)
def _update_visualization(self):
# Trigger re-render when parameters change
self._render()
# Use the data explorer
df = pd.DataFrame({
'x': range(100),
'y': [i**2 for i in range(100)],
'category': ['A', 'B'] * 50
})
explorer = DataExplorer(data=df)class AnyWidgetSlider(pn.custom.AnyWidgetComponent):
value = param.Number(default=50)
min = param.Number(default=0)
max = param.Number(default=100)
step = param.Number(default=1)
_esm = """
function render({ model, el }) {
const slider = document.createElement('input');
slider.type = 'range';
slider.min = model.get('min');
slider.max = model.get('max');
slider.step = model.get('step');
slider.value = model.get('value');
slider.addEventListener('input', () => {
model.set('value', parseFloat(slider.value));
model.save_changes();
});
model.on('change:value', () => {
slider.value = model.get('value');
});
el.appendChild(slider);
}
export default { render };
"""
_css = """
input[type="range"] {
width: 100%;
height: 20px;
background: #ddd;
outline: none;
border-radius: 10px;
}
"""
# Use the AnyWidget component
slider = AnyWidgetSlider(value=75, min=0, max=200, step=5)class ComponentWithControls(pn.custom.PyComponent):
def __init__(self, **params):
super().__init__(**params)
# Create control widgets
self.controls = pn.Column(
pn.widgets.ColorPicker(name="Color", value="#ff0000"),
pn.widgets.IntSlider(name="Size", start=10, end=100, value=50),
pn.widgets.Select(name="Shape", options=['circle', 'square', 'triangle'])
)
# Bind control changes to component updates
for widget in self.controls:
widget.param.watch(self._update_from_controls, 'value')
def _update_from_controls(self, event):
# Update component based on control values
self._render()
def panel(self):
return pn.Row(self.controls, self)
component = ComponentWithControls()def create_custom_widget(widget_type, **config):
"""Factory function for creating custom widgets"""
if widget_type == 'counter':
return CounterComponent(**config)
elif widget_type == 'chart':
return ChartComponent(**config)
elif widget_type == 'data_explorer':
return DataExplorer(**config)
else:
raise ValueError(f"Unknown widget type: {widget_type}")
# Use the factory
custom_widgets = [
create_custom_widget('counter', value=0, step=1),
create_custom_widget('chart', data=[1,2,3,4,5], chart_type='line'),
create_custom_widget('data_explorer', data=df)
]