JS callbacks on ReactiveHTML component?

Hello :slight_smile:
Well done for the great Holoviz tools!

I would like to make an Echarts plot with ReactiveHTML and add a multi-choice to add or remove lines. The code works fine when serving it. When I save it to use it as standalone html with interactivity, it doesn’t work due to the lack of JS callbacks.

Is there any way to add JS callbacks to the ReactiveHTML components??

Otherwise, I should implement inside the component the associated multi choice widget with vanilla JS and HTML, which also works, but I prefer the Panel widget.

import panel as pn
import param
from panel.reactive import ReactiveHTML
pn.extension(js_files={'echarts': 'https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js'})
import numpy as np

x = list(range(7))
dataxx = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

data = {
    'Apple': [18, 3, 6, 4, 5, 6, 4],
    'Banana': [28, 3, 6, 4, 5, 6, 4],
    'Cherry': [38, 3, 6, 4, 5, 6, 4],
    'Date': np.random.randn(7).cumsum().tolist(),
}

multi = pn.widgets.MultiChoice(
    name='Select Fruits',
    options=list(data.keys()),
    value=['Apple']
)
series = []
@pn.depends(multi)
def makeseries(fruits):
    series = []

    for fruit in fruits:
        series.append({
            'name': fruit,
            'data': data[fruit],
            'type': 'line',
            'smooth': True
        })
    return series
        
class CustomComponent(ReactiveHTML):
    datax = param.List(default=dataxx)
    datay = param.List(default=series)
    _template = """<div id="chartDom" style="height: 100%;width:100%"></div>"""
    _scripts = {"render": """
option = {
  tooltip: {
      trigger: 'axis'
    },
  xAxis: {
    type: 'category',
    data: data.datax
  },
  yAxis: {
    type: 'value'
  },
  series: data.datay
};
const config = {width: 1200, height: 500}
state.myChart = echarts.init(chartDom,null,config);
state.myChart.setOption(option);
"""
    }

@pn.depends(multi)
def chart(fruits):
    component = CustomComponent(datay = makeseries(fruits), datax = ['aa','bb','cc','dd','ee','ff','gg']);  
    return component

app = pn.Column(
    "## MultiChoice ECharts Line Chart",
    multi,
    chart
)

# app.show() 
app.save("p1.html", embed=True)

Thanks in advance!
Cheers.

Sorry for the slow reply. I’d strongly suggest you implement this as a JSComponent which supports adding a jscallback and provides a much cleaner approach for building custom components.

1 Like

Cool, and thanks for the suggestion. I use .jslink() to update the JSComponent:

import panel as pn
import param
import numpy as np
from panel.custom import JSComponent

pn.extension(js_files={'echarts': 'https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js'})

# Data setup
dataxx = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
data = {
    'Apple': [18, 3, 6, 4, 5, 6, 4],
    'Banana': [28, 3, 6, 4, 5, 6, 4],
    'Cherry': [38, 3, 6, 4, 5, 6, 4],
    'Date': np.random.randn(7).cumsum().tolist(),
}

class EChartsComponent(JSComponent):
    datax = param.List(default=dataxx)
    selected_fruits = param.List(default=['Apple'])

    # Inject fruit data as a static field (converted to JSON on Python->JS bridge)
    fruit_data = param.Dict(default={k: v for k, v in data.items()})

    _esm = """
    export function render({ model }) {
      let div = document.createElement("div");
      div.style.height = "500px";
      div.style.width = "100%";

      function renderChart() {
        if (!window.echarts) { setTimeout(renderChart, 100); return; }
        if (!div._echart) {
          div._echart = echarts.init(div, null, {width: 1200, height: 500});
        }
        // Get data from the model
        const fruits = model.selected_fruits || [];
        const fruitData = model.fruit_data || {};
        // Build series
        const series = [];
        fruits.forEach(fruit => {
          if (fruitData[fruit]) {
            series.push({
              name: fruit,
              data: fruitData[fruit],
              type: 'line',
              smooth: true
            });
          }
        });
        const option = {
          tooltip: { trigger: 'axis' },
          legend: { data: fruits },
          xAxis: { type: 'category', data: model.datax },
          yAxis: { type: 'value' },
          series: series
        };
        div._echart.setOption(option, true);
      }

      renderChart();
      // React to changes
      model.on('selected_fruits', renderChart);
      model.on('datax', renderChart);
      model.on('fruit_data', renderChart);

      return div;
    }
    """

