Embedding Bokeh into HTML with Pyscript, a CSS resize handle & Custom JS callback passing results back to a div on our html page
Tags: python, javascript, webdev, tutorial
Published: 2024-06-28 12:21:47

Here we explore a deeper dive into Pyscript where all my code below is circa 2024 (the newest version of Pyscript). 

Pyscript allows us to embed a Bokeh Chart into HTML. Then we make our html interactive by adding some CSS with a resize handle. We also make our bokeh interactive using the stretch_both method which allows us to entirely fill the html div we are embedding into. When we drag the resize handle either up, down or sideways, our bokeh chart responds accordingly and also renders the plot width and height coordinates using a JS callback with custom JS.

In the old days, prior to 2022 when Pyscript was invented, in order to embed bokeh into html we needed bokeh on a server along with lots of python and javascript to make this solution work. Now Pyscript shortens the process with the embed.embed_item method and ffi (foreign function interface, circa pyscript 2024) which converts a Python object into its JavaScript counterpart, thus enabling web interactivity.


**Embedding vs showing**
In Pyscript we are embedding and in Python we are showing. Both involve an html file but it is the Pyscript html file we want to write our code in and share with others.

More specifically when working with Bokeh directly in Python there is the show() method and in Pyscript we have the embed.embed_item() method and these are separate ways to render Bokeh.

Our show() method creates an output html file and opens bokeh in a browser. This bare html template is not what we want because it is not usable code, it is mainly a combo of json and javascript just to render a page. The link to this html page is not the link we want to provide others either. Show() actually creates an html file after running from Python but we want to be in the html before running so we can manage our code.  When writing Pyscript we start with html and script tags in a notepad and then save as html on our local drive before we click the link to run it. We will use this link to share with others.


**Interactivity Everywhere**
In this example the idea of interactivity encompasses more than just bokeh interactivity, it also combines it with html/css interactivity and javascript interactivity (ffi), plus optional custom js if needed. HTML, CSS, Bokeh and Javascript all play nicely together which is why I chose Bokeh in the first place. Bokeh offers a stretch_both method which allows the bokeh chart to completely fill the html object which is a div in this case. Then we apply pure css to resize the div and as div is wider so is bokeh. Our custom js works here too, so lots of interaction happening in our example below.

**Pyscript link, not the show() link**
In this use case we can save the Pyscript link to a remote html web server so as to provide a public link to others. When the user clicks the link he can also press ctrl u to view its source.

Here is my public link
https://rickd.s3.us-east-2.amazonaws.com/Pyscript+Template+with+div+resize+handle_2.html

With this pyscript link we can copy and edit the code in a notepad and then resave to your local link or upload to a public domain.

**Custom JS in a Callback**
Custom Javascript is optional and adds more interactivity to our app. This custom JS component allows us to pass Python args into the javascript and then out to an html div. We can trigger an event to call the callback which executes the custom js. Python allows us to build bokeh in Pyscript and Javascript allows us to build interactivity in our callback where we respond to the python event. Here in our example, the event is where we drag the resizing handle which triggers some custom js to display a result in an html div. Pyscript provides a healthy environment where Python, Javascript and HTML can play and interact together in harmony.



**My code - Pyscript, Python, CSS all on one sharable HTML page**

The purpose of this example is to show how an elongated grid distorts trend lines and angles. The chart grid lines need to be equalized or squared in order to give us an aspect ratio of 1:1 between height and width of the grid lines for the trends on a line chart to be accurate. For example, a stock chart has normally elongated grid lines thus flattening the angle of any slopes. This is why angles should never be applied to stock prices unless the height and width of grid lines are equal.

Note: in a previous version of bokeh the resizing method was abandoned so we need to do resizing on the html side now which is why I provide this example.


