0
# Custom Components
1
2
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.
3
4
## Capabilities
5
6
### Python-Based Components
7
8
Create custom components using pure Python with reactive parameters and custom rendering logic.
9
10
```python { .api }
11
class PyComponent:
12
"""
13
Python-based custom component for creating widgets with pure Python.
14
15
Parameters:
16
- _template: HTML template string for component rendering
17
- _stylesheets: List of CSS stylesheets to include
18
- _extension_name: Name for the component extension
19
- **params: Component parameters that become reactive
20
"""
21
```
22
23
### JavaScript Components
24
25
Create custom components using JavaScript for client-side interactivity and custom behavior.
26
27
```python { .api }
28
class JSComponent:
29
"""
30
JavaScript-based component for client-side custom widgets.
31
32
Parameters:
33
- _esm: ECMAScript module code or file path
34
- _template: HTML template for the component
35
- _stylesheets: CSS stylesheets to include
36
- _importmap: Import map for module dependencies
37
- **params: Component parameters
38
"""
39
```
40
41
### React Components
42
43
Create custom components using React for building complex interactive interfaces.
44
45
```python { .api }
46
class ReactComponent:
47
"""
48
React-based component for building React-powered widgets.
49
50
Parameters:
51
- _template: React JSX template
52
- _stylesheets: CSS stylesheets for styling
53
- _importmap: Import map for React dependencies
54
- **params: Component parameters passed as React props
55
"""
56
```
57
58
### ESM Module Components
59
60
Create components from ECMAScript modules for modern JavaScript functionality.
61
62
```python { .api }
63
class ReactiveESM:
64
"""
65
ESM module-based component with reactive parameter binding.
66
67
Parameters:
68
- _esm: ECMAScript module source code or file path
69
- _template: HTML template for component
70
- _stylesheets: CSS stylesheets
71
- _importmap: Module import map
72
- **params: Reactive parameters
73
"""
74
```
75
76
### AnyWidget Integration
77
78
Create components compatible with the AnyWidget specification for cross-framework compatibility.
79
80
```python { .api }
81
class AnyWidgetComponent:
82
"""
83
AnyWidget-compatible component for cross-framework integration.
84
85
Parameters:
86
- _esm: JavaScript module implementing AnyWidget interface
87
- _css: CSS styles for the component
88
- **params: Widget parameters
89
"""
90
```
91
92
## Usage Examples
93
94
### Basic Python Component
95
96
```python
97
import panel as pn
98
import param
99
100
class CounterComponent(pn.custom.PyComponent):
101
102
value = param.Integer(default=0)
103
step = param.Integer(default=1)
104
105
_template = """
106
<div class="counter">
107
<button id="decrement" onclick="${script('decrement')}">-</button>
108
<span class="value">${value}</span>
109
<button id="increment" onclick="${script('increment')}">+</button>
110
</div>
111
"""
112
113
_stylesheets = ["""
114
.counter {
115
display: flex;
116
align-items: center;
117
gap: 10px;
118
font-family: sans-serif;
119
}
120
.counter button {
121
padding: 5px 10px;
122
font-size: 16px;
123
border: 1px solid #ccc;
124
background: #f8f9fa;
125
cursor: pointer;
126
}
127
.counter .value {
128
font-size: 18px;
129
font-weight: bold;
130
min-width: 40px;
131
text-align: center;
132
}
133
"""]
134
135
def _handle_increment(self, event):
136
self.value += self.step
137
138
def _handle_decrement(self, event):
139
self.value -= self.step
140
141
# Use the custom component
142
counter = CounterComponent(value=10, step=2)
143
```
144
145
### JavaScript Component
146
147
```python
148
class ChartComponent(pn.custom.JSComponent):
149
150
data = param.List(default=[])
151
chart_type = param.Selector(default='line', objects=['line', 'bar', 'pie'])
152
153
_esm = """
154
export function render({ model, el }) {
155
// Create chart using Chart.js or D3
156
const canvas = document.createElement('canvas');
157
el.appendChild(canvas);
158
159
const ctx = canvas.getContext('2d');
160
const chart = new Chart(ctx, {
161
type: model.chart_type,
162
data: {
163
datasets: [{
164
data: model.data,
165
backgroundColor: 'rgba(75, 192, 192, 0.2)',
166
borderColor: 'rgba(75, 192, 192, 1)',
167
}]
168
},
169
options: {
170
responsive: true,
171
plugins: {
172
legend: { display: false }
173
}
174
}
175
});
176
177
// Update chart when parameters change
178
model.on('change:data', () => {
179
chart.data.datasets[0].data = model.data;
180
chart.update();
181
});
182
183
model.on('change:chart_type', () => {
184
chart.config.type = model.chart_type;
185
chart.update();
186
});
187
}
188
"""
189
190
_importmap = {
191
"imports": {
192
"chart.js": "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/+esm"
193
}
194
}
195
196
# Use the chart component
197
chart = ChartComponent(
198
data=[10, 20, 30, 25, 15],
199
chart_type='bar'
200
)
201
```
202
203
### React Component
204
205
```python
206
class ReactButton(pn.custom.ReactComponent):
207
208
clicks = param.Integer(default=0)
209
label = param.String(default="Click me")
210
variant = param.Selector(default='primary', objects=['primary', 'secondary', 'success', 'danger'])
211
212
_template = """
213
export default function ReactButton({ model }) {
214
const [clicks, setClicks] = React.useState(model.clicks);
215
216
const handleClick = () => {
217
const newClicks = clicks + 1;
218
setClicks(newClicks);
219
model.clicks = newClicks;
220
};
221
222
return (
223
<button
224
className={`btn btn-${model.variant}`}
225
onClick={handleClick}
226
style={{
227
padding: '8px 16px',
228
fontSize: '16px',
229
border: 'none',
230
borderRadius: '4px',
231
cursor: 'pointer',
232
backgroundColor: getVariantColor(model.variant)
233
}}
234
>
235
{model.label} ({clicks})
236
</button>
237
);
238
}
239
240
function getVariantColor(variant) {
241
const colors = {
242
primary: '#007bff',
243
secondary: '#6c757d',
244
success: '#28a745',
245
danger: '#dc3545'
246
};
247
return colors[variant] || colors.primary;
248
}
249
"""
250
251
_importmap = {
252
"imports": {
253
"react": "https://esm.sh/react@18",
254
"react-dom": "https://esm.sh/react-dom@18"
255
}
256
}
257
258
# Use the React component
259
react_btn = ReactButton(label="React Button", variant='success')
260
```
261
262
### Complex Interactive Component
263
264
```python
265
import param
266
import pandas as pd
267
268
class DataExplorer(pn.custom.PyComponent):
269
270
data = param.DataFrame()
271
selected_columns = param.List(default=[])
272
chart_type = param.Selector(default='scatter', objects=['scatter', 'line', 'bar', 'histogram'])
273
274
_template = """
275
<div class="data-explorer">
276
<div class="controls">
277
<h3>Data Explorer</h3>
278
<div class="column-selector">
279
<label>Select Columns:</label>
280
<select multiple size="5" onchange="${script('update_columns')}">
281
{% for col in data.columns %}
282
<option value="{{ col }}"
283
{% if col in selected_columns %}selected{% endif %}>
284
{{ col }}
285
</option>
286
{% endfor %}
287
</select>
288
</div>
289
<div class="chart-type">
290
<label>Chart Type:</label>
291
<select onchange="${script('update_chart_type')}">
292
<option value="scatter" {% if chart_type == 'scatter' %}selected{% endif %}>Scatter</option>
293
<option value="line" {% if chart_type == 'line' %}selected{% endif %}>Line</option>
294
<option value="bar" {% if chart_type == 'bar' %}selected{% endif %}>Bar</option>
295
<option value="histogram" {% if chart_type == 'histogram' %}selected{% endif %}>Histogram</option>
296
</select>
297
</div>
298
</div>
299
<div class="preview">
300
{% if selected_columns %}
301
<h4>Selected Data Preview:</h4>
302
<div class="data-preview">
303
{{ data[selected_columns].head().to_html() }}
304
</div>
305
{% endif %}
306
</div>
307
</div>
308
"""
309
310
_stylesheets = ["""
311
.data-explorer {
312
display: flex;
313
flex-direction: column;
314
gap: 20px;
315
padding: 20px;
316
border: 1px solid #ddd;
317
border-radius: 8px;
318
background: #f9f9f9;
319
}
320
.controls {
321
display: flex;
322
gap: 20px;
323
align-items: flex-start;
324
}
325
.column-selector select, .chart-type select {
326
padding: 5px;
327
min-width: 150px;
328
}
329
.data-preview {
330
max-height: 300px;
331
overflow: auto;
332
background: white;
333
padding: 10px;
334
border-radius: 4px;
335
}
336
"""]
337
338
def _handle_update_columns(self, event):
339
# Extract selected columns from event
340
selected = event.target.selectedValues
341
self.selected_columns = list(selected)
342
343
def _handle_update_chart_type(self, event):
344
self.chart_type = event.target.value
345
346
@param.depends('selected_columns', 'chart_type', watch=True)
347
def _update_visualization(self):
348
# Trigger re-render when parameters change
349
self._render()
350
351
# Use the data explorer
352
df = pd.DataFrame({
353
'x': range(100),
354
'y': [i**2 for i in range(100)],
355
'category': ['A', 'B'] * 50
356
})
357
358
explorer = DataExplorer(data=df)
359
```
360
361
### AnyWidget Component
362
363
```python
364
class AnyWidgetSlider(pn.custom.AnyWidgetComponent):
365
366
value = param.Number(default=50)
367
min = param.Number(default=0)
368
max = param.Number(default=100)
369
step = param.Number(default=1)
370
371
_esm = """
372
function render({ model, el }) {
373
const slider = document.createElement('input');
374
slider.type = 'range';
375
slider.min = model.get('min');
376
slider.max = model.get('max');
377
slider.step = model.get('step');
378
slider.value = model.get('value');
379
380
slider.addEventListener('input', () => {
381
model.set('value', parseFloat(slider.value));
382
model.save_changes();
383
});
384
385
model.on('change:value', () => {
386
slider.value = model.get('value');
387
});
388
389
el.appendChild(slider);
390
}
391
392
export default { render };
393
"""
394
395
_css = """
396
input[type="range"] {
397
width: 100%;
398
height: 20px;
399
background: #ddd;
400
outline: none;
401
border-radius: 10px;
402
}
403
"""
404
405
# Use the AnyWidget component
406
slider = AnyWidgetSlider(value=75, min=0, max=200, step=5)
407
```
408
409
## Integration Patterns
410
411
### Custom Component with Panel Widgets
412
413
```python
414
class ComponentWithControls(pn.custom.PyComponent):
415
416
def __init__(self, **params):
417
super().__init__(**params)
418
419
# Create control widgets
420
self.controls = pn.Column(
421
pn.widgets.ColorPicker(name="Color", value="#ff0000"),
422
pn.widgets.IntSlider(name="Size", start=10, end=100, value=50),
423
pn.widgets.Select(name="Shape", options=['circle', 'square', 'triangle'])
424
)
425
426
# Bind control changes to component updates
427
for widget in self.controls:
428
widget.param.watch(self._update_from_controls, 'value')
429
430
def _update_from_controls(self, event):
431
# Update component based on control values
432
self._render()
433
434
def panel(self):
435
return pn.Row(self.controls, self)
436
437
component = ComponentWithControls()
438
```
439
440
### Custom Component Factory
441
442
```python
443
def create_custom_widget(widget_type, **config):
444
"""Factory function for creating custom widgets"""
445
446
if widget_type == 'counter':
447
return CounterComponent(**config)
448
elif widget_type == 'chart':
449
return ChartComponent(**config)
450
elif widget_type == 'data_explorer':
451
return DataExplorer(**config)
452
else:
453
raise ValueError(f"Unknown widget type: {widget_type}")
454
455
# Use the factory
456
custom_widgets = [
457
create_custom_widget('counter', value=0, step=1),
458
create_custom_widget('chart', data=[1,2,3,4,5], chart_type='line'),
459
create_custom_widget('data_explorer', data=df)
460
]
461
```