joel.wilson
joel.wilson

Reputation: 8413

Bokeh - How to return objects from EventHandler functions

I want to return a data.frame object when a upload button(code is from here) is clicked within the bokeh app. Below code,

## Load library
import pandas as pd
from xlrd import XLRDError
import io
import base64

from bokeh.layouts import row, column, widgetbox, layout
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Button, Div, TextInput, DataTable, TableColumn, Panel, Tabs
from bokeh.io import curdoc
from bokeh.plotting import figure

sheet_names_expected = ['Sheet'+str(i) for i in range(1,7)]
## Uploaded Data Handling 
file_source = ColumnDataSource({'file_contents':[], 'file_name':[], 'upload_message':[]})

def file_callback(attr,old,new):
    print('filename:', file_source.data['file_name'])
    #print('filename:', type(file_source.data['file_contents'][0]))
    raw_contents = file_source.data['file_contents'][0]

    prefix, b64_contents = raw_contents.split(",", 1)
    file_contents = base64.b64decode(b64_contents)
    file_io = io.BytesIO(file_contents)

    try:
        df_dict = pd.read_excel(file_io, sheet_name = sheet_names_expected)
        file_source.data['upload_message'] = 'Successfully loaded data'
    except XLRDError:
        file_source.data['upload_message'] = 'One or more of the sheet names are mis-spelled/missing.\n'
    except :
        file_source.data['upload_message'] = 'Error occured while uploading the file. Ensure it\'s a xlsx file'

## How to now have the 6 dataframes within `df_dict` outside the callback function for further analysis and plotting ?



file_source.on_change('data', file_callback)
###########################################################################
## Upload Button Widget
###########################################################################
button = Button(label="Upload Data", button_type="success")
# when butotn is clicked, below code in CustomJS will be called
button.callback = CustomJS(args=dict(file_source=file_source), code = """
function read_file(filename) {
    var reader = new FileReader();
    reader.onload = load_handler;
    reader.onerror = error_handler;
    // readAsDataURL represents the file's data as a base64 encoded string
    reader.readAsDataURL(filename);
}

function load_handler(event) {
    var b64string = event.target.result;
    file_source.data = {'file_contents' : [b64string], 'file_name':[input.files[0].name]};
    file_source.trigger("change");
}

function error_handler(evt) {
    if(evt.target.error.name == "NotReadableError") {
        alert("Can't read file!");
    }
}

var input = document.createElement('input');
input.setAttribute('type', 'file'); 
input.onchange = function(){
    if (window.FileReader) {
        read_file(input.files[0]);
    } else {
        alert('FileReader is not supported in this browser');
    }
}
input.click();
""")

Not sure how to use ColumnDataSource . The excel_file has 6 tabs and thus 6 pandas data.frames will be read into a dictionary by pd.read_excel()

Upvotes: 1

Views: 1345

Answers (1)

Anthonydouc
Anthonydouc

Reputation: 3364

If I understand your question correctly, then I think you are somewhat confused as to how bokeh works. I recommend you read through the documentation, and understand how some of the examples in the gallery work.

Never the less, I believe you need to use a ColumnDataSource to achieve what you want. First you can read in your data, then load it into one or more columndatasources. Then when you press the button, it will execute all of the code within the callback function. Read here:

https://docs.bokeh.org/en/latest/docs/user_guide/data.html#columndatasource

The callback function here does the following:
1: access the existing column source data
2: run other python functions and then
3: update the existing data, which is stored in the CDS. This will automatically update the plot.

If you do not use a column data source, there is no way to update the plot without recreating it and updating the html document it is displayed within.

from os.path import dirname, abspath
import pandas as pd
from bokeh.layouts import row
from bokeh.plotting import figure
from bokeh.plotting import curdoc
from bokeh.models.widgets import Button
from bokeh.models import ColumnDataSource

button = Button(label='Click')

# replace this with file i/o
df = pd.DataFrame({'x':[1,2,3,4],'y':[10,20,30,40]})

# create a column data source from the read in data
# source.data = dictionary containing the data
# using a CDS will allow any plots or tables
# to automatically update when the source.data
# is modified

source = ColumnDataSource(df)

def mycalc(x):
    """
    Arbitary python function
    """
    x *= 2
    return x


def callback():
    x = source.data['x']
    # compute python based functions here to manipulate data
    x = mycalc(x)
    # update the column data source with the new data
    source.data['x'] = x

p = figure(plot_width=400, plot_height=400)

p.circle('x','y', source=source)

button.on_click(callback)
curdoc().add_root(row([button, p]))

Upvotes: 1

Related Questions