So here it is
```js
<!DOCTYPE html>
<!-- here we embed bokeh into html and use pure css resize syntax along with stretch_both-->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Using Pyscript, embed Bokeh into html, with resize handle and interactive results</title>
    <link rel="stylesheet" href="https://pyscript.net/releases/2024.6.1/core.css"> <!--most recent pyscript lib-->
    <script type="module" src="https://pyscript.net/releases/2024.6.1/core.js"></script> <!--most recent pyscript lib-->
    <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.4.1.min.js"></script> <!--recent bokeh lib-->
    <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.1.min.js"></script> <!--need for div to work-->
       <py-config>packages = ["bokeh"]</py-config>
<style>
.div {
  border: 2px solid;  padding: 20px; 
  width: 450px; /* here is where we size our html div then stretch_both makes plot below fill the div entirely*/
  height: 450px;  resize: both;  overflow: auto;
}
</style></head>
<body>
    <h3>wait 8 seconds to display Bokeh Line Chart, then drag resize handle in lower right corner and changes will display</h3>
    <p>as we widen the plot, the angle of the slope becomes flatter, squaring the plot provides accurate trend line, elongating the plot distorts the trendline</p>
        <script type="py" async>
        from bokeh.plotting import column, figure
        from bokeh.models import ColumnDataSource, Div, CustomJS
        from math import atan2, degrees
        from bokeh.embed import json_item
        from pyscript import window, fetch, ffi 
        Bokeh = window.Bokeh   #pass Bokeh into the main threads window object
        async def get_data():
            p = figure(x_range=(0,7),y_range=(0,7), sizing_mode="stretch_both")
            p.xgrid.grid_line_color = 'black'
            p.ygrid.grid_line_color = 'black'
            p.toolbar.autohide = True
            div = Div(text = ' ')  #initialize/define blank div and div2 here
            div2= Div(text = ' ')   
                #add angle line
            source = ColumnDataSource( data = {'x': [2,4], 'y': [3,5]} )
            r_c = p.scatter(x = 'x', y = 'y', size = 10, fill_color = 'red', line_color = 'black', source=source )
            r_l = p.line(x = 'x', y = 'y', line_color = 'blue', line_width = 3, source = source )     
                #add callback
            callback = CustomJS(
                     #pass above python vars as args here...we are declaring vars in our custom js
                args = {'p':p, 'div2': div2, 'div': div},
                code = '''
                    const dx = `${p.outer_width.toFixed(2)}`
                    const dy = `${p.outer_height.toFixed(2)}`
                    const angle = (180/Math.PI)*(Math.atan2(dy, dx));
                    div.text = `width = `+`${p.outer_width.toFixed(2)} `+ ` ......height = `+` ${p.outer_height.toFixed(2)}`;
                    div2.text = ` angle = `+`${angle.toFixed(2)}`+` degrees`;
                    const result2 = div.text;  //this is width / height...passing txt into result2
                    const result3 = div2.text;             //passing div2 text into result3
                          //prepare to send div to output tags outside pyscript in main body
                    let element = ' ';  //declare empty element
                    element = document.createElement('h3'); //h3 is the font size for output div below
                    element.innerText = result2;
                    document.getElementById("output").innerHTML = ""; //clears what is in output div so we dont keep appending
                    document.getElementById("output").append(element); //send element to output in html area outside pyscript tag
                    let element2 = ' ';  //declare empty element
                    element2 = document.createElement('h3');
                    element2.innerText = result3;
                    document.getElementById("output2").innerHTML = "";
                    document.getElementById("output2").append(element2);
                '''
            )
            p.js_on_change("outer_width", callback) #when outer width changes (the event) then execute custom js inside the callback above
            p.js_on_change("outer_height", callback)
            await Bokeh.embed.embed_item(ffi.to_js(json_item(p, "chart"))) #ffi converts a Python object into its JavaScript counterpart
        await get_data()
    </script>
    <div id="chart" class="div"></div> <!--bokeh chart renders in this div-->
    <div id="output"></div> <!--width and length render here-->
    <div id="output2"></div> <!-- degree of angle renders here-->
</body>
</html>
```
Happy Coding !!