Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
holoviz
GitHub Repository: holoviz/panel
Path: blob/main/doc/how_to/custom_components/esm/custom_panes.md
2012 views

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.

::::{tab-set}

:::{tab-item} JSComponent

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. :::

:::{tab-item} ReactComponent

import panel as pn import param from panel.custom import ReactComponent class ChartReactComponent(ReactComponent): object = param.Dict() _esm = """ import { Chart } from 'https://esm.sh/[email protected]'; import { Chart as ChartJS, registerables } from "https://esm.sh/[email protected]"; 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()

:::

::: {tab-item} AnyWidgetComponent

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.

::::{tab-set}

:::{tab-item} JSComponent

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()

:::

:::{tab-item} ReactComponent

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()

:::

:::{tab-item} AnyWidgetComponent

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()

:::

::::