# MultiChoice widget
multi = pn.widgets.MultiChoice(
    name='Select Fruits',
    options=list(data.keys()),
    value=['Apple']
)

# Component instance
chart_component = EChartsComponent(datax=dataxx, selected_fruits=multi.value)

# Sync MultiChoice to chart
multi.jslink(chart_component, value='selected_fruits')

app = pn.Column(
    "## MultiChoice ECharts Line Chart (Panel JSComponent, all-JS updates)",
    pn.pane.Markdown("Select fruits from the dropdown to update the chart. All updates happen in JavaScript!"),
    multi,
    chart_component
)

app.show()
app.save("p_jslink.html", embed=True)

And also here is the version with ReactiveHTML:

import panel as pn
import param
from panel.reactive import ReactiveHTML

pn.extension(js_files={'echarts': 'https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js'})

import numpy as np

# Data setup
dataxx = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
data = {
    'Apple': [18, 3, 6, 4, 5, 6, 4],
    'Banana': [28, 3, 6, 4, 5, 6, 4],
    'Cherry': [38, 3, 6, 4, 5, 6, 4],
    'Date': np.random.randn(7).cumsum().tolist(),
}

# MultiChoice widget
multi = pn.widgets.MultiChoice(
    name='Select Fruits',
    options=list(data.keys()),
    value=['Apple']
)

class CustomComponent(ReactiveHTML):
    datax = param.List(default=dataxx)
    selected_fruits = param.List(default=['Apple'])
    
    _template = """<div id="chartDom" style="height: 500px; width: 100%;"></div>"""
    
    _scripts = {
        "render": """
            // Initialize the chart
            const config = {width: 1200, height: 500};
            state.myChart = echarts.init(chartDom, null, config);
            
            // Store fruit data in state for JavaScript access
            state.fruitData = {
                'Apple': [18, 3, 6, 4, 5, 6, 4],
                'Banana': [28, 3, 6, 4, 5, 6, 4],
                'Cherry': [38, 3, 6, 4, 5, 6, 4],
                'Date': """ + str(data['Date']) + """
            };
            
            // Define the updateChart function first
            state.updateChart = () => {
                console.log('Updating chart with fruits:', data.selected_fruits);
                
                const series = [];
                
                data.selected_fruits.forEach(fruit => {
                    if (state.fruitData[fruit]) {
                        series.push({
                            name: fruit,
                            data: state.fruitData[fruit],
                            type: 'line',
                            smooth: true
                        });
                    }
                });
                
                const option = {
                    tooltip: { trigger: 'axis' },
                    legend: { data: data.selected_fruits },
                    xAxis: { type: 'category', data: data.datax },
                    yAxis: { type: 'value' },
                    series: series
                };
                
                if (state.myChart) {
                    state.myChart.setOption(option, true);
                }
            };
            
            // Initial render
            state.updateChart();
        """,
        
        "selected_fruits": """
            // This script runs when selected_fruits parameter changes
            if (state.updateChart) {
                state.updateChart();
            }
        """
    }

# Create the chart component
chart_component = CustomComponent(datax=dataxx, selected_fruits=multi.value)


multi.jslink(chart_component, value='selected_fruits')

app = pn.Column(
    "## MultiChoice ECharts Line Chart (JavaScript Callbacks)",
    pn.pane.Markdown("Select fruits from the dropdown to update the chart. All updates happen in JavaScript!"),
    multi,
    chart_component
)
app.show()
app.save("p1_jslink.html", embed=True)