Create Panes using ESM Components#
In this guide we will show you how to efficiently implement custom panes using JSComponent
, ReactComponent
and AnyWidgetComponent
to get input from the user.
Creating a ChartJS Pane#
This example will show you the basics of creating a ChartJS pane.
import panel as pn
import param
from panel.custom import JSComponent
class ChartJSComponent(JSComponent):
object = param.Dict()
_esm = """
import { Chart } from "https://esm.sh/chart.js/auto"
export function render({ model, el }) {
const canvasEl = document.createElement('canvas')
// Add DOM node before creating the chart
el.append(canvasEl)
const create_chart = () => new Chart(canvasEl.getContext('2d'), model.object)
let chart = create_chart()
model.on("object", () => {
chart.destroy()
chart = create_chart()
})
model.on('remove', () => chart.destroy());
}
"""
def plot(chart_type="line"):
return {
"type": chart_type,
"data": {
"labels": ["January", "February", "March", "April", "May", "June", "July"],
"datasets": [
{
"label": "Data",
"backgroundColor": "rgb(255, 99, 132)",
"borderColor": "rgb(255, 99, 132)",
"data": [0, 10, 5, 2, 20, 30, 45],
}
],
},
"options": {
"responsive": True,
"maintainAspectRatio": False,
},
}
chart_type = pn.widgets.RadioBoxGroup(
name="Chart Type", options=["bar", "line"], inline=True
)
chart = ChartJSComponent(
object=pn.bind(plot, chart_type), height=400, sizing_mode="stretch_width"
)
pn.Column(chart_type, chart).servable()
Note how we had to add the canvasEl
to the el
before we could render the chart. Some libraries will require the element to be attached to the DOM before we could render it. Dealing with layout issues like this sometimes requires a bit of iteration. If you get stuck, share your question and minimum, reproducible code example on Discourse.
import panel as pn
import param
from panel.custom import ReactComponent
class ChartReactComponent(ReactComponent):
object = param.Dict()
_esm = """
import { Chart } from 'https://esm.sh/react-chartjs-2@4.3.1';
import { Chart as ChartJS, registerables } from "https://esm.sh/chart.js@3.9.1";
ChartJS.register(...registerables);
export function render({ model }) {
const [plot] = model.useState('object')
return <Chart {...plot}></Chart>
};
"""
def data(chart_type="line"):
return {
"type": chart_type,
"data": {
"labels": ["January", "February", "March", "April", "May", "June", "July"],
"datasets": [
{
"label": "Data",
"backgroundColor": "rgb(255, 99, 132)",
"borderColor": "rgb(255, 99, 132)",
"data": [0, 10, 5, 2, 20, 30, 45],
}
],
},
"options": {
"responsive": True,
"maintainAspectRatio": False,
},
}
chart_type = pn.widgets.RadioBoxGroup(
name="Chart Type", options=["bar", "line"], inline=True
)
chart = ChartReactComponent(
object=pn.bind(data, chart_type), height=600, sizing_mode="stretch_width"
)
pn.Column(chart_type, chart).servable()
import panel as pn
import param
from panel.custom import AnyWidgetComponent
class AnyWidgetComponent(AnyWidgetComponent):
object = param.Dict()
_esm = """
import { Chart } from "https://esm.sh/chart.js/auto"
function render({ model, el }) {
const canvasEl = document.createElement('canvas')
// Add DOM node before creating the chart
el.append(canvasEl)
const create_chart = () => new Chart(canvasEl.getContext('2d'), model.get("object"))
let chart = create_chart()
model.on("object", () => {
chart.destroy()
chart = create_chart()
})
return () => chart.destroy()
}
export default { render };
"""
def data(chart_type="line"):
return {
"type": chart_type,
"data": {
"labels": ["January", "February", "March", "April", "May", "June", "July"],
"datasets": [
{
"label": "Data",
"backgroundColor": "rgb(255, 99, 132)",
"borderColor": "rgb(255, 99, 132)",
"data": [0, 10, 5, 2, 20, 30, 45],
}
],
},
"options": {
"responsive": True,
"maintainAspectRatio": False,
},
}
chart_type = pn.widgets.RadioBoxGroup(
name="Chart Type", options=["bar", "line"], inline=True
)
chart = AnyWidgetComponent(
object=pn.bind(data, chart_type), height=400, sizing_mode="stretch_width"
)
pn.Column(chart_type, chart).servable()
Note, again, that we have to append the canvasEl
to the el
before we create the chart.
Creating a Cytoscape Pane#
This example will show you how to build a more advanced CytoscapeJS pane.
import param
import panel as pn
from panel.custom import JSComponent
class CytoscapeJS(JSComponent):
object = param.List()
layout = param.Selector(
default="cose",
objects=[
"breadthfirst",
"circle",
"concentric",
"cose",
"grid",
"preset",
"random",
],
)
style = param.String("", doc="Use to set the styles of the nodes/edges")
zoom = param.Number(1, bounds=(1, 100))
pan = param.Dict({"x": 0, "y": 0})
data = param.List(doc="Use to send node's data/attributes to Cytoscape")
selected_nodes = param.List()
selected_edges = param.List()
_esm = """
import { default as cytoscape} from "https://esm.sh/cytoscape"
let cy = null;
function removeCy() {
if (cy) { cy.destroy() }
}
export function render({ model }) {
removeCy();
const div = document.createElement('div');
div.style.width = "100%";
div.style.height = "100%";
// Cytoscape raises warning of position is static
div.style.position = "relative";
model.on('after_render', () => {
cy = cytoscape({
container: div,
layout: {name: model.layout},
elements: model.object,
zoom: model.zoom,
pan: model.pan
})
cy.style().resetToDefault().append(model.style).update()
cy.on('select unselect', function (evt) {
model.selected_nodes = cy.elements('node:selected').map(el => el.id())
model.selected_edges = cy.elements('edge:selected').map(el => el.id())
});
model.on('object', () => {cy.json({elements: model.object});cy.resize().fit()})
model.on('layout', () => {cy.layout({name: model.layout}).run()})
model.on('zoom', () => {cy.zoom(model.zoom)})
model.on('pan', () => {cy.pan(model.pan)})
model.on('style', () => {cy.style().resetToDefault().append(model.style).update()})
window.addEventListener('resize', function(event){
cy.center();
cy.resize().fit();
});
model.on('remove', removeCy)
})
return div
}
"""
pn.extension(sizing_mode="stretch_width")
elements = [
{"data": {"id": "A", "label": "A"}},
{"data": {"id": "B", "label": "B"}},
{"data": {"id": "A-B", "source": "A", "target": "B"}},
]
graph = CytoscapeJS(
object=elements,
sizing_mode="stretch_width",
height=600,
styles={"border": "1px solid black"},
)
pn.Row(
pn.Param(
graph,
parameters=[
"object",
"zoom",
"pan",
"layout",
"style",
"selected_nodes",
"selected_edges",
],
sizing_mode="fixed",
width=300,
),
graph,
).servable()
import param
import panel as pn
from panel.custom import ReactComponent
class CytoscapeReact(ReactComponent):
object = param.List()
layout = param.Selector(
default="cose",
objects=[
"breadthfirst",
"circle",
"concentric",
"cose",
"grid",
"preset",
"random",
],
)
style = param.String("", doc="Use to set the styles of the nodes/edges")
zoom = param.Number(1, bounds=(1, 100))
pan = param.Dict({"x": 0, "y": 0})
data = param.List(doc="Use to send node's data/attributes to Cytoscape")
selected_nodes = param.List()
selected_edges = param.List()
_esm = """
import CytoscapeComponent from 'https://esm.sh/react-cytoscapejs';
export function render({ model }) {
function configure(cy){
cy.on('select unselect', function (evt) {
model.selected_nodes = cy.elements('node:selected').map(el => el.id())
model.selected_edges = cy.elements('edge:selected').map(el => el.id())
});
}
const [layout] = model.useState('layout')
const [object] = model.useState('object')
const [pan] = model.useState('pan')
const [style] = model.useState('style')
const [zoom] = model.useState('zoom')
return (
<CytoscapeComponent
elements={object}
//layout={ { 'name': layout} }
zoom={zoom}
pan={pan}
stylesheet={style}
style={{ width: '100%', height: model.height, position: 'relative' }}
cy={configure}
/>
);
}
"""
pn.extension(sizing_mode="stretch_width")
elements = [
{"data": {"id": "A", "label": "A"}},
{"data": {"id": "B", "label": "B"}},
{"data": {"id": "A-B", "source": "A", "target": "B"}},
]
graph = CytoscapeReact(
object=elements,
sizing_mode="stretch_width",
height=600,
styles={"border": "1px solid black"},
)
pn.Row(
pn.Param(
graph,
parameters=[
"object",
"zoom",
"pan",
"layout",
"style",
"selected_nodes",
"selected_edges",
"height",
],
sizing_mode="fixed",
width=300,
),
graph,
).servable()
import param
import panel as pn
from panel.custom import AnyWidgetComponent
class CytoscapeAnyWidget(AnyWidgetComponent):
object = param.List()
layout = param.Selector(
default="cose",
objects=[
"breadthfirst",
"circle",
"concentric",
"cose",
"grid",
"preset",
"random",
],
)
style = param.String("", doc="Use to set the styles of the nodes/edges")
zoom = param.Number(1, bounds=(1, 100))
pan = param.Dict({"x": 0, "y": 0})
data = param.List(doc="Use to send node's data/attributes to Cytoscape")
selected_nodes = param.List()
selected_edges = param.List()
_esm = """
import { default as cytoscape} from "https://esm.sh/cytoscape"
let cy = null;
function removeCy() {
if (cy) { cy.destroy() }
}
function render({ model, el }) {
removeCy();
cy = cytoscape({
container: el,
layout: {name: model.get('layout')},
elements: model.get('object'),
zoom: model.get('zoom'),
pan: model.get('pan')
})
cy.style().resetToDefault().append(model.get('style')).update()
cy.on('select unselect', function (evt) {
model.set("selected_nodes", cy.elements('node:selected').map(el => el.id()))
model.set("selected_edges", cy.elements('edge:selected').map(el => el.id()))
model.save_changes()
});
model.on('change:object', () => {cy.json({elements: model.get('object')});cy.resize().fit()})
model.on('change:layout', () => {cy.layout({name: model.get('layout')}).run()})
model.on('change:zoom', () => {cy.zoom(model.get('zoom'))})
model.on('change:pan', () => {cy.pan(model.get('pan'))})
model.on('change:style', () => {cy.style().resetToDefault().append(model.get('style')).update()})
window.addEventListener('resize', function(event){
cy.center();
cy.resize().fit();
});
}
export default { render };
"""
_stylesheets=["""
.__________cytoscape_container {
position: relative;
}
"""]
pn.extension(sizing_mode="stretch_width")
elements = [
{"data": {"id": "A", "label": "A"}},
{"data": {"id": "B", "label": "B"}},
{"data": {"id": "A-B", "source": "A", "target": "B"}},
]
graph = CytoscapeAnyWidget(
object=elements,
sizing_mode="stretch_width",
height=600,
styles={"border": "1px solid black"},
)
pn.Row(
pn.Param(
graph,
parameters=[
"object",
"zoom",
"pan",
"layout",
"style",
"selected_nodes",
"selected_edges",
],
sizing_mode="fixed",
width=300,
),
graph,
).servable()