Passing data from Python to javascript and causing execution of a function

How should I really implement this?

I have some javascript plots I would like to revive:
To do so, I need to pass data to a javascript function and cause it to update its plots.
(Note I want more than one such plot at a time)

A way that works is as follows:

import panel as pn
import numpy as np
import uuid

def create_vector_app(nm):
    # Generate a unique ID for the container and JavaScript function
    unique_id = f"app_{uuid.uuid4().hex[:8]}"

    # Create a slider to adjust the size of the vector
    size_slider = pn.widgets.IntSlider(name=f"{nm}: Vector Size", start=1, end=10, value=5)

    # Create a button to generate a random vector
    generate_button = pn.widgets.Button(name=f"{nm} Generate Vector", button_type="primary")

    # Create a pane to include the JavaScript logic
    js_pane = pn.pane.HTML(f"""
    <div id="{unique_id}">Waiting for vector...</div>
    <script>
        // Ensure the function and variable are immediately available globally
        (function() {{
            let vector = []; // Local vector for this instance
            let container = document.getElementById("{unique_id}");

            function updateVector(newVector) {{
                vector = newVector; // Update the local vector
                console.log("Instance {unique_id} vector:", vector); // Log to console
                if (container) {{
                    container.innerHTML = "Vector: " + JSON.stringify(vector); // Display vector in the container
                }}
            }}

            // Expose the updateVector function to the global window immediately
            window["updateVector_{unique_id}"] = updateVector;
        }})();
    </script>
    """, height=50)

    # Define a callback to update the vector in JavaScript
    def update_vector(event):
        # Generate a random vector based on the slider value
        vector = np.random.rand(size_slider.value).tolist()
        # Generate JavaScript code to call the local updateVector function
        js_code = f"""
        window["updateVector_{unique_id}"]({vector});
        """
        js_pane.object = f"<script>{js_code}</script>"

    # Attach the callback to the button click event
    generate_button.on_click(update_vector)

    # Layout the widgets and the display pane
    return pn.Column(
        size_slider,
        generate_button,
        js_pane,
        f"Open the browser console (F12 -> Console) to see the vector for instance {unique_id}."
    )

# Create two independent instances
vector_app1 = create_vector_app("1")
vector_app2 = create_vector_app("2")

# Serve the apps
pn.Column( vector_app1, vector_app2 ).servable()
  • Might there be an easier solution?
  • Is there a way to do the opposite? Have a javascript function pass data back to python and trigger some action in python? (I have not yet figured out any way to do that… :frowning: )

The answer to any JS ↔ Python communication question these days will always be JSComponent, in this case you can send data on a param.Array parameter.

Thank you @philippjfr
This is a working solution I arrived at following your suggestion:

Send an Array for Python to Javascript

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

pn.extension()

class ArrayLogger(JSComponent):
    """
    A custom JSComponent where Python sends an array, and JavaScript logs it to the console.
    Each instance is uniquely identified.
    """

    # Explicitly define parameters
    array = param.List(default=[])  # Array to send to JavaScript
    logger_id = param.String(default="")  # Unique ID for the logger instance

    _esm = """
export function render({model, el}) {
    console.log(`ArrayLogger initialized (ID: ${model.logger_id})`);

    // Create a display area in the DOM
    const output = document.createElement('div');
    output.style.padding = "10px";
    output.style.border = "1px solid #ddd";
    output.style.marginTop = "10px";
    output.style.fontFamily = "monospace";
    output.style.background = "#f9f9f9";
    output.textContent = `Logger ${model.logger_id}: No array received yet.`;
    el.appendChild(output);

    // Watch for changes in the 'array' parameter
    model.on('change:array', () => {
        const receivedArray = model.array;
        console.log(`Logger ${model.logger_id} - Received array:`, receivedArray);

        // Update the display
        if (Array.isArray(receivedArray)) {
            output.textContent = `Logger ${model.logger_id}: Array Received: ` + JSON.stringify(receivedArray);
        } else {
            output.textContent = `Logger ${model.logger_id}: Invalid array received.`;
        }
    });
}
"""

