Plotting FileInput in standalone HTML script. Is that possible?

Hello,
I am very new to bokeh and webapplication stuff. I am Trying to create a simple python script with bokeh that is supposed to spit out a standalone HTML file, so that it runs without any server/ “bokeh serve”, etc. in the background. That HTML file should simply load a .csv file using the FileInput widget and then plots it as a line plot. It sounded simple for me, but for some reason the HTML doesn’t plot the file… Here is the code:


from bokeh.plotting import save, show, output_file, figure
from bokeh.models import FileInput, ColumnDataSource
from bokeh.layouts import column

import base64
import io
import pandas as pd
from math import pi


def loadData(attr, old, new):
    
   decoded = base64.b64decode(new) #file upload encodes file in bas64 --> need to decode it
   fil = io.BytesIO(decoded)
   df = pd.read_csv(fil, header=None, names=['Date', 'HH']) #read in csv file with no header and declare header 'Date' and 'HH'
   df['Date'] = pd.to_datetime(df['Date'], format='%Y-%m-%d %H:%M:%S') #converting Str to date Type
   plt.line(df['Date'], df['HH'])
    

df = pd.DataFrame()
src = ColumnDataSource(df)
output_file('SpitItOut.html') 

file_input = FileInput(accept='.csv', width=200)

file_input.on_change('value', loadData)

#Plotting
plt = figure(
    title="PLOT IT, DAMN IT !!!!!!!",
    x_axis_label="Date",
    x_axis_type='datetime', # sets date as axis type (simple layout format)
    y_axis_label="Hydraulic Head [m]"
    )

plt.xaxis.major_label_orientation = pi/4 #shows x axis label with 45 degree angle

bundle = column(file_input, plt)
#Output
show(bundle)
save(bundle, output_file)


Can you please tell me if it is even possible, what i try to do? If yes, what am I doing wrong? I
Thank you very much in advance.

all the Best

Hey @CarlMarx please edit your post to use code formatting so that the code is intelligible (either with the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks).

To build “standalone”, you can’t use python code running as a callback. Web browsers don’t have python, let alone your python environment (pandas etc) in them, so the whole “bokeh serve” routine essentially works by the user sending a request to your host computer (i.e. the server), the host computer executes the python function, then the result gets sent back to the user. In standalone apps, that back and forth doesn’t exist —> all the work gets done on the user’s computer, which is awesome… but that callback/function has to be written in javascript (because that’s the language of web browsers). So to build your application, your function “loadData” needs to be “translated” into a CustomJS model containing the necessary javascript code (and trust me if you’re new to JS and its paradigms you can expect to struggle… a lot). But also trust me, it’s worth it :slight_smile:

Then, instead on on_change , and pointing to the python function (i.e. this line):

file_input.on_change('value',loadData)

You tell file_input to execute the CustomJS using js_on_change, like this:

file_input.js_on_change('value',CustomJS_version_of_loadData_function)

Now to build that CustomJS model I can probably help (I have built this kind of thing before), but i’d recommend building yourself a more basic CustomJS based mickey mouse type thing first just to wrap your head around how that works first → see JavaScript callbacks — Bokeh 2.4.2 Documentation

The only reason I suggest this is based on my personal experience learning this stuff: writing a JS based csv parser is a lot more JS than I’d want to take on before I had a good handle on the core mechanics of the CustomJS callback and basic JS syntax etc.

2 Likes

Thank you very much, that is very helpful…

Here is a small update to the question:
thanks to @gmerritt123 I wrote this piece of JS code to read and parse the data from my chosen .csv file.
here is the code:

var obj_csv = {
                size: 0,
                dataFile: []
              };

              var obj_table = {
                datetime: [],
                hh: []
              };

              function parseData(tableData){                
                tableData.forEach( row => {
                  let date = new Date(row[0]); //create new Date object with specific date
                  obj_table.datetime.push(date); //load Date obj in array
                  obj_table.hh.push(parseFloat(row[1])); //convert str inte float number and load it in array
                });
              }

              function readCSV(input){
              console.log(input)
                if(input.files && input.files[0]){
                  let reader = new FileReader();
                  reader.readAsBinaryString(input.files[0]);
                  reader.onload = function(e){
                    obj_csv.size = e.total;
                    obj_csv.dataFile = e.target.result
                    //console.log(obj_csv.dataFile);
                    parseCSV(obj_csv.dataFile);
                  }
                }
              }

              function parseCSV(data){
                let csvData = [];
                let lbreak = data.split("\n"); //split into rows by '\n' as seperator
                lbreak.forEach(res => {
                csvData.push(res.split(",")); //split into columns by ',' as seperator
                });
                parseData(csvData);
              }

              const uplFile = document.getElementById('uploadFile');
              uplFile.onchange = () => {
                readCSV(uplFile);
                console.table(obj_table);
              }

Like @gmerritt123 said, I applyed the code above with:

file_input.js_on_change('value',CustomJS_version_of_loadData_function)

in my bokeh.py file and it works.
thanks again and I hope this might help others as well.

2 Likes

Fantastic! Thanks so much for sharing, this is very helpful.

2 Likes