# Instantiate two ArrayLogger components with unique IDs
array_logger_1 = ArrayLogger(logger_id="1")
array_logger_2 = ArrayLogger(logger_id="2")

# Buttons to trigger sending arrays to each logger
send_button_1 = pn.widgets.Button(name="Send Array to JS - Logger 1", button_type="primary")
send_button_2 = pn.widgets.Button(name="Send Array to JS - Logger 2", button_type="primary")

# Callbacks to send arrays to JavaScript
def send_array_to_js_1(event):
    example_array = [1.23, 4.56, 7.89, 10.11, 12.34]
    array_logger_1.array = example_array
    print("Sent array to Logger 1:", example_array)

def send_array_to_js_2(event):
    example_array = [9.87, 6.54, 3.21, 0.98, 0.12]
    array_logger_2.array = example_array
    print("Sent array to Logger 2:", example_array)

# Attach callbacks to buttons
send_button_1.on_click(send_array_to_js_1)
send_button_2.on_click(send_array_to_js_2)

# Layout to display the app
layout = pn.Column(
    pn.pane.Markdown("### Python Sends Arrays to Two Independent JavaScript Loggers"),
    pn.Row(
        pn.Column(
            "#### Logger 1",
            send_button_1,
            array_logger_1
        ),
        pn.Column(
            "#### Logger 2",
            send_button_2,
            array_logger_2
        )
    )
)

# Serve the app
layout.servable()

Send an array from Javascript to Python

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

pn.extension()

class RandomArrayGenerator(JSComponent):
    """
    A custom JSComponent that generates two arrays of random floats and sends them to Python.
    """

    # Parameter to store the generated array
    value = param.String(default="")

    _esm = """
export function render({model, el}) {
    console.log("RandomArrayGenerator initialized.");

    // Create a container to display generated arrays
    const output = document.createElement('div');
    output.style.padding = "10px";
    output.style.border = "1px solid #ddd";
    output.style.marginTop = "10px";
    output.style.fontFamily = "monospace";
    output.style.background = "#f9f9f9";
    output.textContent = "Waiting for data...";
    el.appendChild(output);

    // Track the number of updates
    let count = 0;

    // Function to generate a random array of floats
    const generateArray = () => {
        if (count >= 2) {
            clearInterval(interval); // Stop after sending two arrays
            console.log("Stopped after sending two arrays.");
            return;
        }

        count += 1;

        const randomArray = Array.from({length: 10}, () => Math.random().toFixed(2));
        console.log("Generated Array:", randomArray);

        // Update the display
        output.textContent = "Generated Array: " + JSON.stringify(randomArray);

        // Notify Python by updating the `value` parameter
        model.value = JSON.stringify(randomArray);
    };

    // Start generating random arrays every 3 seconds
    const interval = setInterval(generateArray, 3000);

    // Cleanup when the component is removed
    model.on('change', (changed) => {
        if (changed.value === undefined) {
            clearInterval(interval);
            console.log("RandomArrayGenerator stopped.");
        }
    });
}
"""

# Instantiate the custom JSComponent
random_array_generator = RandomArrayGenerator()

# Widget to display the received array in Python
data_display = pn.widgets.StaticText(name="Received Array", value="Waiting for data...")

# Python callback to process the data when `value` changes
@pn.depends(random_array_generator.param.value, watch=True)
def update_display(value):
    import json
    try:
        array = json.loads(value)
        formatted = ", ".join(f"{float(x):.2f}" for x in array)
        data_display.value = f"Received Array: {formatted}"
    except (json.JSONDecodeError, ValueError):
        data_display.value = "Invalid data received"

# Layout of the Panel app
layout = pn.Column(
    pn.pane.Markdown("### Random Float Array Generator (Sends Two Arrays Only)"),
    pn.Row(
        pn.Column(
            "# JavaScript generates two random arrays and stops.",
            random_array_generator
        ),
        pn.Column(
            "# Python receives and displays the array:",
            data_display
        )
    )
)

# Serve the app
layout.servable()